From 648f0ff76840d42d85d2dc921e41ba2c8e152563 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Tue, 13 Oct 2026 19:44:46 -0400 Subject: [PATCH] Open large designs faster: count the cache budget without cloning wires (#911) Body::retained_bytes counts face and edge uses on the borrowed boundaries (the same numbers, checked by a test), and each model body keeps the footprint measured when a feature made or changed it, so the evaluation cache's budget no longer walks every body's topology after every feature. --- crates/doc/src/eval.rs | 8 +++++++- crates/kernel/src/body.rs | 11 ++++++++++- crates/kernel/src/tests.rs | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index c09d6253..8a5ca8c5 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -44,6 +44,9 @@ pub struct ModelBody { pub transferred: bool, /// The look it was imported with, carried through later features. pub imported: Option>, + /// [`Body::retained_bytes`] of `body`, measured when a feature made or changed it (0: not + /// measured), so the evaluation cache's budget does not walk every body after every feature. + bytes: usize, } impl ModelBody { @@ -62,6 +65,7 @@ impl ModelBody { component: 0, transferred: false, imported: None, + bytes: 0, } } pub fn definition_name(&self) -> &str { @@ -486,6 +490,7 @@ impl ModelState { Some(x) => { b.key = x.key; b.names = x.names.clone(); + b.bytes = if x.bytes > 0 { x.bytes } else { b.body.retained_bytes() }; } None => { let mut h = std::collections::hash_map::DefaultHasher::new(); @@ -507,6 +512,7 @@ impl ModelState { { b.imported = crate::appearance::ImportedLook::of(old); } + b.bytes = b.body.retained_bytes(); } } } @@ -597,7 +603,7 @@ impl ModelState { /// topology is what makes a kept timeline snapshot expensive, so it dominates; sketches /// and the rest are counted from their entity counts. pub fn retained_bytes(&self) -> usize { - let bodies: usize = self.bodies.iter().map(|b| b.body.retained_bytes()).sum(); + let bodies: usize = self.bodies.iter().map(|b| if b.bytes > 0 { b.bytes } else { b.body.retained_bytes() }).sum(); let sketches: usize = self.sketches.iter().map(|s| s.retained_bytes()).sum(); bodies + sketches + self.sheets.len() * 4096 + self.threads.len() * 192 + self.construct.len() * 192 } diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 211cfcd8..a0e9710f 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -939,7 +939,16 @@ impl Body { if let Some(m) = &self.mesh { return m.positions.len() * 24 + m.triangles.len() * 12; } - self.solid.face_iter().count() * 2048 + self.solid.edge_iter().count() * 512 + self.solid.vertex_iter().count() * 96 + // Counted on the borrowed boundaries: `Solid::edge_iter` and `vertex_iter` clone every + // face's wires to walk them, which made this estimate (taken after every feature, over + // every body) cost more than evaluating most features. Each edge use is one edge and + // one vertex in those iterators, so the counts are the same. + let (mut faces, mut uses) = (0usize, 0usize); + for face in self.solid.face_iter() { + faces += 1; + uses += face.absolute_boundaries().iter().map(|w| w.len()).sum::(); + } + faces * 2048 + uses * (512 + 96) } /// Distinct edges in a stable order (the order of `edges()` and `tessellate().edges`). diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index abd2a89a..6d180db6 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -2312,3 +2312,17 @@ fn shell_of_a_disc_rounded_smaller_than_the_wall() { } } } + +/// The cache-budget estimate counts edge and vertex uses on the borrowed boundaries; it must +/// equal what walking the cloned wires (`Solid::edge_iter`, `vertex_iter`) counts. +#[test] +fn retained_bytes_matches_the_iterator_counts() { + let block = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 10.0)).unwrap(); + let hole = cylinder(Vec3::new(20.0, 15.0, -5.0), Vec3::Z, 5.0, 30.0).unwrap(); + let drilled = boolean(&block, &hole, BoolOp::Cut).unwrap().unwrap(); + for body in [&block, &hole, &drilled] { + let s = &body.solid; + let walked = s.face_iter().count() * 2048 + s.edge_iter().count() * 512 + s.vertex_iter().count() * 96; + assert_eq!(body.retained_bytes(), walked); + } +}