perf(glyphs): memoize constraint-glyph placements behind constraints_epoch

Placements (entity lookups, intersections, String labels, projections)
recomputed per view() and per mouse-move hit-test, with overlay re-clones
and 2-3x offset recomputes. New constraints_epoch bumped on every set
mutation/visibility/undo/DWG-load (plus new set_constraint_enabled API);
single-slot Arc<[GlyphEntry]> memo on an 11-field key (hover/selection
excluded); overlay de-cloned/de-duped to the single geometry source;
hit paths reuse the cache.

Tests:
- cached_glyph_placements_hit_returns_same_arc: same-key Arc identity
- cached_glyph_placements_invalidated_by_metadata_edit: edit + selection change yield new Arc
- glyph_hit_test_entries_matches_precomputed_boxes: shared hit-test helper

Bench (cargo bench glyph, 60 constraints): 8.18us -> 0.05-0.15us (~55x).
A/B reference deleted.
This commit is contained in:
Karim Jerbi committed 2026-09-21 22:52:47 +01:00
1 parent 2d49204c50
commit 4432f633e7
12 files changed
+767 -239

No files matched your search

+2 -1
View File
@@ -19,7 +19,7 @@ cargo bench --bench performance_benchmarks -- --quick
---
## Benchmark Suite Catalog (29 Metrics)
## Benchmark Suite Catalog (30 Metrics)
### 1. Scene & Entity Management
- `scene_entity_ingestion_10k`: Ingestion throughput for 10,000 mixed 2D entities (Lines, Circles, Arcs, Polylines).
@@ -55,6 +55,7 @@ cargo bench --bench performance_benchmarks -- --quick
- `ui_statusbar_derived_data`: Status-bar cached derived-data hit (layout names + scale picker `Arc` clones).
- `ui_grip_budget`: Budgeted per-frame selection-grip projection (dense-polyline grips capped to `MAX_SELECTED_GRIPS`).
- `ui_grip_budget_build`: Selection-change `apply_grip_budget` cost over the full grip set (sort + FxHashSet + filter).
- `ui_constraint_glyphs`: Constraint-glyph cache hit (`cached_glyph_placements` key build + `Arc` clone over dozens of glyphs, no recompute).
### 6. State Management & Draworder
- `selection_state_deep_clone`: Deep cloning of `SelectionState` (with 64 polygon points).
+94
View File
@@ -1618,6 +1618,99 @@ fn bench_ui_grip_budget(runner: &mut BenchmarkRunner) {
);
}
// ── 12e. UI Constraint-Glyph Cache Hit (memoised placements) ────────────────
// Covers the parametric constraint-glyph memo (`Scene::cached_glyph_placements`,
// src/scene/parametric_constraints.rs): dozens of constraints whose per-frame
// view + hit-test + dwell + click share one key, so the hot path is a cache
// hit (key build + Arc clone), not a placements recompute (entity lookups,
// intersections, String labels, projections).
//
// NOTE: the bench calls the real `cached_glyph_placements` with the same key
// inputs the production consumers (app/view, viewport hit paths, overlay) and
// the `cached_glyph_placements_*` unit-test fixture use
// (`ParametricScope::ModelSpace`, full-canvas vp, values on, display 3,
// bar 4095), so the measured hit is exactly what the frame shares.
fn bench_ui_constraint_glyphs(runner: &mut BenchmarkRunner) {
if !runner.should_run("ui_constraint_glyphs") {
return;
}
// Fixture: dozens of horizontal constraints over a grid of short lines
// around the origin — mirrors the `cached_glyph_placements_*` unit-test
// fixture (Scene::new + add_entity + constraint add + note applied).
let n_constraints = if runner.quick_mode { 24 } else { 60 };
let mut scene = Scene::new();
let cols = 8;
for i in 0..n_constraints {
let x = ((i % cols) as f64) * 30.0 - 100.0;
let y = ((i / cols) as f64) * 30.0 - 100.0;
let line = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(x, y, 0.0),
Vector3::new(x + 20.0, y, 0.0),
)));
let id = scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
None,
);
scene.note_parametric_constraint_applied(ParametricScope::ModelSpace, id, 3);
}
scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
let vp = (1920.0_f32, 1080.0_f32);
// Prime the cache so every timed call below is a hit (same key, same Arc).
let primed =
scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095);
assert!(
!primed.is_empty(),
"glyph fixture must yield placements"
);
let n_glyphs = primed.len();
// Warm-up for allocator settling.
for _ in 0..10 {
let hit =
scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095);
black_box(hit);
}
let n = if runner.quick_mode { 200 } else { 1_000 };
let runs = 5;
let mut samples = Vec::with_capacity(runs);
for _ in 0..runs {
let t0 = Instant::now();
for _ in 0..n {
let hit = scene.cached_glyph_placements(
black_box(ParametricScope::ModelSpace),
black_box(vp),
black_box(true),
black_box(3),
black_box(4095),
);
black_box(hit);
}
let per_us = (t0.elapsed().as_micros() as f64) / (n as f64);
samples.push(per_us);
}
let median_us = samples[samples.len() / 2];
runner.record(
"ui_constraint_glyphs",
&format!(
"Constraint-glyph cache hit over {} glyphs (key build + Arc clone, no recompute)",
n_glyphs
),
"µs",
samples,
Some(((n_glyphs as f64) / (median_us / 1_000_000.0), "glyphs/s")),
Some(0.5), // Target threshold < 0.5 µs (measured ~0.06 µs quick / ~0.15 µs full, ~3x headroom)
);
}
// ── 13. Wide & Tapered Arc + Donut Tessellation ─────────────────────────────
fn bench_wide_and_tapered_arc_tessellation(runner: &mut BenchmarkRunner) {
@@ -2023,6 +2116,7 @@ fn main() {
bench_ui_plotstyle_layer_usage(&mut runner);
bench_ui_statusbar_derived_data(&mut runner);
bench_ui_grip_budget(&mut runner);
bench_ui_constraint_glyphs(&mut runner);
bench_wide_and_tapered_arc_tessellation(&mut runner);
bench_zoom_extents_calculation(&mut runner);
bench_batch_entity_mutation(&mut runner);