luv

Workshop wiki

tracy.lisp

hal/tracy.lisp

system luv · 40 definitions · on GitHub

A Tracy profiler client for luv's CPU zones.

Tracy takes every name by pointer and asks about it later, when a viewer wants to know what a zone it has already seen was called. Nothing handed to the profiler may therefore die with the call that handed it over, so names and source locations live in never-freed foreign memory, interned one copy per distinct zone rather than one per macro expansion.

The client is on-demand: it collects nothing until a viewer connects, so leaving the profiler started in a durable image costs a predictable nothing rather than an ever-growing buffer.

It is also built for delayed initialization, which buys the image control over when the profiler starts at the price of a sharp edge: every entry point here asserts that the profiler is running, and a failed assertion ends the process rather than signalling something a handler could catch. Everything below is therefore gated on *TRACY*, and that gate is the one invariant this file cannot be sloppy about.

in-package#:luv

Loading the client.

The flake builds a client with TRACY_ON_DEMAND and TRACY_MANUAL_LIFETIME and names it in luv_TRACY_CLIENT. A client from somewhere else still works; it may simply start collecting the moment it is loaded, and start-tracy notices that it has no lifetime entry points to call.

cffi:define-foreign-librarytracy-client
:darwin
:or"libTracyClient.dylib""libTracyClient.0.14.0.dylib"
:unix
:or"libTracyClient.so""libTracyClient.so.0.14.0"
t
:default"libTracyClient"
defvar*tracy-client*nil"The loaded Tracy client library, or NIL before anything asked for one."
defparameter*tracy-client-required-shim-symbols*'
"luv_tracy_emit_zone_begin""luv_tracy_emit_zone_end""luv_tracy_emit_zone_value""luv_tracy_zone_depth"
"Native context-stack entry points required by luv's Tracy zone ABI."
defuntracy-client-override

Return the client luv_TRACY_CLIENT names, when it names one that exists.

let
name
uiop:getenv"LUV_TRACY_CLIENT"
when
andname
plusp
lengthname
probe-filename
defunload-tracy-client

Load the Tracy client library, preferring the one luv_TRACY_CLIENT names.

or*tracy-client*
let*
client
ifoverride
cffi:load-foreign-library
namestringoverride
cffi:use-foreign-librarytracy-client
missing
remove-if
lambda
name
cffi:foreign-symbol-pointername:libraryclient
*tracy-client-required-shim-symbols*
whenmissing
ignore-errors
cffi:close-foreign-libraryclient
error"The Tracy client~@[ at ~A~] lacks luv's native zone-context shim: ~{~A~^, ~}.~%Use the Tracy client built by this checkout."overridemissing
setf*tracy-client*client
defuntracy-client-available-p

Answer whether a Tracy client can be loaded, without complaining if not.

handler-case
error
nil

The C API.

The entry points are spelled with the three leading underscores TracyC.h gives them; the Mach-O underscore on top of that is dlopen's business, not ours.

cffi:defcstructtracy-source-location
name:pointer
zone-function:pointer
file:pointer
line:uint32
color:uint32

TracyCZoneCtx is deliberately opaque. In Tracy 0.14 it grew from two 32-bit fields to those fields plus a 64-bit on-demand connection id. The native shim retains exact contexts in a thread-local nesting stack, so the Lisp hot path neither translates a structure through libffi nor allocates a Lisp representation that can go stale at the next Tracy upgrade.

cffi:defcfun
"___tracy_startup_profiler"%tracy-startup-profiler
:void
cffi:defcfun
"___tracy_shutdown_profiler"%tracy-shutdown-profiler
:void
cffi:defcfun
"___tracy_connected"%tracy-connected
:int32
cffi:defcfun
"luv_tracy_emit_zone_begin"%tracy-emit-zone-begin
:void
source-location:pointer
active:int32
cffi:defcfun
"luv_tracy_emit_zone_end"%tracy-emit-zone-end
:void
cffi:defcfun
"luv_tracy_emit_zone_value"%tracy-emit-zone-value
:void
value:uint64
cffi:defcfun
"luv_tracy_zone_depth"%tracy-zone-depth
:size
cffi:defcfun
"___tracy_set_thread_name"%tracy-set-thread-name
:void
name:pointer
cffi:defcfun
"___tracy_emit_frame_mark"%tracy-emit-frame-mark
:void
name:pointer
cffi:defcfun
"___tracy_emit_plot"%tracy-emit-plot
:void
name:pointer
value:double
cffi:defcfun
"___tracy_emit_plot_config"%tracy-emit-plot-config
:void
name:pointer
format:int32
step:int32
fill:int32
color:uint32
cffi:defcfun
"___tracy_emit_message"%tracy-emit-message
:void
text:pointer
size:size
callstack-depth:int32
cffi:defcfun
"___tracy_emit_messageC"%tracy-emit-colored-message
:void
text:pointer
size:size
color:uint32
callstack-depth:int32
cffi:defcfun
"___tracy_emit_message_appinfo"%tracy-emit-application-info
:void
text:pointer
size:size
defparameter*tracy-plot-formats*'
:number.0
:memory.1
:percentage.2
:watt.3
"TracyPlotFormatEnum, by the keyword luv names each format with."

Interned names and source locations.

defvar*tracy-names*
make-hash-table:test#'equal
"Foreign copies of every name handed to Tracy, keyed by the Lisp string."
defvar*tracy-source-locations*
make-hash-table:test#'equal
"Foreign source locations, keyed by the fields that describe them."
defuntracy-name
string

Return a stable foreign copy of string.

Tracy keeps names by pointer and resolves them long after the call that introduced them, so these copies are deliberately never freed.

