Files
OneCAD-Draft/PERFORMANCE.md
T
Hakan SevenandClaude Sonnet 4.6 9c8c9a4060 perf: cache SortEntitiesTable scan (option E)
Replace the O(objects) linear scan inside wires_for_block() with a
sort_cache keyed by geometry_epoch. On a cache miss the index is built
once by scanning all document objects; subsequent calls within the same
epoch do an O(1) HashMap lookup (block_handle → entity→sort map).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-26 02:17:40 +03:00

125 lines
5.1 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 Optimization Plan
## Problem
The application lags severely when large files are open — even simple mouse movement causes noticeable frame drops.
### Root Cause
Every frame (triggered by any mouse event), Iced calls `shader::Program::draw()` → `build_primitive()`.
This function:
1. **Re-tessellates every entity from scratch** — `wires_for_block()` iterates all document entities and calls `tessellate_one()` for each, every single frame.
2. **Recreates all GPU buffers** — `prepare()` calls `upload_wires/hatches/images/meshes()`, each of which allocates new `wgpu::Buffer` objects for every entity, every frame.
3. **Linear-scans all objects** to find `SortEntitiesTable` on every frame.
During pure mouse navigation (pan/zoom), nothing in the geometry data changes — only the camera matrix (uniforms) changes. Despite this, all work above is repeated for every mouse-move event.
---
## Option A — Wire Tessellation Cache ✅ Implemented
**Status:** Done
Added `wire_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>` to `Scene`.
`entity_wires_arc()` checks the cache first; if `geometry_epoch` matches it returns the cached
`Arc` (O(1) refcount bump). On a cache miss it tessellates, wraps the result in `Arc`, stores it,
and returns a clone of the `Arc`.
`build_primitive()` uses `entity_wires_arc()` directly:
- **No preview wires (navigation):** stores the Arc as-is — zero Vec clones, zero tessellation.
- **Command active (preview wires present):** clones the cached entity Vec once, appends
preview/interim wires, wraps in a new Arc — only one allocation instead of full tessellation.
`Primitive::wires` changed from `Vec<WireModel>` to `Arc<Vec<WireModel>>`.
**Impact:** During navigation in large files the per-frame cost of the wire step drops from
O(entities × tessellation) to O(1). Combined with Option B the GPU upload is also skipped,
making navigation frames nearly free regardless of file size.
---
## Option B — GPU Buffer Cache ✅ Implemented
**Status:** Done
Add a `geometry_epoch: u64` counter to `Scene`. Bump it whenever geometry-affecting state changes.
Carry the epoch through `Primitive` to `Pipeline`, which stores `cached_epoch: u64`.
In `prepare()`:
- **Always** upload uniforms (camera changes every frame).
- **Skip** geometry buffer uploads (`upload_wires/hatches/images/meshes`) when `geometry_epoch == cached_epoch`.
- After uploading, set `pipeline.cached_epoch = geometry_epoch`.
**Impact:** During navigation, only a single 192-byte uniform write reaches the GPU instead of
re-creating thousands of buffers. Fixes mouse-movement lag in large files.
**Difficulty:** Medium. Requires bumping the epoch in all geometry-mutation paths.
### Epoch bump locations
| Method | Location |
|--------|----------|
| `add_entity()` | `scene/mod.rs` |
| `erase_entities()` | `scene/mod.rs` |
| `populate_hatches_from_document()` | `scene/mod.rs` |
| `populate_images_from_document()` | `scene/mod.rs` |
| `populate_meshes_from_document()` | `scene/mod.rs` |
| `set_preview_wires()` | `scene/mod.rs` |
| `clear_preview_wire()` | `scene/mod.rs` |
| `set_interim_wire()` | `scene/mod.rs` |
| `select_entity()` | `scene/mod.rs` |
| `deselect_all()` | `scene/mod.rs` |
| `expand_selection_for_groups()` | `scene/mod.rs` |
| `toggle_layer_visibility()` | `scene/mod.rs` |
| `transform_entities()` | `scene/mod.rs` |
| `copy_entities()` | `scene/mod.rs` |
| `apply_grip()` | `scene/mod.rs` |
| `clear()` | `scene/mod.rs` |
---
## Option C — Parallel Tessellation (Rayon)
**Status:** Planned
Replace the sequential `.flat_map(|e| self.tessellate_one(e))` chain in `wires_for_block()` with
`rayon::par_iter()`.
**Impact:** Spreads tessellation across all CPU cores. Improves file-open time and post-edit
refresh. Does **not** fix per-frame navigation lag (Option A/B address that).
**Difficulty:** Easy. Requires making `tessellate_one` free of interior mutability, or collecting
into a Vec first then parallel-processing.
---
## Option D — Frustum / Viewport Culling
**Status:** Planned
Build a spatial index (R-tree or uniform grid) over entity bounding boxes.
During `wires_for_block()`, skip entities whose bounding box lies entirely outside the camera frustum.
**Impact:** Dramatic speedup for large drawings when zoomed in — only visible entities are
tessellated and uploaded. No benefit when the full drawing is visible.
**Difficulty:** Hard. Requires maintaining an up-to-date spatial index and computing per-entity
bounding boxes for all entity types.
---
## Option E — SortEntitiesTable Scan Cache ✅ Implemented
**Status:** Done
Added `sort_cache: RefCell<Option<(u64, HashMap<Handle, HashMap<u64, u64>>)>>` to `Scene`.
On the first call to `wires_for_block()` after a geometry change, the cache scans all
`document.objects` once and builds an index: `block_handle → (entity_handle.value() →
sort_handle.value())`. Subsequent calls within the same epoch do an O(1) HashMap lookup
instead of the previous O(objects) `find_map` scan.
The cache is keyed by `geometry_epoch` and invalidated automatically alongside the wire
and GPU caches whenever geometry changes.