Large STEP imports: no more multi-minute hang, shared STEP text, cancel and progress (#925)
This commit is contained in:
1 parent
dfbf7dd6a8
commit
2f91338d3d
18 files changed
+525
-45
No files matched your search
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user