BREP_app 0.17.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! 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),
];