Skip to main content

manifold_rust/
timing.rs

1// Lightweight, env-gated phase timing for the boolean pipeline, mirroring the
2// C++ MANIFOLD_TIMING instrumentation (Timer::Print in boolean3.cpp /
3// boolean_result.cpp). Enabled by setting the MANIFOLD_TIMING environment
4// variable to any non-empty value; otherwise every call is a no-op so release
5// performance is unaffected. Used to compare per-stage wall-clock against the
6// C++ reference when hunting performance gaps (see CLAUDE.md "Instrumentation
7// Strategy").
8
9use std::sync::OnceLock;
10use std::time::Instant;
11
12fn enabled() -> bool {
13    static ENABLED: OnceLock<bool> = OnceLock::new();
14    *ENABLED.get_or_init(|| {
15        std::env::var("MANIFOLD_TIMING").map_or(false, |v| !v.is_empty())
16    })
17}
18
19/// Start a stage timer. Returns None (and times nothing) unless the
20/// MANIFOLD_TIMING environment variable is set.
21pub(crate) fn start() -> Option<Instant> {
22    if enabled() {
23        Some(Instant::now())
24    } else {
25        None
26    }
27}
28
29/// Platform-safe stopwatch for ALWAYS-ON aggregate instrumentation (hot-path
30/// counters that accumulate into atomics). `std::time::Instant::now()`
31/// PANICS on wasm32-unknown-unknown ("time not implemented on this
32/// platform"), so any unconditional timing in library code must go through
33/// this type — on wasm it measures nothing and reports zero.
34#[derive(Clone, Copy)]
35pub(crate) struct Stopwatch {
36    #[cfg(not(target_arch = "wasm32"))]
37    t0: Instant,
38}
39
40impl Stopwatch {
41    #[inline]
42    pub fn start() -> Self {
43        Stopwatch {
44            #[cfg(not(target_arch = "wasm32"))]
45            t0: Instant::now(),
46        }
47    }
48
49    #[inline]
50    pub fn elapsed_ns(self) -> u64 {
51        #[cfg(not(target_arch = "wasm32"))]
52        {
53            self.t0.elapsed().as_nanos() as u64
54        }
55        #[cfg(target_arch = "wasm32")]
56        {
57            0
58        }
59    }
60
61    #[inline]
62    pub fn elapsed_secs(self) -> f64 {
63        #[cfg(not(target_arch = "wasm32"))]
64        {
65            self.t0.elapsed().as_secs_f64()
66        }
67        #[cfg(target_arch = "wasm32")]
68        {
69            0.0
70        }
71    }
72}
73
74/// Optional memory reporter, registered by profiling harnesses (e.g. the
75/// mem_profile example's counting allocator). Returns (current bytes, peak
76/// bytes since the previous call) — the implementation resets its peak
77/// watermark on read so each stage line reports that stage's own peak.
78pub type MemHook = fn() -> (usize, usize);
79
80static MEM_HOOK: OnceLock<MemHook> = OnceLock::new();
81
82pub fn set_mem_hook(hook: MemHook) {
83    let _ = MEM_HOOK.set(hook);
84}
85
86/// Print a counter/diagnostic line, gated on the same MANIFOLD_TIMING
87/// switch as the stage timers.
88pub(crate) fn print_count(label: &str) {
89    if enabled() {
90        eprintln!("{label}");
91    }
92}
93
94/// Print the elapsed time for a stage started with `start`, matching the C++
95/// Timer::Print format ("label: N sec") on stderr. If a memory hook is
96/// registered, appends current/stage-peak heap use.
97pub(crate) fn print(label: &str, t0: Option<Instant>) {
98    if let Some(t0) = t0 {
99        match MEM_HOOK.get() {
100            Some(hook) => {
101                let (current, peak) = hook();
102                eprintln!(
103                    "{}: {} sec, current = {:.1} MB, stage peak = {:.1} MB",
104                    label,
105                    t0.elapsed().as_secs_f64(),
106                    current as f64 / 1048576.0,
107                    peak as f64 / 1048576.0
108                );
109            }
110            None => eprintln!("{}: {} sec", label, t0.elapsed().as_secs_f64()),
111        }
112    }
113}