Publish an app heartbeat
A job ends; a running app (a terminal UI, a desktop window, a service) does not, and its speed is how smoothly it draws. A monitor can measure CPU, memory and GPU from outside, but only the app knows its fps. The heartbeat is the app saying it.

Open the heartbeat
Section titled “Open the heartbeat”let live = pito_jobs::live( "viewer", env!("CARGO_PKG_VERSION"), pito_jobs::Surface::Desk, pito_jobs::Pacing::Continuous,);SurfaceisTui(target 120 fps),Desk, a GUI window (target 60 fps), orService, a daemon that never draws and only says it is alive.PacingisContinuousfor an app that draws every frame, orOnChangefor one that draws only when something changed. Missed frames are counted only on a continuous surface.
Keep the handle for the app’s life: one low-priority thread rewrites the file once a second, and dropping the handle removes the file.
Record each frame
Section titled “Record each frame”live.frame(frame_time);live.frame_with(cpu_time, Some(gpu_time));live.frame_idle();live.detail("converging 128/512 spp");frame(work)records the CPU time a frame took;frame_withadds its GPU time. Each stores a sample in a fixed ring of 2,048 slots (80 KiB, allocated once): it never allocates or locks, and costs 26.5 ns.frame_idlerecords a tick that drew nothing.detailsets one short line about what the app is doing;size(w, h),renderer(name)andpasses(&[("shadow", 0.8), ("main", 3.1)])add the window size, the renderer’s name and the GPU time of each pass.
When it publishes
Section titled “When it publishes”The heartbeat is opt-in. It publishes only when PITO_JOBS_DIR is set or
the folder pito/apps/ exists in the user’s data folder ($XDG_DATA_HOME,
else ~/.local/share on Linux, ~/Library/Application Support on macOS).
Anywhere else live returns an inert handle with no thread and no file, so
an app you ship writes nothing on its users’ machines.
What the file holds
Section titled “What the file holds”The file is <jobs>/apps/<pid>-<started>.json. One written by a desktop
viewer drawing a frame every 16 ms (the host name changed):
{"v":1,"id":"desk:223115:1792004532561","host":"desk","pid":223115,"pid_start":63451732,"app":"viewer","version":"0.3.0","surface":"desk","ride":false,"fps":25.0,"frame_ms":{"p50":4.9,"p95":5.8,"p99":5.8,"max":5.8},"gpu_ms":{"p50":2.5,"p95":2.5,"p99":2.5,"max":2.5},"interval_ms":{"p50":16.1,"p95":16.1,"p99":16.1,"max":16.6},"missed":0,"target_fps":60.0,"pacing":"continuous","idle":false,"detail":null,"ride_set":null,"started":1792004532561,"updated":1792004535564}frame_ms is the CPU time per frame and interval_ms the time between
presents, each as p50, p95, p99 and max over the last 5 s. Every field is in
Formats.
Read heartbeats back
Section titled “Read heartbeats back”if let Some(dir) = pito_jobs::apps_dir() { for beat in pito_jobs::read_apps(&dir) { println!("{} {:.1} {:?}", beat.app, beat.fps, beat.frame_ms); }}viewer 25.0 Some(Span { p50: 4.9, p95: 5.8, p99: 5.8, max: 5.8 })apps_dir is the folder live publishes to, or None when the heartbeat
is off. To watch two apps publish while a reader draws them:
cargo run --release --example heartbeat