Per-command timings for script runs and batches (#1868)
Thanks @DuBearte.
This commit is contained in:
1 parent
d1b96100d3
commit
b04e0801d6
17 files changed
+526
-37
No files matched your search
@@ -1038,6 +1038,10 @@ impl Body {
|
||||
/// [`Body::tessellate_with`] in which no facet turns more than `angle` radians either
|
||||
/// (infinite: the chord tolerance alone).
|
||||
fn tessellate_within(&self, tol: f64, angle: f64, refine: bool) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.tessellate_within_untimed(tol, angle, refine))
|
||||
}
|
||||
|
||||
fn tessellate_within_untimed(&self, tol: f64, angle: f64, refine: bool) -> Result<Mesh> {
|
||||
let tol = if tol.is_finite() && tol > 0.0 { tol } else { 0.05 };
|
||||
let tol = tol.max(self.size() * 1e-6);
|
||||
if let Some(m) = &self.mesh {
|
||||
@@ -1148,6 +1152,10 @@ impl Body {
|
||||
}
|
||||
|
||||
fn plan_within(&self, tol: f64, angle: Option<f64>) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.plan_within_untimed(tol, angle))
|
||||
}
|
||||
|
||||
fn plan_within_untimed(&self, tol: f64, angle: Option<f64>) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
if self.is_mesh() {
|
||||
return Err(KernelError::Invalid("a mesh body has no surfaces to tessellate".into()));
|
||||
}
|
||||
@@ -1266,6 +1274,10 @@ impl Body {
|
||||
/// body's [`Body::display_plan`] (shared edge nodes, structured grids), or the way
|
||||
/// [`Body::tessellate`] meshes when no plan can be made.
|
||||
pub fn display_mesh(&self, tol: f64) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_untimed(tol))
|
||||
}
|
||||
|
||||
fn display_mesh_untimed(&self, tol: f64) -> Result<Mesh> {
|
||||
if self.is_mesh() {
|
||||
return self.flagged(self.tessellate(tol)?);
|
||||
}
|
||||
@@ -1285,6 +1297,10 @@ impl Body {
|
||||
/// sagitta of a small radius on a large body (a sheet-metal bend, a small hole), which the
|
||||
/// tolerance alone leaves as one flat facet.
|
||||
pub fn display_mesh_angular(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_angular_untimed(tol, angle))
|
||||
}
|
||||
|
||||
fn display_mesh_angular_untimed(&self, tol: f64, angle: f64) -> Result<Mesh> {
|
||||
if self.is_mesh() {
|
||||
return self.flagged(self.tessellate(tol)?);
|
||||
}
|
||||
@@ -1300,6 +1316,10 @@ impl Body {
|
||||
/// The display mesh of a plan made from this body ([`Body::display_plan`],
|
||||
/// [`Body::display_plan_angular`]): evaluated on the CPU, with its edges flagged.
|
||||
pub fn display_mesh_of(&self, plan: &solidcraft_geom::tess::TessPlan) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.display_mesh_of_untimed(plan))
|
||||
}
|
||||
|
||||
fn display_mesh_of_untimed(&self, plan: &solidcraft_geom::tess::TessPlan) -> Result<Mesh> {
|
||||
self.flagged(plan.evaluate())
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ mod prism;
|
||||
mod prism_curve;
|
||||
mod prism_local;
|
||||
mod prism_measure;
|
||||
pub mod profile;
|
||||
pub mod provenance;
|
||||
mod record;
|
||||
mod seam_faces;
|
||||
|
||||
@@ -300,6 +300,10 @@ thread_local! {
|
||||
/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`,
|
||||
/// whichever way it was made, or none where a step it took does not report one.
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || boolean_untimed(a, b, op))
|
||||
}
|
||||
|
||||
fn boolean_untimed(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
// Earlier coarse bores can leave a junction a later pattern copy cannot intersect.
|
||||
// Resolve conical-cut junctions from the first copy, while the operands are unchanged.
|
||||
if op == BoolOp::Cut && !APART.with(|c| c.get()) && conical_cut_junction(a, b) {
|
||||
@@ -1273,6 +1277,10 @@ pub(crate) fn by_lumps(a: &Body, b: &Body, la: Vec<Body>, lb: Vec<Body>, op: Boo
|
||||
/// intersection work (many identical pattern tools, #261). `None` when any two may meet, or a
|
||||
/// body is a mesh: the caller unions those the usual way.
|
||||
pub fn union_apart(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || union_apart_untimed(bodies))
|
||||
}
|
||||
|
||||
fn union_apart_untimed(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
if bodies.len() < 2 || bodies.len() > 10_000 || bodies.iter().any(Body::is_mesh) {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1311,6 +1319,10 @@ pub fn union_apart(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
/// shows they are apart (profiles apart in their sketch, extruded alike). `None` for fewer than
|
||||
/// two bodies or a mesh.
|
||||
pub fn join_apart(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || join_apart_untimed(bodies))
|
||||
}
|
||||
|
||||
fn join_apart_untimed(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
if bodies.len() < 2 || bodies.len() > 10_000 || bodies.iter().any(Body::is_mesh) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Where a command's time goes inside the kernel (#1833): wall time in booleans and in
|
||||
//! tessellation, measured on this thread while [`capture`] runs and not at all otherwise (one
|
||||
//! thread-local check per timed call).
|
||||
//!
|
||||
//! Only the outermost timed call counts: a tessellation a boolean does to check its result is
|
||||
//! the boolean's time, so the two never add up to more than the command took. Work a call hands
|
||||
//! to other threads is inside that call's wall time.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
/// Kernel time spent while a [`capture`] ran, in milliseconds.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct KernelTimes {
|
||||
pub boolean_ms: f64,
|
||||
pub tessellate_ms: f64,
|
||||
}
|
||||
|
||||
/// What a timed call is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Kind {
|
||||
Boolean,
|
||||
Tessellate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct State {
|
||||
/// A capture is running on this thread.
|
||||
on: bool,
|
||||
/// A timed call is running (nested ones aren't timed).
|
||||
busy: bool,
|
||||
times: KernelTimes,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static STATE: Cell<State> = const { Cell::new(State { on: false, busy: false, times: KernelTimes { boolean_ms: 0.0, tessellate_ms: 0.0 } }) };
|
||||
}
|
||||
|
||||
/// Run `f`, and the kernel time it spent in booleans and tessellation on this thread. Captures
|
||||
/// nest: an inner capture's time is the outer one's too.
|
||||
pub fn capture<T>(f: impl FnOnce() -> T) -> (T, KernelTimes) {
|
||||
struct Restore(State);
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
let inner = STATE.with(Cell::get).times;
|
||||
let mut outer = self.0;
|
||||
if outer.on {
|
||||
outer.times.boolean_ms += inner.boolean_ms;
|
||||
outer.times.tessellate_ms += inner.tessellate_ms;
|
||||
}
|
||||
STATE.with(|s| s.set(outer));
|
||||
}
|
||||
}
|
||||
let outer = STATE.with(Cell::get);
|
||||
STATE.with(|s| s.set(State { on: true, busy: outer.busy, times: KernelTimes::default() }));
|
||||
let restore = Restore(outer);
|
||||
let r = f();
|
||||
let times = STATE.with(Cell::get).times;
|
||||
drop(restore);
|
||||
(r, times)
|
||||
}
|
||||
|
||||
/// Time `f` as `kind` when a capture is running and no timed call is.
|
||||
pub(crate) fn timed<T>(kind: Kind, f: impl FnOnce() -> T) -> T {
|
||||
let st = STATE.with(Cell::get);
|
||||
if !st.on || st.busy {
|
||||
return f();
|
||||
}
|
||||
struct Busy {
|
||||
kind: Kind,
|
||||
started: web_time::Instant,
|
||||
}
|
||||
impl Drop for Busy {
|
||||
fn drop(&mut self) {
|
||||
let ms = self.started.elapsed().as_secs_f64() * 1000.0;
|
||||
STATE.with(|s| {
|
||||
let mut st = s.get();
|
||||
st.busy = false;
|
||||
match self.kind {
|
||||
Kind::Boolean => st.times.boolean_ms += ms,
|
||||
Kind::Tessellate => st.times.tessellate_ms += ms,
|
||||
}
|
||||
s.set(st);
|
||||
});
|
||||
}
|
||||
}
|
||||
STATE.with(|s| s.set(State { busy: true, ..st }));
|
||||
let busy = Busy { kind, started: web_time::Instant::now() };
|
||||
let r = f();
|
||||
drop(busy);
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn spin(ms: u64) {
|
||||
let t = web_time::Instant::now();
|
||||
while t.elapsed().as_millis() < u128::from(ms) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_is_timed_outside_a_capture() {
|
||||
timed(Kind::Boolean, || spin(2));
|
||||
let ((), t) = capture(|| ());
|
||||
assert_eq!(t, KernelTimes::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_outermost_call_counts() {
|
||||
let started = web_time::Instant::now();
|
||||
let ((), t) = capture(|| {
|
||||
timed(Kind::Boolean, || {
|
||||
spin(3);
|
||||
timed(Kind::Tessellate, || spin(3));
|
||||
});
|
||||
timed(Kind::Tessellate, || spin(2));
|
||||
});
|
||||
let elapsed = started.elapsed().as_secs_f64() * 1000.0;
|
||||
assert!(t.boolean_ms >= 6.0 && t.tessellate_ms >= 2.0, "{t:?}");
|
||||
// The tessellation inside the boolean is not counted twice.
|
||||
assert!(t.boolean_ms + t.tessellate_ms <= elapsed + 1e-6, "{t:?} in {elapsed} ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_captures_add_to_the_outer_one_and_a_panic_restores_it() {
|
||||
let ((), outer) = capture(|| {
|
||||
let ((), inner) = capture(|| timed(Kind::Boolean, || spin(2)));
|
||||
assert!(inner.boolean_ms >= 2.0);
|
||||
let _ = std::panic::catch_unwind(|| capture(|| timed(Kind::Tessellate, || panic!("kernel"))));
|
||||
});
|
||||
assert!(outer.boolean_ms >= 2.0, "{outer:?}");
|
||||
let st = STATE.with(Cell::get);
|
||||
assert!(!st.on && !st.busy);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user