Skip to main content

Module perf

Module perf 

Source
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 a skipped marker the mobile dirty-gate sets), aggregated into a ring buffer plus running totals; FrameStats::summary reports 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’s begin() epoch (native-lib load, init entry, adapter/device/renderer ready, first rebuild done, first frame presented — see the SPAN_* 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, alongside enabled, makes FrameStats::record additionally emit one frust-perf raw line per recorded frame (instead of only the rate-limited ~2s frust-perf frame summary FrameStats::emit_log already produces).
  • mark_scenario_start/mark_scenario_end — the benchmark scenario-window edges a harness slices that per-frame series by. They ride enabled alone (not the raw dial), and they are queued rather than logged: FrameStats::record emits 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 is perf-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. See mark_scenario_start for 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§

FramePasses
One frame’s measured pass durations, as recorded by a shell’s frame callback (AndroidAppHandle::frame / frust_render_frame / desktop’s RedrawRequested handler — see docs/ARCHITECTURE.md’s Frame pipeline). skipped is set by the mobile dirty-gate for a frame whose passes never ran; its pass durations are Duration::ZERO in that case and it is excluded from the percentile computation in FrameStats::summary (see that method’s docs) while still counting toward total_frames/skipped_frames.
FrameStats
Per-shell frame-timing recorder: a ring buffer of the last RING_CAPACITY frames’ FramePasses plus lifetime running counters, aggregated on demand by FrameStats::summary and rate-limit logged by FrameStats::should_emit/FrameStats::emit_log.
FrameSummary
A rolling summary over FrameStats’s current ring-buffer window plus the lifetime running counters — the shape FrameStats::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.
RenderSpans
The render-thread half of a render-thread-split frame’s timing: the encode/acquire/submit spans measured on the render thread, folded together with the UI thread’s UiSpans via FramePasses::from_split. See FramePasses::encode/FramePasses::acquire/ FramePasses::submit for each span’s exact boundary (the v3 attribution this split preserves unchanged).
StartupSpans
Named monotonic timestamps from a begin() epoch — a shell records one named span at each startup milestone (see the SPAN_* consts), then calls Self::emit_log once for a single summary line.
SystemClock
The production Clock: wraps std::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/paint spans measured on the UI thread, plus the frame gate’s skipped verdict (the gate stays UI-side — see crate::frame_gate). Rides the scene-handoff channel across to the render thread, which folds it together with its own RenderSpans via FramePasses::from_split and 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 FramePasses split 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 rebuild pass 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-frame frust-perf raw lines and the bench-scenario-* markers land in.
enabled
The perf-trace feature is off: perf instrumentation is compiled out of this build, so the switch is a compile-time false constant — no OnceLock, no environment read. #[inline] so every downstream if enabled() branch constant-folds to nothing, taking the frust-perf emission (and its strings) with it via LLVM dead-code elimination. Build with --features perf-trace (what a debug/profile build does) to restore the runtime FRUST_TRACE switch documented above.
mark_scenario_end
Raise a bench-scenario-end marker — the closing edge of the window mark_scenario_start opened; see its docs, which cover the gating, the emitted n, 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 its n is one past the window’s last frame.
mark_scenario_start
Raise a bench-scenario-start marker for name — the opening edge of a benchmark scenario window, so an external harness can slice the per-frame frust-perf raw series into named scenarios without holding a FrameStats handle 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-trace feature is off: the raw-per-frame dial is compiled out alongside enabled, so it is a compile-time false constant (see enabled’s feature-off arm for the DCE rationale).