renacer_core/phase_timer.rs
1//! PMAT-284: Lightweight sub-phase timing for CPU-GPU pipelining analysis
2//!
3//! `PhaseTimer` captures named sub-phases within a hot loop with minimal
4//! overhead when disabled. Designed for iteration-scheduler-level instrumentation
5//! where BrickProfiler (GPU-side) and InferenceTracer (model-level) don't apply.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! let mut timer = PhaseTimer::from_env("PMAT_283_TIMING");
11//!
12//! loop {
13//! timer.start();
14//! timer.mark("lock"); // write lock acquired
15//! // ... scheduling ...
16//! timer.mark("sched"); // scheduling done
17//! // ... decode step ...
18//! timer.mark("decode"); // GPU decode done
19//! // ... token distribution ...
20//! timer.mark("dist"); // tokens distributed
21//! timer.emit(iteration, batch_size); // prints if enabled + interval
22//! }
23//! ```
24//!
25//! Output: `[PMAT-283] iter=42, m=4: lock=3µs sched=12µs decode=7412µs dist=84µs total=7511µs`
26
27use std::time::Instant;
28
29/// Lightweight sub-phase timer for hot-path instrumentation.
30///
31/// Zero overhead when disabled (all methods are no-ops).
32/// When enabled, captures phase boundaries via `mark()` and
33/// emits structured timing via `emit()`.
34pub struct PhaseTimer {
35 enabled: bool,
36 start: Option<Instant>,
37 marks: Vec<(&'static str, Instant)>,
38 label: &'static str,
39 /// Emit every N iterations (0 = first 5 then every 100)
40 emit_interval: u64,
41}
42
43impl PhaseTimer {
44 /// Create a timer controlled by an environment variable.
45 ///
46 /// Returns a disabled (zero-cost) timer if the env var is unset or != "1".
47 pub fn from_env(env_var: &'static str, label: &'static str) -> Self {
48 let enabled = std::env::var(env_var).map(|v| v == "1").unwrap_or(false);
49 Self {
50 enabled,
51 start: None,
52 marks: Vec::with_capacity(if enabled { 8 } else { 0 }),
53 label,
54 emit_interval: 0,
55 }
56 }
57
58 /// Create an always-enabled timer (for testing).
59 #[cfg(test)]
60 pub fn enabled(label: &'static str) -> Self {
61 Self {
62 enabled: true,
63 start: None,
64 marks: Vec::with_capacity(8),
65 label,
66 emit_interval: 0,
67 }
68 }
69
70 /// Start a new timing cycle. Call at the top of each iteration.
71 #[inline]
72 pub fn start(&mut self) {
73 if self.enabled {
74 self.marks.clear();
75 self.start = Some(Instant::now());
76 }
77 }
78
79 /// Mark the end of a named phase. The duration is from the previous
80 /// mark (or start) to now.
81 #[inline]
82 pub fn mark(&mut self, phase: &'static str) {
83 if self.enabled {
84 self.marks.push((phase, Instant::now()));
85 }
86 }
87
88 /// Emit timing data if enabled and the iteration matches the emit interval.
89 ///
90 /// Default interval: first 5 iterations, then every 100.
91 pub fn emit(&self, iteration: u64, batch_size: usize) {
92 if !self.enabled {
93 return;
94 }
95 // Default: first 5, then every 100
96 let should_emit = if self.emit_interval == 0 {
97 iteration <= 5 || iteration % 100 == 0
98 } else {
99 iteration % self.emit_interval == 0
100 };
101 if !should_emit {
102 return;
103 }
104
105 let Some(start) = self.start else { return };
106 let mut parts = Vec::with_capacity(self.marks.len());
107 let mut prev = start;
108 for (name, ts) in &self.marks {
109 let dur_us = ts.duration_since(prev).as_micros();
110 parts.push(format!("{name}={dur_us}µs"));
111 prev = *ts;
112 }
113 let total_us = prev.duration_since(start).as_micros();
114 eprintln!(
115 "[{}] iter={}, m={}: {} total={}µs",
116 self.label,
117 iteration,
118 batch_size,
119 parts.join(" "),
120 total_us,
121 );
122 }
123
124 /// Check if the timer is enabled.
125 #[inline]
126 #[must_use]
127 pub fn is_enabled(&self) -> bool {
128 self.enabled
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_disabled_is_noop() {
138 let mut t = PhaseTimer::from_env("NONEXISTENT_ENV_VAR_12345", "test");
139 assert!(!t.is_enabled());
140 t.start();
141 t.mark("a");
142 t.mark("b");
143 t.emit(1, 4); // should not panic or print
144 }
145
146 #[test]
147 fn test_enabled_captures_phases() {
148 let mut t = PhaseTimer::enabled("test");
149 t.start();
150 std::thread::sleep(std::time::Duration::from_micros(100));
151 t.mark("phase_a");
152 std::thread::sleep(std::time::Duration::from_micros(100));
153 t.mark("phase_b");
154 assert_eq!(t.marks.len(), 2);
155 assert_eq!(t.marks[0].0, "phase_a");
156 assert_eq!(t.marks[1].0, "phase_b");
157 }
158}