//! The staged interactive-edit latency bench + budget gate (Ruling A //! criterion 2; `spec/PLAN_EDITOR_APP.md` Fact 8). //! //! The normative budget (`spec/core_spec.tex`, //! `req:perf:single-system-edit-latency`): //! //! > The *core's portion* of a single-system edit (operation envelope //! > construction, reduction, incremental layout through `ResolvedLayoutIR`) //! > MUST complete within one frame (16.7 ms at 60 Hz) at the p99 percentile, //! > measured on the reference hardware profile. //! //! ## Why this bench exists, and what it is allowed to conclude //! //! Ruling A's toolkit spike is bounded by six criteria, of which criterion 2 is //! *staged* latency: "reduce / engrave / scene-build / paint measured //! **separately** … a toolkit verdict from an end-to-end number is //! uninformative while reduction or solving dominates." That sentence is a //! sequencing claim nobody had measured. This bench measures it, so the //! decision to run the spike — or to run T4b (incremental materialization) //! first — rests on numbers instead of assumption. //! //! The stage split is not invented here; it is the real seam //! `EditorSession::materialize` walks (`epiphany-editor-core/src/lib.rs`), read //! off its private `render_score` and reproduced stage for stage: //! //! | stage | what runs | in the core's budget? | //! |---|---|---| //! | construct | mint the one new `OperationEnvelope` | **yes** | //! | reduce | `OperationSet::accept` × log + `reduce_onto(&base)` | **yes** | //! | engrave | `to_logical` → `to_constrained` → `Engraver::solve` | **yes** | //! | scene-build | `to_render` + `hit_test_map` (+ SVG string) | no — product layer | //! | paint | `usvg` parse + `resvg` rasterize | no — product layer | //! //! Only the first three are gated, and all three are gated: the requirement //! names "operation envelope **construction**, reduction, incremental layout", //! so `construct` is timed and summed even though it is tens of nanoseconds and //! never moves the verdict. A gate that silently drops a named component is a //! proxy for the requirement rather than the requirement. //! //! `req:perf:single-system-edit-latency` bounds "the core's portion" and says //! so explicitly — "End-to-end edit-to-pixel latency (input handling, hit //! testing, render submission, display flip) is a product-layer obligation" — //! so charging the SVG serializer and `resvg` against a core budget would be a //! category error. They are measured and printed because the ruling asks for //! the stages *separately*, and because today's scene-build+paint is the path //! Ruling A demotes: the number is the baseline a canvas must beat, not a //! budget to defend. //! //! ## The log must be shaped like a session's, or the reduce column lies //! //! `reduce`'s cost is dominated by ordering work over the log's **causal //! edges**, so a log whose envelopes carry empty causal contexts measures a //! different algorithm than the one production runs. The first version of this //! bench made exactly that mistake and understated `reduce` by ~3× at depth //! 10,000 (17 ms rather than 54 ms), which moved the reported wall by more than //! a factor of two and would have mis-sequenced T4b. [`edit_log`] now //! reproduces `EditorSession`'s minting shape: counters from 0, only the root //! context empty, every later envelope carrying the head's context extended by //! the head. //! //! ## What the scale points vary, and what they deliberately do not //! //! The variable is **log depth**: Fact 8's finding is that `apply` reduces the //! *entire accumulated log* onto the pristine open-time base on every edit, so //! the cost of one keystroke grows with the number of keystrokes before it. //! The score stays a fixed 10-measure fixture, so the engrave column is held //! still while the reduce column moves — which is what makes the two //! attributable. //! //! **The score's *size* is held fixed; its *content* is not, and three columns //! read that.** The edits are transpositions, so the reduced score differs from //! the base by up to ±1 semitone per pitch, and a transposed pitch may acquire //! an accidental. Because the edit log cycles the pitch list and alternates //! direction per pass, the accidental count depends on the *parity of the pass //! count* at that depth: depths 1,000 / 3,000 / 5,000 are 25 / 75 / 125 passes //! (odd — every pitch sits one semitone off the base, most carrying an //! accidental), while 10,000 is 250 (even — every pitch is back where it //! started). That is why `paint` is **non-monotonic** in depth below: it sits //! near 2.8 ms at every odd-parity depth and drops to 1.36 ms at 10,000, where //! the score simply has less ink. `reduce` is the only column that tracks //! depth; `engrave`, `scene-build`, and `paint` track score content. Reading //! paint's dip at 10,000 as a scaling win would be a mistake. //! //! **Depth is per session, not per document.** `EditorSession::open` starts //! with an empty `applied` log (`editor-core/src/lib.rs`), so reopening a saved //! score resets the depth this bench varies: the reduced score becomes the new //! pristine base. The wall below is therefore a budget on **one sitting**, not //! on a document's lifetime — which is what keeps a four-figure number from //! being catastrophic. It is still reachable: note entry mints one operation //! per note. //! //! **The honest limitation:** no orchestral-scale score fixture exists in the //! testkit (the largest are three staves × ten measures), so the engrave and //! scene-build columns are measured at *small score size* and are lower //! bounds. The spec's budget contemplates "a 100-page orchestral score". This //! bench therefore cannot prove the budget holds at scale; it can only show //! where the time goes at the scale we can build, and any row that already //! misses at this size misses by more at a realistic one. A score-size axis //! belongs with the engraving-quality track that needs large fixtures anyway. //! //! The edits are ±1-semitone `TransposeInterval`s cycled across the fixture's //! pitches, alternating direction **per pass** (see [`edit_envelope`] for why //! per-pass and not per-operation): distinct operation ids so the log genuinely //! deepens, drift bounded to one semitone so nothing wanders out of range at //! depth 10,000, and the same operation the edit-loop slice already uses //! (`src/editloop.rs`). //! //! Run: `cargo bench -p epiphany-testkit --bench editor_pipeline`. Add //! `--features golden-gate` for the scene-build and paint columns (they need //! `epiphany-render-svg` and `resvg`, which are optional for the MSRV reason //! documented in `Cargo.toml`); without it those columns print an explicit //! skip, never a silent absence. `EPIPHANY_BENCH_QUICK=1` gives the reduced //! PR-CI shape. Under `cargo test --benches` criterion runs each measurement //! once and the gate is skipped, exactly as the sibling benches do. use std::time::Duration; use criterion::{BatchSize, BenchmarkId, Criterion, SamplingMode}; use epiphany_core::{OperationId, PitchId, ReplicaId, Score, TranspositionInterval, WallClockTime}; use epiphany_engrave::Engraver; use epiphany_layout_ir::{ to_constrained, to_logical, to_render, ConstraintSolver, ResolvedLayoutIR, SolverConfig, }; use epiphany_ops::{ AuthorId, CausalContext, HybridLogicalClock, OperationEnvelope, OperationKind, OperationPayload, OperationSet, OperationStamp, TransposeIntervalOp, }; use epiphany_testkit::budget::{self, Expectation}; use epiphany_testkit::fixtures; /// `req:perf:single-system-edit-latency`: one frame at 60 Hz. const FRAME_BUDGET: Duration = Duration::from_micros(16_700); /// One documented scale point of THE STAGE TABLE below. struct ScalePoint { /// Log depth: how many edits precede the one being timed. depth: usize, /// The documented expectation for `edit/core/` against /// [`FRAME_BUDGET`]. expectation: Expectation, /// Budget-gate timed iterations (full mode, quick mode); `0` skips the row /// in that mode, printed as an explicit skip. gate_iters: (usize, usize), /// Criterion measurement time (full mode), or `None` to leave the point /// gate-only. criterion_time: Option, } /// THE STAGE TABLE. Budget: the core's portion (reduce + engrave) within /// 16.7 ms, `req:perf:single-system-edit-latency`. /// /// Measured, dev profile, 2026-07-28, `--features golden-gate`, on a /// **session-shaped log** (see the module note — the first published table used /// empty causal contexts and understated `reduce` by ~3× at depth 10,000): /// /// | depth | construct | reduce | engrave | **core** | scene-build | paint | verdict | /// |-------|-----------|--------|---------|----------|-------------|-------|---------| /// | 100 | 40 ns | 223 µs | 268 µs | **491 µs** | 133 µs | 2.21 ms | Pass, 34× margin | /// | 1,000 | 40 ns | 2.30 ms | 314 µs | **2.61 ms** | 165 µs | 2.86 ms | Pass, 6.4× margin | /// | 3,000 | 40 ns | 8.79 ms | 327 µs | **9.11 ms** | 152 µs | 2.80 ms | Pass, 55% of budget | /// | 5,000 | 40 ns | 17.53 ms | 317 µs | **17.84 ms** | 151 µs | 2.78 ms | **Xfail, 107%** | /// | 10,000 | 40 ns | 54.07 ms | 260 µs | **54.33 ms** | 123 µs | 1.36 ms | Xfail, 3.3× over | /// /// (Depth 4,000, measured clean but not gated — see the note on the scale /// points: core **12.99 ms**, 78% of budget.) /// /// What the table says, in the order it matters: /// /// 1. **`reduce` is the only column that scales with depth, and it is /// superlinear** — 10× the log costs ~23.5× the time between depths 1,000 /// and 10,000, roughly `O(n^1.4)`. It is 45% of the core's portion at depth /// 100 and 99.5% at depth 10,000. (This does not contradict /// `benches/reduction.rs`'s subquadratic result at 50K envelopes: that log /// is generated across three replicas with a different causal shape. Two /// logs of equal length are not equal work.) /// 2. **The frame budget breaks between 3,000 and 5,000 edits** — 9.11 ms /// (55%), 12.99 ms at 4,000 (78%), then 17.84 ms (107%). Call the wall /// ~4,500 in one sitting, on a dev box rather than the reference hardware /// profile and at median rather than the requirement's p99, so treat it as /// an order of magnitude rather than a threshold. /// 3. **`engrave` is flat and small** — 260–327 µs at every depth, because the /// score it engraves is the same size throughout. At depth 100 it is the /// *larger* half of the core's portion, so criterion 2's "uninformative /// while reduction dominates" holds only past roughly depth 500, not from /// the start. /// 4. **`paint` dominates early and is overtaken by depth ~1,000.** At depth /// 100 it is 2.21 ms against a 491 µs core — 4.5×. By 1,000 they are level /// (2.86 ms vs 2.61 ms). Past that the core runs away. The earlier claim /// that the render path dominates "at realistic depths" holds only for the /// first thousand-odd edits of a session. /// 5. **Almost all of `scene-build` is the SVG serializer, not the IR work.** /// The same rows *without* `golden-gate` — which drop the SVG string and /// leave only `to_render` + `hit_test_map` — measure **3–5 µs**, against /// 133–165 µs with it. So the `RenderIR` and hit-test map cost a few /// microseconds and serializing to SVG costs ~130 µs. Stating that plainly /// matters because "scene-build 133 µs" invites attributing the cost to IR /// construction, which is off by a factor of ~30. /// 6. **What a direct-IR canvas avoids, with the denominator named.** It skips /// the ~129 µs serialize and the 2.21 ms rasterize: 2.34 ms at depth 100. /// That is **83% of the full measured per-edit pipeline** (2.83 ms) and /// **99.8% of the render path alone** (2.34 ms). Both figures are worth /// having and they answer different questions; an unqualified "98%" was /// supported by neither. /// /// **Sequencing, stated carefully.** T4 (the canvas) still comes first: it /// removes the cost that dominates a session's first ~1,000 edits, and it is /// the architecture every later tranche builds on. But T4b's trigger is much /// nearer than the first version of this table suggested — ~4,500 edits in one /// sitting, not ~10,000 — and the two are no longer comfortably separated. /// /// A row that starts missing after being marked `Pass` is a fresh regression — /// fix the pipeline, do not re-mark it `Xfail` without a written decision (the /// discipline `benches/reduction.rs` records). const SCALE_POINTS: &[ScalePoint] = &[ ScalePoint { depth: 100, expectation: Expectation::Pass, gate_iters: (9, 5), criterion_time: Some(Duration::from_secs(6)), }, ScalePoint { // Drafted `Xfail` on the assumption that Fact 8 would already bite // here; it does not, and the gate's XPASS notice said so. Promoted on // first measurement rather than left stale. depth: 1_000, expectation: Expectation::Pass, gate_iters: (5, 3), criterion_time: Some(Duration::from_secs(10)), }, // 3,000 and 5,000 bracket the crossing. They exist because the first // version of this bench put the wall at ~10,000 on a context-free log; with // production-shaped contexts it arrives here instead, and a table that only // sampled decades would have reported the wrong order of magnitude for the // trigger T4b is sequenced against. // // The last `Pass` row is 3,000 rather than 4,000 deliberately. A clean run // puts 4,000 at 12.99 ms — a real pass, but only 78% of budget, and a row // that close flaps the moment the machine is doing anything else (a // load-contaminated run measured it at 22.77 ms, *above* the 5,000 row, // which is impossible clean). A `Pass` row that fails under load teaches // people to ignore the gate. 4,000's clean number is kept as data in THE // STAGE TABLE instead of as a gated row. ScalePoint { depth: 3_000, expectation: Expectation::Pass, gate_iters: (5, 3), criterion_time: Some(Duration::from_secs(12)), }, ScalePoint { depth: 5_000, expectation: Expectation::Xfail( "Fact 8: `apply` re-reduces the whole log onto the pristine base on \ every edit, and each envelope's causal context makes that ordering \ work real, so one keystroke costs more than a frame from roughly \ this depth. T4b (checkpointed reduction + per-system re-engrave) \ owns the fix; engrave is NOT implicated, staying flat in the \ hundreds of microseconds at every depth", ), gate_iters: (5, 3), criterion_time: Some(Duration::from_secs(12)), }, ScalePoint { depth: 10_000, expectation: Expectation::Xfail( "Fact 8, well past the wall — see the 5,000 row. Kept as the \ order-of-magnitude datum, and gate-only because a single timed \ reduction here is tens of milliseconds", ), gate_iters: (3, 0), criterion_time: None, }, ]; /// The fixture every scale point edits: the QUICKSTART 10-measure single-staff /// score (40 quarter notes), fixed so the engrave column is held still while /// log depth moves. fn base_score() -> Score { fixtures::ten_measure_single_staff(0x0000_ED17_5EED) } /// Every pitch in the fixture, in arena order — the targets the edit log /// cycles through. fn pitches(score: &Score) -> Vec { let mut out = Vec::new(); for event in score.events.iter() { let mut ips = Vec::new(); event.collect_identified_pitches(&mut ips); out.extend(ips.into_iter().map(|ip| ip.id)); } out } /// One `TransposeInterval` envelope: `counter` gives it a distinct operation id /// (so the log genuinely deepens rather than re-delivering one operation), and /// `direction` alternates **per pass over the pitch list**, so a pitch edited /// many times oscillates by one semitone instead of drifting. /// /// The alternation must key on the pass, not on `counter`: with an even number /// of target pitches, `counter % 2` is *constant for a given pitch*, so every /// edit to it pushes the same way. That version of this fixture drifted each /// pitch by ±25 semitones at depth 1,000 (and would have by ±250 at 10,000), /// which silently inflated the engrave and paint columns with ledger lines and /// accidentals — a score-content change masquerading as a log-depth cost. fn edit_envelope( counter: u64, pitch: PitchId, direction: i32, causal_context: CausalContext, ) -> OperationEnvelope { let id = OperationId::new(REPLICA, counter); let chromatic_steps = direction; OperationEnvelope { id, author: AuthorId(0), stamp: OperationStamp::new( HybridLogicalClock::new(WallClockTime(counter as i64 + 1), 0), id, ), causal_context, transaction: None, payload: OperationPayload::Primitive(OperationKind::TransposeInterval( TransposeIntervalOp { targets: [pitch].into_iter().collect(), interval: TranspositionInterval { diatonic_steps: 0, chromatic_steps, }, }, )), } } /// The replica the synthetic session authors as. const REPLICA: ReplicaId = ReplicaId(1); /// `EditorSession`'s `extend_context`, reproduced (`editor-core/src/lib.rs`): /// a context grows by absorbing the head into its contiguous vector when the /// head continues that replica's run, and by a dot otherwise. A single-replica /// session with no undo always takes the contiguous branch. fn extend_context(context: CausalContext, op: OperationId) -> CausalContext { let continues = context .vector .get(&op.replica) .map_or(op.counter == 0, |&high| op.counter == high + 1); if continues { context.with_seen(op.replica, op.counter) } else { context.with_dot(op) } } /// A reproducible edit log of `depth` envelopes over `score`'s own pitches, /// **shaped like a real `EditorSession` log**. /// /// Two details are load-bearing, and the first version of this bench got both /// wrong — with the empty-context version understating `reduce` by ~3x at depth /// 10,000, because a context-free log gives the reducer no causal edges to /// order and so skips most of `canonical_reduction_order`'s work: /// /// * **Counters start at 0.** `EditorSession` mints with /// `counter = self.authored.len()`, so the root op is counter 0 — which is /// also what `extend_context` recognises as the start of a contiguous run. /// * **Only the root context is empty.** Every later envelope carries /// `active_prior_context()` — the head's own context extended by the head — /// so it covers the whole active prefix. This is what makes two sequential /// edits to one target read as intentional overwrites rather than concurrent /// conflicts, and it is what the reducer's topological ordering consumes. fn edit_log(score: &Score, depth: usize) -> Vec { let targets = pitches(score); assert!( !targets.is_empty(), "the fixture must carry pitches to transpose" ); let mut log: Vec = Vec::with_capacity(depth); let mut context = CausalContext::new(); for i in 0..depth { let pass = i / targets.len(); let direction = if pass % 2 == 0 { 1 } else { -1 }; let envelope = edit_envelope( i as u64, targets[i % targets.len()], direction, context.clone(), ); context = extend_context(context, envelope.id); log.push(envelope); } log } /// **Stage 0 — envelope construction.** The requirement names it first /// ("operation envelope construction, reduction, incremental layout"), so the /// gated core includes it rather than treating it as setup: this builds the /// *one new* envelope an edit mints, on top of a log already `depth` deep. fn construct(targets: &[PitchId], depth: usize, context: &CausalContext) -> OperationEnvelope { let pass = depth / targets.len(); let direction = if pass % 2 == 0 { 1 } else { -1 }; edit_envelope( depth as u64, targets[depth % targets.len()], direction, context.clone(), ) } /// **Stage 1 — reduce.** `EditorSession::materialize`'s first half: accept the /// whole log into a fresh set and reduce it onto the pristine base. This is /// the work Fact 8 identifies as growing with every keystroke. fn reduce(base: &Score, log: Vec) -> Score { let mut set = OperationSet::new(); for envelope in log { set.accept(envelope); } set.reduce_onto(base).score } /// **Stage 2 — engrave.** `materialize`'s second half, up to the resolved /// layout: project to logical, to constrained, and solve. `to_render` and the /// hit-test map belong to scene-build below. fn engrave(score: &Score, engraver: &Engraver) -> Option { let logical = to_logical(score); let report = engraver.solve(&to_constrained(&logical), &SolverConfig::default()); report.status.is_renderable().then_some(report.layout) } /// **Stage 3 — scene-build.** The `RenderIR` and hit-test map every consumer /// needs, plus (with `golden-gate`) the SVG string today's GUI feeds to the /// rasterizer. Not in the core's budget; see the module note. fn scene_build(resolved: &ResolvedLayoutIR) -> usize { let render = to_render(resolved); let map = render.hit_test_map(); #[cfg(feature = "golden-gate")] { let svg = epiphany_render_svg::render(resolved, &epiphany_render_svg::RenderOptions::default()); map.regions.len() + svg.svg.len() } #[cfg(not(feature = "golden-gate"))] { map.regions.len() } } /// **Stage 4 — paint.** The `usvg` parse + `resvg` rasterize the demo binary /// performs per edit — the path Ruling A demotes to export. Not in the core's /// budget, and measured as the baseline a canvas must beat. #[cfg(feature = "golden-gate")] fn paint(svg: &str) -> Option { use resvg::tiny_skia; use resvg::usvg; let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).ok()?; let size = tree.size().to_int_size(); let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height())?; resvg::render( &tree, tiny_skia::Transform::identity(), &mut pixmap.as_mut(), ); Some(pixmap.data().len()) } /// The pre-built inputs for one scale point, produced **outside** every timed /// section: the base score, the log, the already-reduced score (the engrave /// stage's input), and its resolved layout (scene-build's input). struct StageInputs { base: Score, log: Vec, edited: Score, resolved: ResolvedLayoutIR, /// The construct stage's inputs: the fixture's pitch list and the causal /// context the *next* edit would carry (the head's, extended by the head). targets: Vec, next_context: CausalContext, #[cfg(feature = "golden-gate")] svg: String, } fn stage_inputs(depth: usize, engraver: &Engraver) -> StageInputs { let base = base_score(); let log = edit_log(&base, depth); let edited = reduce(&base, log.clone()); let resolved = engrave(&edited, engraver).expect("the fixture engraves renderably"); let targets = pitches(&base); let next_context = match log.last() { None => CausalContext::new(), Some(head) => extend_context(head.causal_context.clone(), head.id), }; #[cfg(feature = "golden-gate")] let svg = epiphany_render_svg::render(&resolved, &epiphany_render_svg::RenderOptions::default()).svg; StageInputs { base, log, edited, resolved, targets, next_context, #[cfg(feature = "golden-gate")] svg, } } fn criterion_measurements(criterion: &mut Criterion, quick: bool) { let engraver = Engraver::default(); let mut group = criterion.benchmark_group("editor_pipeline"); group.sampling_mode(SamplingMode::Flat); group.sample_size(10); for point in SCALE_POINTS { let Some(time) = point.criterion_time else { continue; // the deepest point is gate-only; see THE STAGE TABLE. }; if quick && point.depth > 100 { continue; // quick mode: the gate still measures the deeper points. } let inputs = stage_inputs(point.depth, &engraver); group.measurement_time(if quick { Duration::from_secs(2) } else { time }); group.warm_up_time(Duration::from_millis(if quick { 500 } else { 1500 })); group.bench_with_input( BenchmarkId::new("construct", point.depth), &inputs, |b, inputs| b.iter(|| construct(&inputs.targets, point.depth, &inputs.next_context)), ); group.bench_with_input( BenchmarkId::new("reduce", point.depth), &inputs, |b, inputs| { b.iter_batched( || inputs.log.clone(), |log| reduce(&inputs.base, log), BatchSize::PerIteration, ) }, ); group.bench_with_input( BenchmarkId::new("engrave", point.depth), &inputs, |b, inputs| b.iter(|| engrave(&inputs.edited, &engraver)), ); group.bench_with_input( BenchmarkId::new("scene_build", point.depth), &inputs, |b, inputs| b.iter(|| scene_build(&inputs.resolved)), ); #[cfg(feature = "golden-gate")] group.bench_with_input( BenchmarkId::new("paint", point.depth), &inputs, |b, inputs| b.iter(|| paint(&inputs.svg)), ); } group.finish(); } /// The budget-gate side. Gates the **core's portion** (reduce + engrave) against /// `req:perf:single-system-edit-latency`; prints the product-layer stages as /// attributed measurements with no budget attached. fn budget_gate(quick: bool) -> Vec { let engraver = Engraver::default(); let mut reports = Vec::new(); println!("\n== staged edit latency (Ruling A criterion 2) =="); for point in SCALE_POINTS { let iters = if quick { point.gate_iters.1 } else { point.gate_iters.0 }; if iters == 0 { println!( "skip edit/core/{}: deepest scale point; full/nightly runs only \ (unset EPIPHANY_BENCH_QUICK)", point.depth ); continue; } let inputs = stage_inputs(point.depth, &engraver); // Envelope construction is the requirement's first named component, so // it is timed and summed rather than treated as setup — even though it // is sub-microsecond and never moves the verdict, because a gate that // silently drops a named component is a proxy, not the gate. let construct_median = budget::median_time( iters, || (), |()| construct(&inputs.targets, point.depth, &inputs.next_context), ); let reduce_median = budget::median_time( iters, || inputs.log.clone(), |log| reduce(&inputs.base, log), ); let engrave_median = budget::median_time(iters, || (), |()| engrave(&inputs.edited, &engraver)); let scene_median = budget::median_time(iters, || (), |()| scene_build(&inputs.resolved)); // The gated row: the core's portion, which is exactly what the // requirement bounds — "operation envelope construction, reduction, // incremental layout through ResolvedLayoutIR". let core = construct_median + reduce_median + engrave_median; println!( "stage edit/{}: construct {:.2?} + reduce {:.2?} + engrave {:.2?} = core {:.2?}; \ scene-build {:.2?} (product layer, no core budget)", point.depth, construct_median, reduce_median, engrave_median, core, scene_median ); #[cfg(feature = "golden-gate")] { let paint_median = budget::median_time(iters, || (), |()| paint(&inputs.svg)); println!( "stage edit/{}: paint {:.2?} (product layer, no core budget; the \ SVG-string path Ruling A demotes to export)", point.depth, paint_median ); } #[cfg(not(feature = "golden-gate"))] println!( "skip edit/{}: scene-build's SVG column and the paint column need \ `--features golden-gate` (epiphany-render-svg + resvg)", point.depth ); reports.push(budget::latency_gate( format!("edit/core/{}", point.depth), core, FRAME_BUDGET, point.expectation, )); } reports } fn main() { let bench_mode = std::env::args().any(|arg| arg == "--bench"); let quick = budget::quick_mode(); let mut criterion = Criterion::default().configure_from_args(); criterion_measurements(&mut criterion, quick); criterion.final_summary(); if !bench_mode { return; } if !budget::verdict(&budget_gate(quick)) { std::process::exit(1); } }