cad-editor/PERFORMANCE.md
Hakan Seven 81db3c37d0 perf: cache hatch/wipeout/image/mesh models per geometry epoch (F+G+H)
Add Arc<Vec<...>>-backed epoch caches for hatches, wipeouts, images, and
meshes — the same pattern used for wires. build_primitive() now returns
an O(1) Arc refcount bump on navigation frames instead of rebuilding
every collection from scratch.

Also wrap ImageModel.pixels in Arc<Vec<u8>> so cloning an ImageModel is
O(1) regardless of image resolution.

Navigation frames no longer copy pixel data, tessellated hatch boundaries,
or mesh geometry — all per-frame CPU work on unchanged geometry is now
eliminated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 11:34:52 +03:00

4.5 KiB
Raw Blame History

Performance Optimization Plan

Implemented

Option Description
A Wire tessellation cache — Arc<Vec<WireModel>> keyed by geometry_epoch; O(1) on navigation
B GPU buffer cache — skip upload_wires/hatches/images/meshes when epoch unchanged
C Rayon parallel tessellation — tessellate_entity free function + par_iter() in wires_for_block()
E SortEntitiesTable scan cache — O(objects) linear scan replaced with O(1) HashMap lookup

Remaining per-frame CPU work (navigation path)

With Options A/B implemented, build_primitive() still runs every frame during navigation (pan/zoom), even though the geometry epoch has not changed. The following work happens on every frame and should be eliminated:

1. ImageModel.pixels cloned every frame — highest impact

build_primitive() calls self.images.values().cloned().collect(). ImageModel contains pixels: Vec<u8> — raw RGBA pixel data for each image. A single 2000×1500 image is ~12 MB. With three images, every mouse-move event copies ~36 MB.

2. synced_hatch_models() — full document scan + Insert explosion every frame

The inner loop for entity in self.document.entities() iterates every entity in the document, and for each Insert calls explode_from_document() to find embedded hatches. In drawings with many block references this is O(inserts × avg_block_size) per frame.

3. meshes.values().cloned().collect() — 3D geometry cloned every frame

MeshModel contains verts: Vec<[f32;3]> and indices: Vec<u32>. A tessellated 3D solid can hold tens of thousands of triangles (several MB). All mesh data is copied on every frame even during pure navigation.

4. wipeout_models() — full entity scan every frame

Iterates self.document.entities() to find Wipeout entities and rebuild their boundary polygons. No caching; O(entities) on every frame.


Option F — Cache hatch and wipeout models

Status: Done

Add epoch-keyed Arc caches for hatches and wipeouts, identical in structure to the wire cache:

hatch_cache:   RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,
wipeout_cache: RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,

Introduce hatch_models_arc() and wipeout_models_arc() helpers that return the cached Arc on an epoch hit (O(1) refcount bump) and rebuild on a miss.

build_primitive() stores the Arc directly in Primitive, removing the per-frame synced_hatch_models() / wipeout_models() calls on navigation frames.

Primitive.hatches and Primitive.wipeout_hatches change from Vec<HatchModel> to Arc<Vec<HatchModel>>.

Impact: Eliminates O(inserts × block_size) work per frame in drawings with block references containing hatch entities. Navigation becomes free for hatch-heavy files.

Difficulty: Easy. Same pattern as the wire cache.


Option G — Arc-wrap image pixels

Status: Done

Change ImageModel.pixels from Vec<u8> to Arc<Vec<u8>>.

ImageModel::clone() then copies only the pointer (8 bytes) instead of megabytes of pixel data. No other code needs to change — the GPU upload path reads pixels by reference.

Add an epoch-keyed Arc<Vec<ImageModel>> cache (same pattern as wire cache) so build_primitive() performs only an O(1) Arc bump per frame during navigation:

image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,

Impact: Eliminates MB-scale copies per frame in files that contain raster images. With a 4K image (~32 MB raw), this alone can cut per-frame CPU time by tens of milliseconds.

Difficulty: Easy. Change one field type + add cache field + wire up in build_primitive().


Option H — Arc-wrap mesh models

Status: Done

Apply the same Arc<Vec<MeshModel>> epoch cache to mesh models:

mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshModel>>)>>,

MeshModel contains verts: Vec<[f32;3]> (vertex positions) and indices: Vec<u32> (triangle list). A complex 3D solid can have hundreds of thousands of triangles.

Impact: Eliminates per-frame mesh data copies in files that contain 3D solids (ACIS bodies, extruded polylines). Navigation stays free regardless of model complexity.

Difficulty: Easy. Same pattern as Options F and G.


Implementation order

F → G → H. Each is independent; all three can be done in a single pass since the pattern is identical. F has the most complex rebuild logic (Insert explosion); G has the highest raw byte impact; H is the simplest but matters for 3D-heavy files.