Coarse perf regression test for the MINOR "1000+ node doc render pan/zoom not benchmarked" gap. Three integration tests in `crates/openpencil-shell-core/tests/large_document_perf.rs`: - max_node_id × 100 on 1000-leaf doc < 50 ms (debug build) - node_at_doc_point × 100 across the grid < 25 ms (debug build) - apply_mcp_command(BatchInsert) of 1000 descriptors < 100 ms Bounds are deliberately generous on debug builds — the job is to catch a future 10× regression, not measure wall-clock precision. Real benchmarks via criterion still pending (criterion is not a workspace dep yet); these guards are the bounded alternative. All three pass on the current build.
109 lines
3.9 KiB
Rust
109 lines
3.9 KiB
Rust
//! Coarse perf regression guards for large documents. Not a real
|
||
//! benchmark (no criterion in the workspace yet) — just lower-bound
|
||
//! tests that assert common operations stay reasonable on a 1000+
|
||
//! node document. Tightening the bounds later is an alternative to
|
||
//! adding criterion.
|
||
//!
|
||
//! The gap report flagged that "1000+ node documents render but
|
||
//! pan / zoom is not benchmarked vs Electron". These tests at
|
||
//! least pin the apex hot paths so future patches can't silently
|
||
//! 10× them.
|
||
|
||
use openpencil_shell_core::document::{Document, Node, NodeKind};
|
||
use openpencil_shell_core::{Point2D, Rect};
|
||
use std::time::Instant;
|
||
|
||
/// Build a Document with `n` leaf rect nodes spread across a grid
|
||
/// at the active page root. Used as the perf workload below.
|
||
fn build_doc_with_n_leaves(n: u64) -> Document {
|
||
let mut doc = Document::empty();
|
||
let page = doc.pages.first_mut().expect("default page");
|
||
let cols = 32u64;
|
||
for i in 1..=n {
|
||
let row = (i - 1) / cols;
|
||
let col = (i - 1) % cols;
|
||
let mut node = Node::leaf(i, NodeKind::Rect, format!("rect{i}"));
|
||
node.bounds = Rect::xywh(col as f32 * 24.0, row as f32 * 24.0, 20.0, 20.0);
|
||
page.children.push(node);
|
||
}
|
||
doc
|
||
}
|
||
|
||
#[test]
|
||
fn max_node_id_on_1000_leaves_is_under_50ms() {
|
||
let doc = build_doc_with_n_leaves(1000);
|
||
let t = Instant::now();
|
||
let mut acc: u64 = 0;
|
||
for _ in 0..100 {
|
||
acc = acc.wrapping_add(doc.max_node_id());
|
||
}
|
||
let elapsed_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||
// 100 walks should easily finish in well under 50 ms even on
|
||
// debug builds (the walk is O(node_count) and each is a few
|
||
// i64 comparisons). The bound is generous on purpose — its
|
||
// job is to catch a future 10× regression, not to measure
|
||
// wall-clock precision.
|
||
assert!(
|
||
elapsed_ms < 50.0,
|
||
"max_node_id × 100 took {elapsed_ms:.1} ms on 1000-leaf doc; \
|
||
expected < 50 ms (debug build). Investigate the walker."
|
||
);
|
||
// Touch `acc` so the optimizer doesn't elide the walks.
|
||
assert!(acc > 0);
|
||
}
|
||
|
||
#[test]
|
||
fn node_at_doc_point_on_1000_leaves_under_25ms_per_hit_batch() {
|
||
let doc = build_doc_with_n_leaves(1000);
|
||
let t = Instant::now();
|
||
let mut hits: u64 = 0;
|
||
// Spray 100 points across the populated grid; each call walks
|
||
// top-most-first until a hit. Worst case is the empty space
|
||
// (full walk).
|
||
for i in 0..100 {
|
||
let x = (i * 7 % 32) as f32 * 24.0 + 5.0;
|
||
let y = (i * 11 % 32) as f32 * 24.0 + 5.0;
|
||
if doc.node_at_doc_point(Point2D::new(x, y)).is_some() {
|
||
hits += 1;
|
||
}
|
||
}
|
||
let elapsed_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||
assert!(
|
||
elapsed_ms < 25.0,
|
||
"node_at_doc_point × 100 took {elapsed_ms:.1} ms on 1000-leaf doc; \
|
||
expected < 25 ms (debug build). The hit-test must stay top-most-first \
|
||
+ bail on first match."
|
||
);
|
||
assert!(hits > 0, "at least one of the 100 sprayed points must hit");
|
||
}
|
||
|
||
#[test]
|
||
fn apply_batch_insert_of_1000_descriptors_under_100ms() {
|
||
use openpencil_shell_core::mcp::{BatchInsertItem, McpCommand};
|
||
let mut doc = Document::empty();
|
||
let mut items = Vec::with_capacity(1000);
|
||
for i in 0..1000 {
|
||
let row = i / 32;
|
||
let col = i % 32;
|
||
items.push(BatchInsertItem {
|
||
kind: "rect".into(),
|
||
name: format!("item{i}"),
|
||
x: col as i32 * 24,
|
||
y: row as i32 * 24,
|
||
width: 20,
|
||
height: 20,
|
||
fill_hex: None,
|
||
});
|
||
}
|
||
let cmd = McpCommand::BatchInsert { items };
|
||
let t = Instant::now();
|
||
assert!(doc.apply_mcp_command(&cmd));
|
||
let elapsed_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||
assert!(
|
||
elapsed_ms < 100.0,
|
||
"BatchInsert of 1000 leaves took {elapsed_ms:.1} ms; expected < 100 ms \
|
||
(debug build). Investigate id allocation + Vec growth in mcp_apply."
|
||
);
|
||
assert_eq!(doc.pages[0].children.len(), 1000);
|
||
}
|