From f347a20a4c4e8411b1f80aaf34c2cb7f9833fdf5 Mon Sep 17 00:00:00 2001 From: Kevin Griffin Date: Wed, 22 Jul 2026 12:56:36 -0700 Subject: [PATCH] perf(scene): return the memoized wire set by Arc, not by deep copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit entity_wires() cloned the entire wire set — every point, fill, and snap buffer of every entity — on each call, although the set is already memoized behind an Arc and none of the 18 call sites mutates it. On large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M triangles) that is a multi-GB allocation + memcpy on every pick, snap, selection, or plot pass. Return the Arc instead. Call sites iterate as before; the two window-plot paths that build a culled owned subset now clone only the wires inside the plot window, and the print pipeline moves the Arc into its async task. Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release): resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face). Co-Authored-By: Claude Fable 5 --- src/app/update/file.rs | 10 ++++++---- src/io/print_to_printer.rs | 8 ++++---- src/scene/mod.rs | 14 +++++++++----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 0569b7f78..c968f6200 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1335,10 +1335,11 @@ pub(super) fn on_open_file(&mut self) -> Task { let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); let wires: Vec<_> = scene .entity_wires() - .into_iter() + .iter() .filter(|w| { w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0 }) + .cloned() .collect(); let hatches = scene.paper_canvas_hatches(); let wipeouts = scene.paper_canvas_wipeouts(); @@ -1388,7 +1389,7 @@ pub(super) fn on_open_file(&mut self) -> Task { pub(super) fn layout_plot_params( &self, ) -> ( - Vec, + std::sync::Arc>, Vec, Vec, f64, @@ -1496,7 +1497,7 @@ pub(super) fn on_open_file(&mut self) -> Task { let mut x1 = f32::NEG_INFINITY; let mut y1 = f32::NEG_INFINITY; let mut any = false; - for w in scene.entity_wires() { + for w in scene.entity_wires().iter() { let picked = crate::scene::Scene::handle_from_wire_name(&w.name) .is_some_and(|h| set.contains(&h)); if !picked { @@ -2150,8 +2151,9 @@ pub(super) fn on_open_file(&mut self) -> Task { let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); let wires: Vec<_> = scene .entity_wires() - .into_iter() + .iter() .filter(|w| w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0) + .cloned() .collect(); let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone(); let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone(); diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 1baa17908..586eee0f9 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -36,7 +36,7 @@ pub struct PrintOptions { // is native-only; the web build gets a stub so the call site still compiles. #[cfg(target_arch = "wasm32")] pub async fn print_wires( - _wires: Vec, + _wires: std::sync::Arc>, _hatches: Vec, _wipeouts: Vec, _paper_w: f64, @@ -57,7 +57,7 @@ pub fn list_printers() -> Vec { #[cfg(target_arch = "wasm32")] #[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - _wires: Vec, + _wires: std::sync::Arc>, _hatches: Vec, _wipeouts: Vec, _paper_w: f64, @@ -87,7 +87,7 @@ pub fn print_existing_pdf(_path: &std::path::Path, _opts: &PrintOptions) -> Resu /// Returns `Ok(printer_name)` on success or `Err(message)` on failure. #[cfg(not(target_arch = "wasm32"))] pub async fn print_wires( - wires: Vec, + wires: std::sync::Arc>, hatches: Vec, wipeouts: Vec, paper_w: f64, @@ -150,7 +150,7 @@ pub fn list_printers() -> Vec { #[cfg(not(target_arch = "wasm32"))] #[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - wires: Vec, + wires: std::sync::Arc>, hatches: Vec, wipeouts: Vec, paper_w: f64, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5fc17cfc4..c498108a2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2851,9 +2851,9 @@ impl Scene { /// Collect closed polygon outlines (world XY) from the current layout. pub fn closed_outlines(&self) -> Vec> { self.entity_wires() - .into_iter() + .iter() .filter_map(|wire| { - let pts = wire.points; + let pts = &wire.points; if pts.len() < 4 { return None; } @@ -2872,7 +2872,7 @@ impl Scene { // boundary commands) see one vertex per corner — not the doubled // ring that otherwise shows two grips at every corner. let mut ring: Vec<[f32; 2]> = Vec::with_capacity(pts.len()); - for p in &pts { + for p in pts { if !p[0].is_finite() || !p[1].is_finite() { continue; } @@ -3342,8 +3342,12 @@ impl Scene { } /// Build WireModels from all document entities + optional preview wire. - pub fn entity_wires(&self) -> Vec { - (*self.entity_wires_arc()).clone() + /// + /// Returns the memoized set by `Arc` — callers only ever iterate it, and + /// the previous per-call deep clone was a full copy of every wire buffer + /// (hundreds of MB on large mesh imports, #358). + pub fn entity_wires(&self) -> Arc> { + self.entity_wires_arc() } /// Per-entity normalized draw-order depth, keyed by entity handle value.