luv

Workshop wiki

tracy-capture.lisp

hal/tracy-capture.lisp

system luv/tracy-capture · 73 definitions · on GitHub

Concurrent, application-neutral ownership of Tracy capture subprocesses.

Public operations only publish intent and return. Program lookup, launch, graceful interruption, process waiting, trace validation, GUI launch, and process-handle close all happen on explicitly owned background threads.

in-package#:luv.tracy.capture
defparameter*tracy-capture-program-environment*

luv_TRACY_CAPTURE

defparameter*tracy-profiler-program-environment*

luv_TRACY_PROFILER

defparameter*tracy-capture-poll-seconds*1/50"How often the capture owner observes process exit and stop intent."
defparameter*tracy-capture-interrupt-attempts*3"Graceful-stop failures allowed before the owner forcibly terminates."
defclasstracy-capture-runtime
:documentation

The platform edge of a tracy-capture-controller.

Applications normally use native-tracy-capture-runtime. Tests and unusual hosts can specialize this protocol without replacing the controller's state machine.

defclassnative-tracy-capture-runtime
:documentation

The exact SBCL and luv Tracy process implementation.

defgenerictracy-clock-now
runtime
:documentation

Return the current universal time for names and diagnostics.

defgenerictracy-path-exists-p
runtimepathname
:documentation

Whether pathname names an existing capture output.

defgenericprepare-tracy-client
runtimeapplication-name
:documentation

Ensure the in-process Tracy client is running for application-name.

defgenerictracy-viewer-connected-p
runtime
:documentation

Whether a Tracy consumer is already attached.

defgenericresolve-tracy-program
runtimerole
:documentation

Return the exact executable pathname for role.

role is one of :CAPTURE, :PROFILER, or :REVEAL. Implementations must not silently fall back to a different tool version through PATH lookup.

defgenericlaunch-tracy-process
runtimeroleprogramarguments
:documentation

Launch program for role with exact string arguments and return a handle.

defgenerictracy-process-alive-p
runtimeprocess
:documentation

Whether process is still alive.

defgenericinterrupt-tracy-process
runtimeprocess
:documentation

Request process's graceful Tracy capture stop without waiting for it.

defgenericterminate-tracy-process
runtimeprocess
:documentation

Forcibly terminate process after graceful capture shutdown failed.

This is the terminal orphan-prevention edge. A native implementation may discard the unfinished trace, but must make eventual process exit more likely than another graceful request would.

defgenericwait-tracy-process
runtimeprocess
:documentation

Wait for process on a controller-owned worker thread.

defgenerictracy-process-exit-code
runtimeprocess
:documentation

Return process's exit code after wait-tracy-process.

defgenericclose-tracy-process
runtimeprocess
:documentation

Release the Lisp-side handle for a finished process.

defmethodtracy-clock-now
declare
ignoreruntime
get-universal-time
defmethodtracy-path-exists-p
declare
ignoreruntime
not
null
probe-filepathname
defmethodprepare-tracy-client
application-name
declare
ignoreruntime
luv:start-tracy:application-nameapplication-name

start-tracy initially calls its caller "main". This caller is a deliberately separate control lane; applications name their actual canvas lane when they next observe a connected viewer.

luv:name-tracy-thread"Tracy capture control"
t
defunconfigured-tracy-program
variabledescription
let
configured
uiop:getenvvariable
unless
andconfigured
plusp
lengthconfigured
error

