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}