//! The per-frame timing record as commands: read the rolling window, clear it,
//! and drive the one interaction the record exists to measure — a camera spin.
//!
//! A measurement is "reset, spin, read". Without the reset the window still
//! holds the idle frames that preceded the spin, and an idle frame is cheap, so
//! their average hides exactly the cost being looked for. See [`crate::perf`]
//! for what each phase brackets.
//!
//! The spin is stepped by the APP, one pointer move per frame
//! ([`crate::app::BrepApp::start_spin`]), not by the host issuing one move per
//! call: a host-driven drag puts a round trip between every pair of frames, and
//! `dt` would then report the host's latency instead of the app's frame period.
use crate::automation::command::{app_command, parse_args, CommandSpec, Ctx, NoArgs, Outcome};
use serde::Deserialize;
use serde_json::Value;
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PerfArgs {
/// Drop every sample AFTER reading, so the next read covers only what
/// happened since this call.
#[serde(default)]
pub reset: bool,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SpinArgs {
/// How many frames to orbit for. The timing window holds 120, so a spin
/// shorter than that leaves pre-spin frames in the average.
#[serde(default = "default_frames")]
pub frames: u32,
/// Pointer step per frame in logical px, `[dx, dy]`. The default sweeps
/// horizontally, which turns the camera about the up axis.
#[serde(default)]
pub step: Option<[f64; 2]>,
/// Clear the timing window as the spin starts (default true), so the read
/// afterwards is the spin and nothing else.
#[serde(default = "default_true")]
pub reset: bool,
}
fn default_frames() -> u32 {
240
}
fn default_true() -> bool {
true
}
fn perf_reset_result_schema() -> Value {
serde_json::json!({"type":"object","properties":{},"additionalProperties":false})
}
fn perf_spin_result_schema() -> Value {
serde_json::json!({"type":"object","properties":{
"frames":{"type":"integer","minimum":0,"maximum":4294967295u64},
"step":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2}
},"required":["frames","step"],"additionalProperties":false})
}
fn perf_stats_result_schema() -> Value {
let count = serde_json::json!({"type":"integer","minimum":0});
let timing = serde_json::json!({"type":"object","properties":{
"avg":{"type":"number"},"p95":{"type":"number"},"max":{"type":"number"}
},"required":["avg","p95","max"],"additionalProperties":false});
let tally = serde_json::json!({"type":"object","properties":{
"count":count,"ms":{"type":"number"},"features":count,"reused":count,"errors":count
},"required":["count","ms","features","reused","errors"],"additionalProperties":false});
serde_json::json!({"type":"object","properties":{
"frames":count,"window":count,"staleFrames":count,"spinning":{"type":"boolean"},
"dt":timing,"ui":timing,"publish":timing,"sync":timing,"fit":timing,"overlays":timing,"draw":timing,
"runs":{"type":"object","properties":{
"sites":{"type":"object","additionalProperties":tally},"total":tally
},"required":["sites","total"],"additionalProperties":false}
},"required":["frames","window","staleFrames","spinning","dt","ui","publish","sync","fit","overlays","draw","runs"],"additionalProperties":false})
}
fn perf_stats(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let args: PerfArgs = parse_args(args)?;
let json = crate::perf::json();
let runs = brep_render::run_trace::tally_json();
if args.reset {
crate::perf::reset();
brep_render::run_trace::reset();
}
let mut value: Value = serde_json::from_str(&json).map_err(|e| e.to_string())?;
// The poll condition: a host reads until the spin it asked for is over.
if let Some(map) = value.as_object_mut() {
map.insert("spinning".into(), Value::Bool(ctx.app.spinning()));
// The history-replay tally. Frame milliseconds cannot tell a duplicate
// replay from a slow machine; this counts the executions themselves.
map.insert("runs".into(), runs);
}
Ok(Outcome::Done(value))
}
fn perf_spin(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let args: SpinArgs = parse_args(args)?;
let [dx, dy] = args.step.unwrap_or([3.0, 0.0]);
if args.reset {
crate::perf::reset();
brep_render::run_trace::reset();
}
ctx.app.start_spin(args.frames, (dx, dy));
Ok(Outcome::Done(serde_json::json!({ "frames": args.frames, "step": [dx, dy] })))
}
fn perf_reset(_ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
crate::perf::reset();
brep_render::run_trace::reset();
Ok(Outcome::Done(serde_json::json!({})))
}
pub static COMMANDS: &[CommandSpec] = &[
app_command!("frame", "perf_stats", "Where the last ~120 frames' milliseconds went, in ms. `dt` is the frame PERIOD a user feels; `ui` brackets the whole UI body; `publish`, `sync`, `fit`, `overlays` and `draw` are nested inside it (the remainder is egui's own layout and painting). Each is {avg, p95, max}. `spinning` says whether a `perf_spin` is still running — poll this until it is false, then read. `runs` is the history-replay tally since the last reset: one {count, ms, features, reused, errors} per SITE that called `execute_history`, plus their total. `runs.sites.runner` is the one execution an edit is supposed to cost, and any other site is a duplicate replay of the whole document — a thing frame milliseconds cannot state, because a duplicate and a slow machine look alike. Bracket an interaction with `perf_reset` and read this after it.", PerfArgs, perf_stats, Read, READ, perf_stats_result_schema),
app_command!("frame", "perf_spin", "Orbit the camera for `frames` frames, one pointer step per frame, through the same drag path the mouse takes. Clears the timing window and the history-replay counts first (unless `reset` is false), so `perf_stats` afterwards reports the spin alone. Returns at once — poll `perf_stats.spinning`.", SpinArgs, perf_spin, Mutate, MUTATE_NOWAIT, perf_spin_result_schema),
app_command!("frame", "perf_reset", "Drop every timing sample and every history-replay count. Use before an interaction whose cost is the question; `perf_stats` afterwards then reports that interaction alone, in frame milliseconds AND in `execute_history` calls.", NoArgs, perf_reset, Mutate, MUTATE_NOWAIT, perf_reset_result_schema),
];