No Tracy ~A is configured in ~A. Enter the luv Tracy ~ environment (nix develop .#tracy) or configure that variable ~ with one exact executable path.

descriptionvariable
let
pathname
pathnameconfigured
unless
uiop:absolute-pathname-ppathname
error"~A must name one exact absolute executable, got ~S."variableconfigured
or
probe-filepathname
error"The Tracy ~A configured by ~A does not exist: ~A"descriptionvariablepathname
defmethodresolve-tracy-program
declare
ignoreruntime
ecaserole
:reveal#+darwin (or (probe-file #P"/usr/bin/open") (error "macOS capture reveal requires /usr/bin/open."))#-darwin
error"Capture reveal has no exact native program on this host."
defmethodlaunch-tracy-process
roleprogramarguments
declare
ignoreruntimerole
sb-ext:run-program
namestringprogram
arguments:searchnil:inputnil:outputnil:errornil:waitnil
defmethodtracy-process-alive-p
declare
ignoreruntime
sb-ext:process-alive-pprocess
defmethodinterrupt-tracy-process
declare
ignoreruntime

SIGINT is tracy-capture's graceful disconnect-and-save operation.

when
sb-ext:process-alive-pprocess
sb-ext:process-killprocesssb-posix:sigint
t
defmethodterminate-tracy-process
declare
ignoreruntime
when
sb-ext:process-alive-pprocess
sb-ext:process-killprocesssb-posix:sigkill
t
defmethodwait-tracy-process
declare
ignoreruntime
sb-ext:process-waitprocess
defmethodtracy-process-exit-code
declare
ignoreruntime
sb-ext:process-exit-codeprocess
defmethodclose-tracy-process
declare
ignoreruntime
sb-ext:process-closeprocess
defstruct
tracy-capture-diagnostic
:constructormake-tracy-capture-diagnostic
&keyoperationstatepathnameconditiontimestamp

One contained asynchronous failure, suitable for an inspector or HUD.

operationstatepathnameconditiontimestamp
defvar*tracy-capture-controller-counter-lock*
sb-thread:make-mutex:name"Tracy capture controller identities"
defclasstracy-capture-controller
application-name:initarg:application-name:readertracy-capture-application-name
directory:initarg:directory:readertracy-capture-directory
runtime:initarg:runtime:readertracy-capture-runtime
open-on-completion-p:initarg:open-on-completion-p:readertracy-capture-open-on-completion-p
identity:initarg:identity:readertracy-capture-identity
lock:initform
sb-thread:make-mutex:name"Tracy capture semantic state"
:readertracy-capture-lock
state:initform:idle:accessor%tracy-capture-state
generation:initform0:accessortracy-capture-generation
serial:initform0:accessortracy-capture-serial
pathname:initformnil:accessor%tracy-capture-pathname
process:initformnil:accessortracy-capture-process
stop-requested-p:initformnil:accessortracy-capture-stop-requested-p
stop-sent-p:initformnil:accessortracy-capture-stop-sent-p
stop-requested-at:initformnil:accessortracy-capture-stop-requested-at
stop-failure-count:initform0:accessortracy-capture-stop-failure-count
termination-sent-p:initformnil:accessortracy-capture-termination-sent-p
graceful-stop-seconds:initarg:graceful-stop-seconds:initform5.0d0:readertracy-capture-graceful-stop-seconds
last-completed-pathname:initformnil:accessor%tracy-capture-last-completed-pathname
diagnostics:initformnil:accessor%tracy-capture-diagnostics
:documentation

One concurrent Tracy capture owner.

The public state sequence is :IDLE -> :STARTING -> :RECORDING -> :STOPPING -> :FINALIZING -> :IDLE, with :RELEASED terminal. A single generation owns at most one capture subprocess. No public operation waits for a subprocess.

defunmake-tracy-capture-controller
&keyapplication-namedirectory
runtime
open-on-completion-pt
graceful-stop-seconds5.0d0

Make an idle controller whose trace files live beneath directory.

check-typeapplication-namestring
unless
plusp
lengthapplication-name
error"A Tracy capture controller needs a nonempty application name."
unlessdirectory
error"A Tracy capture controller needs an output directory."
check-typegraceful-stop-seconds
real0
make-instance'tracy-capture-controller:application-nameapplication-name:directory
uiop:ensure-directory-pathnamedirectory
:runtimeruntime:open-on-completion-p
not
nullopen-on-completion-p
:graceful-stop-seconds
coercegraceful-stop-seconds'double-float
:identity
defuntracy-capture-state
controller

Return controller's semantic state without exposing its lock.

sb-thread:with-mutex
tracy-capture-lockcontroller
%tracy-capture-statecontroller
defmethodprint-object
stream
print-unreadable-object
controllerstream:typet:identityt
formatstream"~A ~(~A~)"
tracy-capture-application-namecontroller
defuntracy-capture-controller-released-p
controller
eq:released
defuntracy-capture-active-p
controller

Whether a capture is starting, recording, stopping, or finalizing.

not
null
member'
:starting:recording:stopping:finalizing
defuntracy-capture-pathname
controller

Return the current generation's reserved output pathname, if any.

sb-thread:with-mutex
tracy-capture-lockcontroller
%tracy-capture-pathnamecontroller
defuntracy-capture-last-completed-pathname
controller

Return the most recent successfully frozen trace pathname, if any.

sb-thread:with-mutex
tracy-capture-lockcontroller
%tracy-capture-last-completed-pathnamecontroller
defuntracy-capture-diagnostics
controller

Return an oldest-first snapshot of contained asynchronous failures.

sb-thread:with-mutex
tracy-capture-lockcontroller
reverse
copy-list
%tracy-capture-diagnosticscontroller
defuntracy-capture-last-diagnostic
controller

Return controller's newest contained asynchronous failure.

sb-thread:with-mutex
tracy-capture-lockcontroller
first
%tracy-capture-diagnosticscontroller
defundiagnostic-time
controller
or
get-universal-time
defun%record-tracy-capture-diagnostic
controlleroperationcondition&optionalpathnamestate

Record a diagnostic while controller's lock is already held.

push
make-tracy-capture-diagnostic:operationoperation:state
orstate
%tracy-capture-statecontroller
:pathnamepathname:conditioncondition:timestamp
diagnostic-timecontroller
%tracy-capture-diagnosticscontroller
defunrecord-tracy-capture-diagnostic
controlleroperationcondition&optionalpathnamestate
sb-thread:with-mutex
tracy-capture-lockcontroller
%record-tracy-capture-diagnosticcontrolleroperationconditionpathnamestate
defunsafe-capture-name
name
string-downcase
with-output-to-string
stream
loopforcharacteracrossnamedo
write-char
if
alphanumericpcharacter
character#\-
stream
defunreserve-tracy-capture-pathname
controller

Reserve a unique semantic name while controller's lock is held.

let*
runtime
now
multiple-value-bind
secondminutehourdatemonthyear
decode-universal-timenow0
ensure-directories-exist
tracy-capture-directorycontroller
loopforserial=
prog1
tracy-capture-serialcontroller
incf
tracy-capture-serialcontroller
forpathname=
merge-pathnames
formatnil"~A-~4,'0D~2,'0D~2,'0D-~2,'0D~2,'0D~2,'0DZ-p~D-c~D-~D.tracy"
safe-capture-name
tracy-capture-application-namecontroller
yearmonthdatehourminutesecond
sb-posix:getpid
tracy-capture-identitycontroller
serial
tracy-capture-directorycontroller
unless
tracy-path-exists-pruntimepathname
returnpathname
defungeneration-current-p
controllergeneration
=generation
tracy-capture-generationcontroller
defunfinish-tracy-capture-generation
controllergeneration

Clear generation's ownership while controller's lock is already held.

when
generation-current-pcontrollergeneration
setf
tracy-capture-processcontroller
nil
%tracy-capture-pathnamecontroller
nil
tracy-capture-stop-requested-pcontroller
nil
tracy-capture-stop-sent-pcontroller
nil
tracy-capture-stop-requested-atcontroller
nil
tracy-capture-stop-failure-countcontroller
0
tracy-capture-termination-sent-pcontroller
nil
unless
eq:released
%tracy-capture-statecontroller
setf
%tracy-capture-statecontroller
:idle
defunspawn-tracy-controller-thread
controlleroperationpathnamefunction

Spawn function, containing thread creation and unhandled worker failures.

handler-case
sb-thread:make-thread
lambda
handler-case
funcallfunction
error
condition
record-tracy-capture-diagnosticcontrolleroperationconditionpathname
:name
formatnil"~A Tracy ~(~A~)"
tracy-capture-application-namecontroller
operation
error
condition
record-tracy-capture-diagnosticcontrolleroperationconditionpathname
nil
defuncapture-process-arguments
pathname
list"-o"
namestringpathname
"-a""127.0.0.1"
defunauxiliary-process-arguments
rolepathname
ecaserole
:profiler
list
namestringpathname
:reveal#+darwin(list "-R" (namestring pathname))#-darwin
list
namestringpathname
defunrun-tracy-auxiliary-process
controlleroperationrolepathname

Launch, reap, and close one non-capture helper on this worker thread.

let*
runtime
processnil
unwind-protect
progn
unless
tracy-path-exists-pruntimepathname
error"No completed Tracy capture exists at ~A."pathname
let
program
setfprocess
wait-tracy-processruntimeprocess
let
exit-code
unless
eql0exit-code
error"Tracy ~(~A~) process exited with code ~S for ~A."roleexit-codepathname
whenprocess
handler-case
close-tracy-processruntimeprocess
error
condition
record-tracy-capture-diagnosticcontrolleroperationconditionpathname
defunschedule-tracy-auxiliary-process
controlleroperationrolepathname
spawn-tracy-controller-threadcontrolleroperationpathname
lambda
run-tracy-auxiliary-processcontrolleroperationrolepathname
defunopen-tracy-capture
controller&optionalpathname

Open pathname, or the last completed trace, without waiting for its GUI.

let
whentarget
schedule-tracy-auxiliary-processcontroller:open:profilertarget
target
defunreveal-tracy-capture
controller&optionalpathname

Reveal pathname, or the last completed trace, without waiting for Finder.

let
whentarget
schedule-tracy-auxiliary-processcontroller:reveal:revealtarget
target
defuntracy-stop-elapsed-seconds
controller
let
started
tracy-capture-stop-requested-atcontroller
whenstarted
/
-
get-internal-real-time
started
coerceinternal-time-units-per-second'double-float
defunclaim-tracy-capture-stop-action
controllergenerationprocess

Claim the next stop action for generation while its owner remains alive.

sb-thread:with-mutex
tracy-capture-lockcontroller
when
and
generation-current-pcontrollergeneration
eqprocess
tracy-capture-processcontroller
tracy-capture-stop-requested-pcontroller
cond
tracy-capture-termination-sent-pcontroller
nil
or
>=
tracy-capture-stop-failure-countcontroller
*tracy-capture-interrupt-attempts*
and
tracy-capture-stop-sent-pcontroller
>=
tracy-capture-graceful-stop-secondscontroller
setf
tracy-capture-termination-sent-pcontroller
t
:terminate
not
tracy-capture-stop-sent-pcontroller
setf
tracy-capture-stop-sent-pcontroller
t
:interrupt
defunperform-tracy-capture-stop-action
controllergenerationprocesspathnameaction

Perform action on the generation-owning worker, retrying contained errors.

let
runtime
ecaseaction
:interrupt
handler-case
error
condition
sb-thread:with-mutex
tracy-capture-lockcontroller
when
and
generation-current-pcontrollergeneration
eqprocess
tracy-capture-processcontroller
setf
tracy-capture-stop-sent-pcontroller
nil
incf
tracy-capture-stop-failure-countcontroller
%record-tracy-capture-diagnosticcontroller:stopconditionpathname
:terminate
record-tracy-capture-diagnosticcontroller:terminate
make-condition'simple-error:format-control"Graceful Tracy shutdown did not settle; forcibly terminating ~A.":format-arguments
listpathname
pathname
handler-case
error
condition
sb-thread:with-mutex
tracy-capture-lockcontroller
when
and
generation-current-pcontrollergeneration
eqprocess
tracy-capture-processcontroller

A failed terminal signal remains owned and is retried on the next bounded observation rather than becoming an orphan.

setf
tracy-capture-termination-sent-pcontroller
nil
%record-tracy-capture-diagnosticcontroller:terminateconditionpathname
defunservice-tracy-capture-stop
controllergenerationprocesspathname

Let the capture owner service one published stop intent without a new thread.

let
action
claim-tracy-capture-stop-actioncontrollergenerationprocess
whenaction
perform-tracy-capture-stop-actioncontrollergenerationprocesspathnameaction
defunwait-for-owned-tracy-process
controllergenerationpathnameprocess

Observe process on its owner worker, servicing stop and escalation intents.

let
runtime
loopwhile
tracy-process-alive-pruntimeprocess
do
service-tracy-capture-stopcontrollergenerationprocesspathname

Reap exactly once after the nonblocking observations report exit.

wait-tracy-processruntimeprocess
defunfinalize-tracy-capture
controllergenerationpathnameprocess

Wait, validate, publish, and close one exact capture generation.

let*
runtime
completed-pnil
open-pnil
unwind-protect
handler-case
progn
wait-for-owned-tracy-processcontrollergenerationpathnameprocess
sb-thread:with-mutex
tracy-capture-lockcontroller
when
and
generation-current-pcontrollergeneration
not
eq:released
%tracy-capture-statecontroller
setf
%tracy-capture-statecontroller
:finalizing
let
exit-code
unless
eql0exit-code
error"tracy-capture exited with code ~S for ~A."exit-codepathname
unless
tracy-path-exists-pruntimepathname
error"tracy-capture exited successfully without writing ~A."pathname
sb-thread:with-mutex
tracy-capture-lockcontroller
when
generation-current-pcontrollergeneration
setf
%tracy-capture-last-completed-pathnamecontroller
pathname
completed-ptopen-p
and
tracy-capture-open-on-completion-pcontroller
not
eq:released
%tracy-capture-statecontroller
error
condition
record-tracy-capture-diagnosticcontroller:finalizeconditionpathname

Closing a live Lisp process handle does not terminate its OS child. A finalizer failure therefore makes one terminal attempt before the handle becomes unreachable.

handler-case
when
tracy-process-alive-pruntimeprocess
error
termination-condition
record-tracy-capture-diagnosticcontroller:terminatetermination-conditionpathname
handler-case
close-tracy-processruntimeprocess
error
condition
record-tracy-capture-diagnosticcontroller:closeconditionpathname
sb-thread:with-mutex
tracy-capture-lockcontroller
when
andcompleted-popen-p
open-tracy-capturecontrollerpathname
defunrun-tracy-capture-generation
controllergenerationpathname

Launch and own generation entirely on its background worker.

let*
runtime
processnil
installed-pnil
stop-now-pnil
handler-case
progn
prepare-tracy-clientruntime
tracy-capture-application-namecontroller
when
error"A Tracy viewer is already connected; stop it before starting a capture."
let
program
resolve-tracy-programruntime:capture
setfprocess
launch-tracy-processruntime:captureprogram
sb-thread:with-mutex
tracy-capture-lockcontroller
when
generation-current-pcontrollergeneration
setf
tracy-capture-processcontroller
process
installed-ptstop-now-p
or
tracy-capture-stop-requested-pcontroller
eq:released
%tracy-capture-statecontroller
unless
orstop-now-p
eq:released
%tracy-capture-statecontroller
setf
%tracy-capture-statecontroller
:recording
unlessinstalled-p

A stale launch is never allowed to become an orphan.

when
tracy-process-alive-pruntimeprocess

When STOP-NOW-P is true, the generation owner observes the already-published intent immediately in its bounded wait loop. A second worker is neither needed nor able to lose that signal.

finalize-tracy-capturecontrollergenerationpathnameprocess
error
condition
whenprocess
handler-case
when
tracy-process-alive-pruntimeprocess
error
termination-condition
record-tracy-capture-diagnosticcontroller:terminatetermination-conditionpathname
handler-case
close-tracy-processruntimeprocess
error
close-condition
record-tracy-capture-diagnosticcontroller:closeclose-conditionpathname
sb-thread:with-mutex
tracy-capture-lockcontroller
%record-tracy-capture-diagnosticcontroller:startconditionpathname
defunbegin-tracy-capture
controller

Publish a new generation and return its pathname and worker closure.

sb-thread:with-mutex
tracy-capture-lockcontroller
ecase
%tracy-capture-statecontroller
:idle
let
generation
incf
tracy-capture-generationcontroller
setf
%tracy-capture-statecontroller
:starting
%tracy-capture-pathnamecontroller
pathname
tracy-capture-processcontroller
nil
tracy-capture-stop-requested-pcontroller
nil
tracy-capture-stop-sent-pcontroller
nil
tracy-capture-stop-requested-atcontroller
nil
tracy-capture-stop-failure-countcontroller
0
tracy-capture-termination-sent-pcontroller
nil
valuespathnamegeneration
:starting:recording:stopping:finalizing
values
%tracy-capture-pathnamecontroller
nil
:released
error"The Tracy capture controller for ~A has been released."
tracy-capture-application-namecontroller
defunstart-tracy-capture
controller

Start one capture asynchronously, or return the current capture pathname.

multiple-value-bind
pathnamegeneration
whengeneration
unless
spawn-tracy-controller-threadcontroller:startpathname
lambda
run-tracy-capture-generationcontrollergenerationpathname
sb-thread:with-mutex
tracy-capture-lockcontroller
when
generation-current-pcontrollergeneration
pathname
defunnote-tracy-capture-stop-requested
controller

Publish the first stop timestamp while controller's lock is held.

setf
tracy-capture-stop-requested-pcontroller
t
unless
tracy-capture-stop-requested-atcontroller
setf
tracy-capture-stop-requested-atcontroller
get-internal-real-time
defunrequest-tracy-capture-stop
controllerrelease-p

Atomically publish stop intent for the generation-owning worker.

sb-thread:with-mutex
tracy-capture-lockcontroller
let
pathname
%tracy-capture-pathnamecontroller
case
%tracy-capture-statecontroller
:idle
whenrelease-p
setf
%tracy-capture-statecontroller
:released
:starting
setf
%tracy-capture-statecontroller
ifrelease-p:released:stopping
:recording
setf
%tracy-capture-statecontroller
ifrelease-p:released:stopping
:stopping:finalizing
whenrelease-p
setf
%tracy-capture-statecontroller
:released
:released

Terminal idempotence needs no public retry call: the generation owner retains the controller and automatically retries itself.

nil
pathname
defunstop-tracy-capture
controller

Request one graceful stop and return immediately.

Concurrent and repeated calls publish only one interrupt for a generation.

defuntoggle-tracy-capture
controller

Atomically start from idle or stop the one busy generation.

let
pathnamenil
generationnil
start-pnil
sb-thread:with-mutex
tracy-capture-lockcontroller
case
%tracy-capture-statecontroller
:idle
setfpathnamegeneration
incf
tracy-capture-generationcontroller
%tracy-capture-statecontroller
:starting
%tracy-capture-pathnamecontroller
pathname
tracy-capture-processcontroller
nil
tracy-capture-stop-requested-pcontroller
nil
tracy-capture-stop-sent-pcontroller
nil
tracy-capture-stop-requested-atcontroller
nil
tracy-capture-stop-failure-countcontroller
0
tracy-capture-termination-sent-pcontroller
nil
start-pt
:starting:recording
setfpathname
%tracy-capture-pathnamecontroller
generation
tracy-capture-generationcontroller
%tracy-capture-statecontroller
:stopping
:stopping:finalizing
setfpathname
%tracy-capture-pathnamecontroller
:released
error"The Tracy capture controller for ~A has been released."
tracy-capture-application-namecontroller
whenstart-p
unless
spawn-tracy-controller-threadcontroller:startpathname
lambda
run-tracy-capture-generationcontrollergenerationpathname
sb-thread:with-mutex
tracy-capture-lockcontroller
when
generation-current-pcontrollergeneration
pathname
defunrelease-tracy-capture-controller
controller

Detach controller immediately and asynchronously stop its owned capture.

This operation is terminal and idempotent. It never waits for capture finalization or for a profiler window.

nil