Skip to main content

blockworx_bench/
lib.rs

1//! What a user waits for, on the settled 50×50 grid, as scenarios both the
2//! criterion benches (`benches/kernel.rs`) and the phase breakdown
3//! (`examples/spans.rs`) run — so the number and its breakdown are of the same
4//! thing.
5//!
6//! - `cargo bench -p blockworx-bench` times them; `-- --save-baseline <name>`
7//!   and `-- --baseline <name>` compare runs.
8//! - `cargo run --release -p blockworx-bench --example spans` tallies where
9//!   each one's time goes, span by span. The tally is attribution, not timing:
10//!   the subscriber costs time of its own.
11//!
12//! Each scenario is one kernel call as the frame loop makes it, over a
13//! container in memory so a commit pays for the rev it writes. Where the call
14//! could quietly do nothing — a nudge refused, a drop that moves nothing — its
15//! preparation checks that it does not, since it would time nothing.
16
17use blockworx_doc::id::BlockId;
18use blockworx_editor::shape::ShapeId;
19use blockworx_geom::{Pos2, Rect, Vec2, grid::GRID_SIZE, vec2};
20use blockworx_kernel::{
21    Event, Session,
22    driving::{SettledGrid, VIEWPORT, batch_in, blocks, down, moved, up},
23    kernel,
24};
25use blockworx_paint::Move;
26use blockworx_text::Shaper;
27use blockworx_tools::{
28    commands::{CommandId, Heading},
29    tool::Action,
30};
31
32/// The grid's side. 2,500 blocks and 7,550 wires: the scale the region router
33/// was built for.
34pub const SIDE: usize = 50;
35
36/// One session under test, and what driving it needs.
37pub struct Rig<'a> {
38    grid: &'a SettledGrid,
39    layout: &'a Shaper,
40    session: Session,
41    millis: u64,
42    turn: usize,
43    /// Where the scenario's block rests, for the ones that move it.
44    home: Pos2,
45    /// The canvas each batch reports.
46    viewport: Rect,
47}
48
49impl<'a> Rig<'a> {
50    /// A session over `grid`, opened — its first frame drawn.
51    #[must_use]
52    pub fn opened(grid: &'a SettledGrid, layout: &'a Shaper) -> Self {
53        let mut rig = Self {
54            grid,
55            layout,
56            session: grid.session(),
57            millis: 0,
58            turn: 0,
59            home: Pos2::ZERO,
60            viewport: VIEWPORT,
61        };
62        rig.call(Vec::new());
63        rig
64    }
65
66    /// One batch, one 60 Hz frame after the last.
67    pub fn call(&mut self, events: Vec<Event>) {
68        self.millis += 16;
69        kernel(
70            &mut self.session,
71            batch_in(self.viewport, self.millis, events),
72            self.layout,
73        );
74    }
75
76    /// Which of two things this turn is, alternating — so a block walks back
77    /// and forth rather than into its neighbour.
78    fn alternate<T: Copy>(&mut self, pair: [T; 2]) -> T {
79        let picked = pair[self.turn % 2];
80        self.turn += 1;
81        picked
82    }
83
84    /// The block the scenarios work on: the grid's first, whose neighbours sit
85    /// eight cells away on either side — room to move two cells each way.
86    fn subject(&self) -> BlockId {
87        blocks(&self.session)[1]
88    }
89
90    #[expect(
91        clippy::expect_used,
92        reason = "a scenario whose block is missing has nothing to measure"
93    )]
94    fn centre(&mut self, block: BlockId) -> Pos2 {
95        self.session
96            .drawing()
97            .shape(ShapeId::Rect(block))
98            .expect("the block is on the opening level")
99            .gui_rect()
100            .center()
101    }
102
103    fn select(&mut self, block: BlockId) {
104        self.call(vec![Event::Action(Action::NavSelect {
105            block,
106            extend: false,
107        })]);
108    }
109
110    fn rev(&self) -> u64 {
111        self.session.doc.repo().rev().get()
112    }
113}
114
115/// Two cells to the right: a move that stays clear of the neighbours.
116const TWO_CELLS: Vec2 = vec2(2.0 * GRID_SIZE, 0.0);
117
118/// A call a user waits for, in three parts: what sets it up once, what comes
119/// before each one without being part of it, and the call itself.
120pub struct Scenario {
121    pub name: &'static str,
122    pub prepare: fn(&mut Rig<'_>),
123    pub approach: fn(&mut Rig<'_>),
124    pub timed: fn(&mut Rig<'_>),
125}
126
127fn nothing(_: &mut Rig<'_>) {}
128
129/// The widths, in world px, of the two framings the zoom scenario swaps
130/// between — about 20% and 21% on the benches' 800 px viewport.
131const ZOOMED: [f32; 2] = [4_000.0, 3_800.0];
132
133/// A world rect of `width` about the sheet's middle, in the viewport's shape.
134fn far_out(width: f32) -> Rect {
135    Rect::from_center_size(Pos2::new(6_000.0, 6_000.0), vec2(width, width * 0.75))
136}
137
138fn frame(rig: &mut Rig<'_>) {
139    rig.call(Vec::new());
140}
141
142/// The browser's canvas on the MBP, in CSS px: what the far-zoom rendering
143/// work measured the canvas and the GPU at.
144const BROWSER: Rect = Rect {
145    min: Pos2::ZERO,
146    max: Pos2 {
147        x: 1_600.0,
148        y: 1_000.0,
149    },
150};
151
152/// The browser's canvas, framed at `zoom` about the sheet's middle — or the
153/// whole sheet fitted (the 10% floor) when `None` — checked to land there.
154/// A framing pads the rect it is given, so the rect is corrected by the zoom
155/// the last framing landed at.
156fn framed_in_browser(rig: &mut Rig<'_>, zoom: Option<f32>) {
157    rig.viewport = BROWSER;
158    let zoom_now = |rig: &Rig<'_>| f32::from(rig.session.vantage().zoom);
159    match zoom {
160        None => rig.call(vec![Event::Action(Action::ResetView)]),
161        Some(zoom) => {
162            let mut size = vec2(BROWSER.width() / zoom, BROWSER.height() / zoom);
163            for _ in 0..3 {
164                let rect = Rect::from_center_size(Pos2::new(6_000.0, 6_000.0), size);
165                rig.call(vec![Event::Action(Action::FrameRect(rect))]);
166                size *= zoom_now(rig) / zoom;
167            }
168        }
169    }
170    rig.call(Vec::new());
171    let landed = zoom_now(rig);
172    let wanted = zoom.unwrap_or(0.1);
173    assert!(
174        (landed - wanted).abs() < 0.01,
175        "framed at {landed}, not {wanted}"
176    );
177}
178
179/// A pan a trackpad frame carries, back and forth: the camera moves and
180/// nothing else does.
181fn pan(rig: &mut Rig<'_>) {
182    let by = rig.alternate([vec2(12.0, 0.0), vec2(-12.0, 0.0)]);
183    let before = rig.session.vantage();
184    rig.call(vec![Event::Move(Move::Pan(by))]);
185    debug_assert_ne!(rig.session.vantage(), before, "the pan moved nothing");
186}
187
188/// Every scenario, in the order they are reported.
189pub const SCENARIOS: &[Scenario] = &[
190    // Opening: the first frame re-derives every wire from its corners.
191    Scenario {
192        name: "open",
193        prepare: nothing,
194        approach: |rig| {
195            rig.session = rig.grid.session();
196            rig.millis = 0;
197        },
198        timed: frame,
199    },
200    // A frame with nothing happening: the draw, at the opening zoom.
201    Scenario {
202        name: "frame",
203        prepare: nothing,
204        approach: nothing,
205        timed: frame,
206    },
207    // The same with the whole sheet in view: every block and wire drawn.
208    Scenario {
209        name: "frame_at_fit",
210        prepare: |rig| {
211            rig.call(vec![Event::Action(Action::ResetView)]);
212            rig.call(Vec::new());
213        },
214        approach: nothing,
215        timed: frame,
216    },
217    // The pointer crossing the sheet with the whole of it in view: every move
218    // is a frame, hit-tested and redrawn, though nothing is pressed. Two
219    // points a block apart, one over a block and one over the wires between.
220    Scenario {
221        name: "hover_at_fit",
222        prepare: |rig| {
223            rig.call(vec![Event::Action(Action::ResetView)]);
224            rig.call(Vec::new());
225        },
226        approach: nothing,
227        timed: |rig| {
228            let at = rig.alternate([Pos2::new(400.0, 300.0), Pos2::new(412.0, 306.0)]);
229            rig.call(vec![moved(at)]);
230        },
231    },
232    // Far out, at two zooms 5% apart — nearly the same labels in view, laid
233    // out at two screen sizes: what each frame of a zoom gesture pays, where
234    // the frames above hold the zoom still and find their layouts cached.
235    Scenario {
236        name: "zoom_far_out",
237        prepare: |rig| {
238            let zoom_at = |rig: &mut Rig<'_>, width| {
239                rig.call(vec![Event::Action(Action::FrameRect(far_out(width)))]);
240                f32::from(rig.session.vantage().zoom)
241            };
242            let (near, far) = (zoom_at(rig, ZOOMED[0]), zoom_at(rig, ZOOMED[1]));
243            assert!(
244                (near - far).abs() > 0.001 && near.max(far) < 0.3,
245                "the two framings are two far-out zooms: {near} and {far}"
246            );
247        },
248        approach: nothing,
249        timed: |rig| {
250            let width = rig.alternate(ZOOMED);
251            rig.call(vec![Event::Action(Action::FrameRect(far_out(width)))]);
252        },
253    },
254    // A trackpad pan with the whole sheet in the browser's canvas: every
255    // frame of it re-records the scene the renderer then draws.
256    Scenario {
257        name: "pan_at_fit",
258        prepare: |rig| framed_in_browser(rig, None),
259        approach: nothing,
260        timed: pan,
261    },
262    // The same at 24%, where the labels are legible.
263    Scenario {
264        name: "pan_at_24",
265        prepare: |rig| framed_in_browser(rig, Some(0.24)),
266        approach: nothing,
267        timed: pan,
268    },
269    // A selection change and the frame that draws it.
270    Scenario {
271        name: "select",
272        prepare: nothing,
273        approach: nothing,
274        timed: |rig| {
275            let pair = [blocks(&rig.session)[1], blocks(&rig.session)[2]];
276            let block = rig.alternate(pair);
277            rig.select(block);
278            rig.call(Vec::new());
279        },
280    },
281    // A nudge of one selected block and the frame after it.
282    Scenario {
283        name: "nudge",
284        prepare: |rig| {
285            let block = rig.subject();
286            rig.select(block);
287            let before = rig.rev();
288            rig.call(vec![Event::Command(CommandId::Nudge(Heading::Right))]);
289            assert_ne!(rig.rev(), before, "the nudge wrote nothing");
290        },
291        approach: nothing,
292        timed: |rig| {
293            let heading = rig.alternate([Heading::Left, Heading::Right]);
294            rig.call(vec![Event::Command(CommandId::Nudge(heading))]);
295            rig.call(Vec::new());
296        },
297    },
298    // One frame of a block being dragged: the preview re-solves the wires it
299    // carries. The pointer moves between one and two cells out, both clear.
300    Scenario {
301        name: "drag_frame",
302        prepare: |rig| {
303            let block = rig.subject();
304            rig.home = rig.centre(block);
305            let home = rig.home;
306            rig.call(vec![down(home)]);
307        },
308        approach: nothing,
309        timed: |rig| {
310            let cell = vec2(GRID_SIZE, 0.0);
311            let offset = rig.alternate([cell, 2.0 * cell]);
312            let at = rig.home + offset;
313            rig.call(vec![moved(at)]);
314        },
315    },
316    // The release that ends a two-cell drag, and the frame after it. The press
317    // and the moves before it are the drag frame's, so they are the approach.
318    Scenario {
319        name: "drop",
320        prepare: |rig| {
321            let block = rig.subject();
322            rig.home = rig.centre(block);
323            let before = rig.rev();
324            drop_approach(rig);
325            drop_release(rig);
326            assert_ne!(rig.rev(), before, "the drop wrote nothing");
327            assert_eq!(
328                rig.centre(block),
329                rig.home + TWO_CELLS,
330                "the drop moved the block two cells"
331            );
332        },
333        approach: drop_approach,
334        timed: drop_release,
335    },
336];
337
338/// Where this turn's drag goes: out two cells, then back.
339fn drop_ends(rig: &Rig<'_>) -> (Pos2, Pos2) {
340    let (home, out) = (rig.home, rig.home + TWO_CELLS);
341    if rig.turn.is_multiple_of(2) {
342        (home, out)
343    } else {
344        (out, home)
345    }
346}
347
348/// The press and the moves of a drag. Past the click distance by a pixel
349/// first, as a pointer crosses it: the move that starts a drag carries no
350/// travel of its own.
351fn drop_approach(rig: &mut Rig<'_>) {
352    let (from, to) = drop_ends(rig);
353    let started = from + (to - from).normalized() * 7.0;
354    rig.call(vec![down(from)]);
355    rig.call(vec![moved(started)]);
356    rig.call(vec![moved(to)]);
357}
358
359fn drop_release(rig: &mut Rig<'_>) {
360    let (_, to) = drop_ends(rig);
361    rig.call(vec![up(to)]);
362    rig.call(Vec::new());
363    rig.turn += 1;
364}