Large STEP imports: no more multi-minute hang, shared STEP text, cancel and progress (#925)

This commit is contained in:
bherbruck authored and GitHub committed 2026-10-14 05:01:12 -04:00
1 parent dfbf7dd6a8
commit 2f91338d3d
18 files changed
+525 -45

No files matched your search

+3 -2
View File
@@ -1032,8 +1032,9 @@ pub enum FeatureKind {
Import {
/// Source file name (display only).
file: String,
/// The STEP (ISO 10303-21) text.
step: String,
/// The STEP (ISO 10303-21) text, shared by every copy of the design (undo states,
/// previews, scratch sessions): a large file is held once, not once per edit.
step: std::sync::Arc<str>,
/// The file's product/assembly tree, kept for components.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
components: Vec<solidcraft_kernel::StepNode>,
+63 -1
View File
@@ -4407,9 +4407,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Import { step, .. } => {
let imp = kernel::step_import_shared(step)?;
let digest = crate::config::payload_digest(step);
// Files from board and assembly tools give thousands of bodies the same name: name
// them from a set, not by searching the bodies for every candidate (cubic).
let mut names = BodyNames::new(st);
for (k, b) in imp.bodies.iter().enumerate() {
let base = f.body_names.get(k).filter(|n| !n.trim().is_empty()).unwrap_or(&b.name);
let name = unique_body_name(st, base);
let name = names.unique(base);
let b = crate::lineage::numbered_tool(b.body.clone().with_import_certificate(&digest, f.id)?);
st.bodies.push(ModelBody::traced_new(name, b, f.id));
}
@@ -4442,6 +4445,35 @@ fn mesh_bodies(meshes: &[crate::MeshData]) -> Result<Vec<(String, Body)>> {
Ok(out)
}
/// [`unique_body_name`] for many bodies in a row: the same names, in time linear in the bodies.
struct BodyNames {
taken: std::collections::HashSet<String>,
/// The next suffix worth trying for each base (smaller ones are all taken).
next: std::collections::HashMap<String, usize>,
}
impl BodyNames {
fn new(st: &ModelState) -> Self {
BodyNames { taken: st.bodies.iter().map(|b| b.name.clone()).collect(), next: std::collections::HashMap::new() }
}
fn unique(&mut self, base: &str) -> String {
let base = if base.trim().is_empty() { "Body" } else { base.trim() };
if self.taken.insert(base.to_string()) {
return base.to_string();
}
let mut i = self.next.get(base).copied().unwrap_or(2);
let name = loop {
let candidate = format!("{base} ({i})");
i += 1;
if self.taken.insert(candidate.clone()) {
break candidate;
}
};
self.next.insert(base.to_string(), i);
name
}
}
/// `base`, or `base (2)`, `base (3)`… if a body already has that name.
fn unique_body_name(st: &ModelState, base: &str) -> String {
let base = if base.trim().is_empty() { "Body" } else { base.trim() };
@@ -4780,3 +4812,33 @@ mod chain_tests {
assert_eq!(faces.len(), 3, "{faces:?}");
}
}
#[cfg(test)]
mod body_name_tests {
use super::*;
/// Imported bodies get the names one-at-a-time naming gives them, in time linear in the
/// bodies: a board file with thousands of same-named parts once took minutes (#883).
#[test]
fn many_same_named_bodies_are_named_in_linear_time() {
let mut st = ModelState::default();
let body = kernel::box_solid(Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0)).unwrap();
for name in ["R", "R (3)", "C"] {
st.bodies.push(ModelBody::new(name.into(), body.clone(), 1));
}
// The same names as asking the bodies one at a time.
let mut names = BodyNames::new(&st);
let mut slow = st.clone();
for base in ["R", "R", "C", " ", "Body", "R", "R (2)", "X"] {
let expected = unique_body_name(&slow, base);
assert_eq!(names.unique(base), expected);
slow.bodies.push(ModelBody::new(expected, body.clone(), 2));
}
// 50,000 parts named alike: milliseconds (one-at-a-time naming would take hours).
let began = std::time::Instant::now();
let mut names = BodyNames::new(&ModelState::default());
let last = (0..50_000).map(|_| names.unique("R_0603")).last().unwrap();
assert_eq!(last, "R_0603 (50000)");
assert!(began.elapsed() < std::time::Duration::from_secs(5), "{:?}", began.elapsed());
}
}
+38 -2
View File
@@ -17,11 +17,23 @@ pub struct Progress {
total: AtomicUsize,
/// The feature the evaluation waits at until cancelled ([`Progress::hold_at`]).
hold: AtomicUsize,
/// Long work inside one feature (reading a large STEP file): its step and how far it is.
work_stage: std::sync::Mutex<&'static str>,
work_done: AtomicUsize,
work_total: AtomicUsize,
}
impl Default for Progress {
fn default() -> Self {
Progress { cancelled: AtomicBool::new(false), done: AtomicUsize::new(0), total: AtomicUsize::new(0), hold: AtomicUsize::new(usize::MAX) }
Progress {
cancelled: AtomicBool::new(false),
done: AtomicUsize::new(0),
total: AtomicUsize::new(0),
hold: AtomicUsize::new(usize::MAX),
work_stage: std::sync::Mutex::new(""),
work_done: AtomicUsize::new(0),
work_total: AtomicUsize::new(0),
}
}
}
@@ -42,6 +54,29 @@ impl Progress {
pub fn features(&self) -> (usize, usize) {
(self.done.load(Ordering::Relaxed), self.total.load(Ordering::Relaxed))
}
/// Long work inside a feature, such as reading a large STEP file: (step, done, total);
/// `None` while there is none to show.
pub fn work(&self) -> Option<(&'static str, usize, usize)> {
let total = self.work_total.load(Ordering::Relaxed);
let stage = self.work_stage.lock().map(|s| *s).unwrap_or("");
(total > 0 && !stage.is_empty()).then(|| (stage, self.work_done.load(Ordering::Relaxed).min(total), total))
}
}
/// The kernel's long work (STEP import) reports to the same progress and stops on Cancel.
struct KernelWatch(Arc<Progress>);
impl solidcraft_kernel::watch::Watch for KernelWatch {
fn cancelled(&self) -> bool {
self.0.is_cancelled()
}
fn progress(&self, stage: &'static str, done: usize, total: usize) {
if let Ok(mut s) = self.0.work_stage.lock() {
*s = stage;
}
self.0.work_total.store(total, Ordering::Relaxed);
self.0.work_done.store(done, Ordering::Relaxed);
}
}
struct Current {
@@ -56,6 +91,7 @@ thread_local! {
/// Run `f` with `progress` watching every evaluation it makes on this thread.
pub fn watch<R>(progress: Arc<Progress>, f: impl FnOnce() -> R) -> R {
let kernel = Arc::new(KernelWatch(progress.clone()));
let previous = CURRENT.with(|c| c.replace(Some(Current { progress, depth: 0 })));
struct Restore(Option<Option<Current>>);
impl Drop for Restore {
@@ -66,7 +102,7 @@ pub fn watch<R>(progress: Arc<Progress>, f: impl FnOnce() -> R) -> R {
}
}
let _restore = Restore(Some(previous));
f()
solidcraft_kernel::watch::watching(kernel, f)
}
/// Entered at the start of an evaluation; the guard leaves it.