Files
OneCAD-Draft/PERFORMANCE.md
T
Hakan Seven 45f9f96c93 perf: implement L–S performance improvements
- L: pre-project wire endpoints to screen before ApparentIntersection pair loops
- O+S: tight per-segment AABB cull + move a0/a1 Vec3 outside inner snap loop
- P: epoch-cached entity→block reverse map replaces O(E×B) scan
- Q: lazy per-point projection in hit tests eliminates per-wire Vec<Point> alloc
- R: HatchModel.boundary Arc<Vec> so clone is pointer bump not heap alloc
2026-05-06 22:42:22 +03:00

5.0 KiB
Raw Blame History

Performance Improvement Opportunities

Checklist

  • M — model_space_extents() epoch cache + wire AABB shortcut (src/scene/mod.rs)
  • N — entity_wires_arc() returns paper_sheet_wires_arc() directly in model space, no Vec clone (src/scene/mod.rs)
  • L — ApparentIntersection snap: pre-project screen coords per wire before pair loops (src/snap/mod.rs:483)
  • O — Intersection snap: replace loose r_world * 60.0 midpoint cull with tight per-segment AABB overlap check (src/snap/mod.rs:412)
  • S — Intersection snap: move a0/a1 Vec3 conversion outside inner seg_b loop (src/snap/mod.rs:420)
  • P — belongs_to_visible_block(): precompute entity→block reverse map; replace O(B) scan with O(1) HashMap lookup (src/scene/mod.rs:782)
  • Q — Hit test click_hit/box_hit/poly_hit: eliminate per-wire Vec<Point> allocation; iterate lazily with NaN reset (src/scene/hit_test.rs:35)
  • R — HatchModel.boundary: change Vec<[f32;2]> → Arc<Vec<[f32;2]>> so HatchModel::clone() is a pointer bump (src/scene/hatch_model.rs)

Details

L — ApparentIntersection snap: redundant world_to_screen per pair

File: src/snap/mod.rs:483–506

For every segment pair tested, 4 world_to_screen() calls (matrix multiply + perspective divide) are made just to get 2D coordinates for an intersection test. If no intersection is found, all 4 projections are wasted. Dense drawings → O(N²M²) matrix multiplies per snap event.

Fix: Pre-project all segment endpoints for each in-range wire into a Vec<Point> before entering the pair loops. Each endpoint is then projected once regardless of how many pairs it participates in.


M — model_space_extents() re-tessellates everything

File: src/scene/mod.rs:823

model_space_extents() called tessellate_one() for every entity on every ZOOM E / auto-fit. No cached result was used — geometry rebuilt from scratch each call.

Fix: Epoch-keyed cache (model_extents_cache). On cache hit: O(1). On miss with wire cache available: union wire AABBs (no tessellation). Fallback to tessellate_one() only on first call or when wire cache is not for model space.


N — paper_canvas_wires() defeats Arc cache with full Vec clone

File: src/scene/mod.rs:582

In model space, entity_wires_arc() cloned the entire paper_sheet_wires_arc() Vec just to wrap it in a new Arc. The Arc caching was bypassed on every cache miss.

Fix: For model space, return paper_sheet_wires_arc() directly — the two caches share the same Arc. Clone only needed in paper space where viewport-content wires must be appended.


O — Intersection snap O(N²·M²) segment pair testing

File: src/snap/mod.rs:412–441

The segment-level cull inside the intersection pair loop uses r_world * 60.0 midpoint distance, which is extremely loose and passes most segments. Every pair then runs seg_intersect_xy() (cross-product math).

Fix: Replace midpoint cull with tight AABB overlap check per segment pair: compute [min_x, max_x, min_y, max_y] for each segment and skip if the two AABBs don't overlap. Combined with the fix in S (pre-converted Vec3), this eliminates most cross-product calls.


P — belongs_to_visible_block() O(B) scan repeated per entity

File: src/scene/mod.rs:782–787

The fallback path (null owner_handle, entity_handles not populated) iterates all block_records to confirm an entity is not in any other block. Called once per entity per epoch rebuild. O(E × B) total where E = entity count, B = block count.

Fix: Epoch-keyed entity_block_map_cache: HashMap<Handle, Handle> built from block_records[*].entity_handles. Fallback becomes a single HashMap::contains_key lookup.


Q — Hit testing: linear scan, no spatial index

File: src/scene/hit_test.rs:35

click_hit(), box_hit(), poly_hit() allocate a Vec<Point> for every wire by eagerly projecting all points to screen space, even for wires far from the cursor/box. O(E × S) with MB-scale allocation churn on large drawings.

Fix: Iterate lazily — project one point at a time, maintain a prev: Option<Point>, test segment, advance. NaN points reset prev. Eliminates the per-wire Vec allocation entirely.


R — synced_hatch_models(): unconditional HatchModel clone per epoch

File: src/scene/hatch_model.rs, src/scene/mod.rs:1572

Every visible hatch is cloned in full (boundary Vec<[f32;2]> + pattern enum + name String) on every geometry epoch rebuild, even when only angle_offset, scale, or color changes.

Fix: Change HatchModel.boundary to Arc<Vec<[f32;2]>>. HatchModel::clone() then copies only the Arc pointer (8 bytes) for the boundary instead of heap-allocating a new Vec.


S — Intersection snap: repeated Vec3 endpoint copies in inner loop

File: src/snap/mod.rs:420–423

a0 = Vec3::from(seg_a[0]) and a1 = Vec3::from(seg_a[1]) are recomputed for every iteration of the inner seg_b loop.

Fix: Move a0 and a1 conversions outside the inner loop.