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

109 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Performance Improvement Opportunities
## Checklist
- [x] **M** — `model_space_extents()` epoch cache + wire AABB shortcut (`src/scene/mod.rs`)
- [x] **N** — `entity_wires_arc()` returns `paper_sheet_wires_arc()` directly in model space, no Vec clone (`src/scene/mod.rs`)
- [x] **L** — ApparentIntersection snap: pre-project screen coords per wire before pair loops (`src/snap/mod.rs:483`)
- [x] **O** — Intersection snap: replace loose `r_world * 60.0` midpoint cull with tight per-segment AABB overlap check (`src/snap/mod.rs:412`)
- [x] **S** — Intersection snap: move `a0/a1` Vec3 conversion outside inner `seg_b` loop (`src/snap/mod.rs:420`)
- [x] **P** — `belongs_to_visible_block()`: precompute entity→block reverse map; replace O(B) scan with O(1) HashMap lookup (`src/scene/mod.rs:782`)
- [x] **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`)
- [x] **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.