Expand description
Frame-timing and startup-span perf instrumentation shared by every shell.
§What lives here
FrameStats— a per-shell recorder of one frame’s pass durations (rebuild/layout/paint/encode/acquire/submit, plus askippedmarker the mobile dirty-gate sets), aggregated into a ring buffer plus running totals;FrameStats::summaryreports p50/p95/p99 total frame time, per-pass p95, and frames-over-budget counts against the 16.6ms/8.3ms (60Hz/120Hz) targets.StartupSpans— named monotonic timestamps from a shell’sbegin()epoch (native-lib load, init entry, adapter/device/renderer ready, first rebuild done, first frame presented — see theSPAN_*consts), summarized into one log line.enabled— the process-wide on/off switch every recording API is a no-op behind (see its own docs).raw_enabled— a second dial that, alongsideenabled, makesFrameStats::recordadditionally emit onefrust-perf rawline per recorded frame (instead of only the rate-limited ~2sfrust-perf framesummaryFrameStats::emit_logalready produces).mark_scenario_start/mark_scenario_end— the benchmark scenario-window edges a harness slices that per-frame series by. They rideenabledalone (not the raw dial), and they are queued rather than logged:FrameStats::recordemits each one stamped with the number of the frame that actually carried it, so a window survives the render-thread split’s UI/render interleaving. The whole route isperf-trace-only (a release-lean build has no marker code at all) and per-thread — a marker belongs to the thread that raised it until that thread hands a frame off or records one. Seemark_scenario_startfor the route, the half-open window rule, and what happens to a marker raised on a thread that does neither.
§Layering choice
This lives in frust-shell-common, not frust-core — timing is
shell-owned by design (docs/CODE_STANDARDS.md’s “no Instant::now() in
frust-core/frust-widgets” rule binds the framework layers only;
a shell reading a wall clock to time its own passes is exactly the kind
of shell-facing responsibility this crate already carries alongside
theme_override/ffi_support). Every recording API takes an
already-measured std::time::Duration rather than reading a clock
itself, and StartupSpans is generic over an injectable Clock —
this crate’s own logic stays fully host-testable without a real clock.
§Wiring
This module ships the recorder + switch; the Android, iOS, and desktop
shells each construct and feed a FrameStats/StartupSpans of
their own.
Structs§
- Frame
Passes - One frame’s measured pass durations, as recorded by a shell’s frame
callback (
AndroidAppHandle::frame/frust_render_frame/ desktop’sRedrawRequestedhandler — seedocs/ARCHITECTURE.md’s Frame pipeline).skippedis set by the mobile dirty-gate for a frame whose passes never ran; its pass durations areDuration::ZEROin that case and it is excluded from the percentile computation inFrameStats::summary(see that method’s docs) while still counting towardtotal_frames/skipped_frames. - Frame
Stats - Per-shell frame-timing recorder: a ring buffer of
the last
RING_CAPACITYframes’FramePassesplus lifetime running counters, aggregated on demand byFrameStats::summaryand rate-limit logged byFrameStats::should_emit/FrameStats::emit_log. - Frame
Summary - A rolling summary over
FrameStats’s current ring-buffer window plus the lifetime running counters — the shapeFrameStats::emit_log’s log line reports and tests assert against. - GpuPasses
- One frame’s real GPU time, split by the spans the frust-owned render engine
names — the counterpart of the CPU spans in
FramePasses, measured on the GPU’s own clock rather than inferred from CPU wall time around a submit. - Render
Spans - The render-thread half of a render-thread-split frame’s timing: the
encode/acquire/submitspans measured on the render thread, folded together with the UI thread’sUiSpansviaFramePasses::from_split. SeeFramePasses::encode/FramePasses::acquire/FramePasses::submitfor each span’s exact boundary (the v3 attribution this split preserves unchanged). - Startup
Spans - Named monotonic timestamps from a
begin()epoch — a shell records one named span at each startup milestone (see theSPAN_*consts), then callsSelf::emit_logonce for a single summary line. - System
Clock - The production
Clock: wrapsstd::time::Instant, monotonic for the lifetime of the process. - UiSpans
- The UI-thread half of a render-thread-split frame’s timing: the
rebuild/layout/paintspans measured on the UI thread, plus the frame gate’sskippedverdict (the gate stays UI-side — seecrate::frame_gate). Rides the scene-handoff channel across to the render thread, which folds it together with its ownRenderSpansviaFramePasses::from_splitand records the result through the one emitter.
Constants§
- BUDGET_
60HZ - The 60Hz frame budget (1000/60 ms), truncated to whole microseconds.
- BUDGET_
120HZ - The 120Hz frame budget (1000/120 ms), truncated to whole microseconds.
- RING_
CAPACITY - Ring-buffer capacity for
FrameStats— roughly 2 seconds of frames at 60Hz, enough for a stable rolling percentile without unbounded growth. - SPAN_
ADAPTER_ READY - Startup-span name: the wgpu adapter is acquired.
- SPAN_
DEVICE_ READY - Startup-span name: the wgpu logical device is acquired.
- SPAN_
FIRST_ ENCODE_ DONE - Startup-span name: the app’s first
frame’s GPU/CPU encode has completed — the boundary between the first
frame’s paint/encode work and its swapchain-acquire (present) wait, so a
first-frame outlier (a 3646ms-class span) decomposes into encode vs present
exactly as the per-frame
FramePassessplit does. - SPAN_
FIRST_ FRAME_ PRESENTED - Startup-span name: the app’s first frame has been presented to the surface.
- SPAN_
FIRST_ REBUILD_ DONE - Startup-span name: the app’s first
rebuildpass has completed. - SPAN_
INIT_ ENTRY - Startup-span name: entry into the shell’s init function (
nativeInit/frust_init/ the desktop app-construction entry point). - SPAN_
NATIVE_ LIB_ LOAD - Startup-span name: native library load (process/JNI load, or the C-ABI equivalent on iOS).
- SPAN_
PIPELINE_ CACHE_ RESTORED - Startup-span name: a persisted GPU pipeline cache blob was restored before surface creation — its presence in the startup line is the warm-start (cache-hit) signal, its absence the cold-start (cache-miss) one, so a slow first frame can be attributed to shader-pipeline compilation vs a warm cache. Recorded only on a hit, right before the surface (and thus the pipeline) is built.
- SPAN_
RENDERER_ READY - Startup-span name: the vello renderer (and surface) are ready to present.
Traits§
- Clock
- A monotonic clock reading, injectable so
StartupSpans’s deltas are deterministically testable without a real clock. Only differences between successive readings are meaningful — the absolute value has no defined epoch.
Functions§
- bench_
emit - Emit one already-formatted benchmark trace line into the same
log::info!stream the per-framefrust-perf rawlines and thebench-scenario-*markers land in. - enabled
- The
perf-tracefeature is off: perf instrumentation is compiled out of this build, so the switch is a compile-timefalseconstant — noOnceLock, no environment read.#[inline]so every downstreamif enabled()branch constant-folds to nothing, taking thefrust-perfemission (and its strings) with it via LLVM dead-code elimination. Build with--features perf-trace(what a debug/profile build does) to restore the runtimeFRUST_TRACEswitch documented above. - mark_
scenario_ end - Raise a
bench-scenario-endmarker — the closing edge of the windowmark_scenario_startopened; see its docs, which cover the gating, the emittedn, and the half-open[start_n, end_n)rule identically. In the S3 convention this is raised in the build after the measured one, so itsnis one past the window’s last frame. - mark_
scenario_ start - Raise a
bench-scenario-startmarker forname— the opening edge of a benchmark scenario window, so an external harness can slice the per-framefrust-perf rawseries into named scenarios without holding aFrameStatshandle itself (a marker is a scenario-boundary event, not a per-frame one, hence a free function rather than a method). - raw_
enabled - The
perf-tracefeature is off: the raw-per-frame dial is compiled out alongsideenabled, so it is a compile-timefalseconstant (seeenabled’s feature-off arm for the DCE rationale).