or
gethashstring*tracy-names*
setf
gethashstring*tracy-names*
cffi:foreign-string-allocstring
defuntracy-source-location
name&key
zone-functionname
file""
line0
color0

Return a stable foreign source location describing zone name.

Locations are interned by their fields rather than allocated per expansion. Recompiling a file re-runs its LOAD-TIME-VALUE forms, and Tracy tells zones apart by the address of their location: without the table, recompiling a function in the middle of a capture would split its zone in two.

let
key
listnamezone-functionfilelinecolor
or
setf
let
location
cffi:foreign-alloc'
flet
fill-slot
slotvalue
setf
cffi:foreign-slot-valuelocation'slot
value
fill-slot'name
fill-slot'zone-function
tracy-namezone-function
fill-slot'file
fill-slot'lineline
fill-slot'colorcolor
location
defuntracy-zone-name
designator

Render a zone name designator the way print-cpu-trace renders it.

typecasedesignator
stringdesignator
symbol
string-downcase
symbol-namedesignator
t
princ-to-stringdesignator
defuntracy-literal-zone-name
designator

Return designator's zone name when a macro can already read it, else NIL.

typecasedesignator
stringdesignator
keyword
string-downcase
symbol-namedesignator

Lifecycle.

defvar*tracy*nil"True while this image is offering zones to a Tracy viewer."
defuntracy-manual-lifetime-p

Answer whether the loaded client was built with TRACY_MANUAL_LIFETIME.

and
cffi:foreign-symbol-pointer"___tracy_startup_profiler"
t
defuntracy-application-info
text

Describe this program to any viewer that later connects.

cffi:with-foreign-string
pointersize
text
%tracy-emit-application-infopointer
1-size
defunname-tracy-thread
name

Name the calling thread in Tracy's timeline, if the profiler is running.

Worker threads are worth naming as they start: an unnamed thread still gets a lane, but the lane is a thread id rather than a job. A thread that starts before start-tracy keeps the anonymous lane, so start Tracy before the session whose threads you want to read.

The test is on the profiler rather than on the loaded library, and that is not a detail to relax. A client built for delayed initialization asserts its way out of the whole process -- not into a Lisp condition -- when any entry point is called before startup or after shutdown.

when*tracy*
cffi:with-foreign-string
pointername
%tracy-set-thread-namepointer
defunstart-tracy
&key
application-name"luv"

Load the Tracy client, start the profiler, and answer whether it is running.

The client collects on demand, so starting it does not begin a capture: it makes this image discoverable, and zones start recording when a viewer connects. Leaving it started is the intended state for a durable image.

*tracy*
defunstop-tracy

Stop the Tracy profiler, if this image started one it is allowed to stop.

when*tracy*
setf*tracy*nil
when
%tracy-shutdown-profiler
nil
defuntracy-connected-p

Answer whether a Tracy viewer is attached and therefore recording.

and*tracy*
plusp
%tracy-connected

Zones.

defmacrowith-tracy-zone
name&key
color0
valuenilvalue-supplied-p
&bodybody

Measure body as a Tracy zone named name.

When VALUE is supplied, evaluate it as the zone exits and attach the resulting unsigned integer to the zone. This is useful for semantic work counts such as sites visited: Tracy can then distinguish a slower realization from one which simply performed more work.

A literal name -- a string or a keyword, which is every zone luv writes by hand -- gets one lazily initialized source-location cell per macro expansion. The cell itself may be dumped into a standalone Lisp core, but its foreign pointer is not allocated until the restored process first enters the zone. This distinction matters: foreign memory allocated by LOAD-TIME-VALUE does not survive SAVE-LISP-AND-DIE even though the Lisp pointer object does. A computed name still works through luv's source-location table, which is slower but reuses the same stable foreign record instead of growing Tracy's allocation table on every entry.

let
enabled
gensym"ENABLED"
location-cell
gensym"LOCATION-CELL"
file
if*compile-file-truename*
namestring*compile-file-truename*
""
`
let
,@
whenliteral`
,location-cell
load-time-value
consnilnil
nil
,enabled*tracy*
when,enabled,
ifliteral`
%tracy-emit-zone-begin
or
car,location-cell
setf
car,location-cell
tracy-source-location,literal:file,file:color,color
1
`
%tracy-emit-zone-begin
tracy-source-location:file,file:color,color
1

Keep body in the expansion exactly once. Duplicating it into active

and inactive branches made nested frame instrumentation multiply a

large caller's compiler input even though only one branch ran.

unwind-protect
progn,@body
when,enabled,@
whenvalue-supplied-p`
%tracy-emit-zone-value,value
%tracy-emit-zone-end

Frames, plots, and messages.

defuntracy-frame-mark
&optionalname

End the current Tracy frame, or the secondary frame set called name.

Marking frames is what gives the viewer its frame-time graph, and what lets it say which frame a zone belongs to.

when*tracy*
%tracy-emit-frame-mark
ifname
cffi:null-pointer
defuntracy-plot
namevalue

Record value on Tracy's plot name, drawn against the same timeline.

when*tracy*
%tracy-emit-plot
coercevalue'double-float
defunconfigure-tracy-plot
name&key
format:number
fillnil
color0

Describe how the viewer should draw plot name.

step suits a quantity that holds its value between changes, such as a count of resident chunks, rather than one that is sampled continuously.

when*tracy*
%tracy-emit-plot-config
or
cdr
error"Unknown Tracy plot format ~S."format
ifstep10
iffill10
color
defuntracy-message
text&keycolor

Post text to the viewer's message log at this instant on the timeline.

when*tracy*
cffi:with-foreign-string
pointersize
text
ifcolor
%tracy-emit-colored-messagepointer
1-size
color0
%tracy-emit-messagepointer
1-size
0