Open large designs faster: meshes, face names and Measure on all cores (#916)
Display meshes after opening, the face names read when a feature finds a face by name, and inspect.measure over all bodies are now worked out per body on plain scoped threads (solidcraft_kernel::for_each_on_threads / map_on_threads), in body order and with the same results; the browser build stays serial.
This commit is contained in:
1 parent
c333889fc0
commit
6c61442bb2
7 files changed
+110
-6
No files matched your search
@@ -431,6 +431,8 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let st = s.world_state();
|
||||
// As the desktop app prepares a design it opened.
|
||||
st.warm_meshes(|| true);
|
||||
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
|
||||
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
|
||||
Ok(())
|
||||
|
||||
@@ -619,6 +619,17 @@ impl ModelState {
|
||||
pub fn body(&self, name: &str) -> Option<&ModelBody> {
|
||||
self.bodies.iter().find(|b| b.name == name)
|
||||
}
|
||||
|
||||
/// Make every body's display mesh now, on all cores (bodies are meshed independently and
|
||||
/// each keeps its own mesh, so the meshes are those `ModelBody::mesh` makes one by one).
|
||||
/// Stops starting bodies once `go_on` says no.
|
||||
pub fn warm_meshes(&self, go_on: impl Fn() -> bool + Sync) {
|
||||
kernel::for_each_on_threads(&self.bodies, 16, |b| {
|
||||
if go_on() {
|
||||
let _ = b.mesh();
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn sketch(&self, feature: u64) -> Option<&SolvedSketch> {
|
||||
self.sketches.iter().find(|s| s.feature == feature).map(|s| &**s)
|
||||
}
|
||||
|
||||
@@ -260,6 +260,14 @@ impl EdgeHit {
|
||||
/// answering to it (merged into it, or the named piece told apart by the faces around it); else
|
||||
/// the piece of it nearest `p` (`split`).
|
||||
pub fn locate_face(st: &ModelState, name: &str, p: Vec3) -> Option<FaceHit> {
|
||||
// Every body's names are read below: work out the ones not known yet on all cores first
|
||||
// (each body's names are its own, whichever thread works them out).
|
||||
if legacy_name(name).is_none() {
|
||||
let bodies: Vec<&ModelBody> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
solidcraft_kernel::for_each_on_threads(&bodies, 16, |b| {
|
||||
let _ = naming::names(b);
|
||||
});
|
||||
}
|
||||
locate_in(st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh()), name, p)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,12 @@ fn measure(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let want = string_list(p, "bodies");
|
||||
let st = s.world_state();
|
||||
let selected: Vec<_> = st.bodies.iter().filter(|b| want.is_empty() || want.contains(&b.name)).collect();
|
||||
let mut bodies: Vec<Value> = selected.iter().map(|b| measure_json(b)).collect();
|
||||
// Each body is measured on its own: on all cores, in body order.
|
||||
let mut bodies: Vec<Value> = solidcraft_kernel::map_on_threads(&selected, 16, |b| measure_json(b))
|
||||
.into_iter()
|
||||
.zip(&selected)
|
||||
.map(|(m, b)| m.unwrap_or_else(|| json!({"name": b.name, "error": "measuring this body failed"})))
|
||||
.collect();
|
||||
// Mass from each body's material.
|
||||
for (b, body) in bodies.iter_mut().zip(selected) {
|
||||
if let Some(v) = b["volume_mm3"].as_f64() {
|
||||
|
||||
@@ -56,6 +56,7 @@ mod numbered;
|
||||
mod offset;
|
||||
mod ops;
|
||||
mod par;
|
||||
pub use par::{for_each_on_threads, map_on_threads};
|
||||
#[cfg(test)]
|
||||
mod pinch_tests;
|
||||
mod plane_fit;
|
||||
|
||||
@@ -22,11 +22,90 @@ pub(crate) fn map<T: Sync, R: Send>(what: &'static str, items: &[T], parallel: b
|
||||
items.iter().map(one).collect()
|
||||
}
|
||||
|
||||
/// Run `f` on every item, spread over plain threads (one per core, at most `max_threads`),
|
||||
/// each pulling the next item; on the calling thread in the browser build or when threads
|
||||
/// cannot be started. Each call runs under [`crate::guard`] (a panic is that item's loss).
|
||||
///
|
||||
/// Plain threads rather than the worker pool: an item may wait on a lazily filled cell that
|
||||
/// another item is filling, and a pool worker that picked up a second item while it waited
|
||||
/// inside its own item's parallel work could wait on itself.
|
||||
pub fn for_each_on_threads<T: Sync>(items: &[T], max_threads: usize, f: impl Fn(&T) + Sync) {
|
||||
let _ = map_on_threads(items, max_threads, f);
|
||||
}
|
||||
|
||||
/// `f` over `items` as [`for_each_on_threads`] runs it, results in item order (`None` where an
|
||||
/// item panicked).
|
||||
pub fn map_on_threads<T: Sync, R: Send>(items: &[T], max_threads: usize, f: impl Fn(&T) -> R + Sync) -> Vec<Option<R>> {
|
||||
let one = |item: &T| guard("parallel item", || Ok(f(item))).ok();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
let threads = std::thread::available_parallelism().map(usize::from).unwrap_or(1).min(max_threads).min(items.len());
|
||||
if threads > 1 {
|
||||
let next = AtomicUsize::new(0);
|
||||
let work = || {
|
||||
let mut done = Vec::new();
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(item) = items.get(i) else { break };
|
||||
done.push((i, one(item)));
|
||||
}
|
||||
done
|
||||
};
|
||||
let mut out: Vec<Option<R>> = std::iter::repeat_with(|| None).take(items.len()).collect();
|
||||
std::thread::scope(|scope| {
|
||||
// A thread that cannot start leaves its share to the others.
|
||||
let helpers: Vec<_> =
|
||||
(1..threads).filter_map(|_| std::thread::Builder::new().name("solidcraft-each".into()).spawn_scoped(scope, work).ok()).collect();
|
||||
let mut all = work();
|
||||
for h in helpers {
|
||||
// `work` never panics (every item runs under the guard).
|
||||
if let Ok(done) = h.join() {
|
||||
all.extend(done);
|
||||
}
|
||||
}
|
||||
for (i, r) in all {
|
||||
if let Some(slot) = out.get_mut(i) {
|
||||
*slot = r;
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
}
|
||||
let _ = max_threads;
|
||||
items.iter().map(one).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::KernelError;
|
||||
|
||||
#[test]
|
||||
fn each_item_once_on_threads() {
|
||||
let items: Vec<u32> = (0..1000).collect();
|
||||
let seen: Vec<std::sync::atomic::AtomicU32> = (0..1000).map(|_| std::sync::atomic::AtomicU32::new(0)).collect();
|
||||
for_each_on_threads(&items, 8, |x| {
|
||||
seen[*x as usize].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
assert!(*x != 500, "a panicking item is that item's loss");
|
||||
});
|
||||
assert!(seen.iter().all(|s| s.load(std::sync::atomic::Ordering::Relaxed) == 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_on_threads_keeps_order() {
|
||||
let items: Vec<u32> = (0..1000).collect();
|
||||
let out = map_on_threads(&items, 8, |x| {
|
||||
assert!(*x != 7, "a panicking item is that item's loss");
|
||||
x * 3
|
||||
});
|
||||
assert_eq!(out.len(), 1000);
|
||||
for (i, r) in out.iter().enumerate() {
|
||||
assert_eq!(*r, (i != 7).then_some(3 * i as u32));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_order_and_errors() {
|
||||
let items: Vec<u32> = (0..1000).collect();
|
||||
|
||||
@@ -370,11 +370,9 @@ fn compute(mut session: Session, command: &str, params: &Value, thumbnail: bool,
|
||||
let placement_ms = view_began.elapsed().as_secs_f64() * 1000.0;
|
||||
let mesh_began = std::time::Instant::now();
|
||||
// Fit, picking and the first viewport frame must not trigger cold tessellation on the UI.
|
||||
for b in &world.bodies {
|
||||
if progress.is_cancelled() {
|
||||
return cancelled();
|
||||
}
|
||||
let _ = b.mesh();
|
||||
world.warm_meshes(|| !progress.is_cancelled());
|
||||
if progress.is_cancelled() {
|
||||
return cancelled();
|
||||
}
|
||||
let tessellation_ms = mesh_began.elapsed().as_secs_f64() * 1000.0;
|
||||
phase.store(2, Ordering::Relaxed);
|
||||
|
||||
Reference in new issue
Block a user