Skip to main content

streaming_crypto/core_api/telemetry/
timers.rs

1// ## src/telemetry/timers.rs
2
3//! telemetry/timers.rs
4//! Stage timers for streaming pipelines.
5//!
6//! Summary: Records durations for read, compress, encrypt/decrypt, and write stages.
7//! Industry notes: TLS/QUIC libraries track per-record timings for performance analysis.
8
9// ### `Stage` enum with `Display`
10
11use std::fmt;
12use std::time::Instant;
13use std::time::Duration;
14use std::collections::{HashMap, hash_map};
15use serde::{Serialize, Deserialize};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Stage {
19    Read,
20    Write,
21    Encode,
22    Decode,
23    Compress,
24    Decompress,
25    Encrypt,
26    Decrypt,
27    Validate,
28    Digest,
29}
30
31impl fmt::Display for Stage {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        let name = match self {
34            Stage::Read       => "read",
35            Stage::Write      => "write",
36            Stage::Encode     => "encode",
37            Stage::Decode     => "decode",
38            Stage::Compress   => "compress",
39            Stage::Decompress => "decompress",
40            Stage::Encrypt    => "encrypt",
41            Stage::Decrypt    => "decrypt",
42            Stage::Validate   => "validate",
43            Stage::Digest     => "digest",
44        };
45        f.write_str(name)
46    }
47}
48
49// ### Benefits
50// - **Type safety**: we call `Stage::Encrypt` instead of `"encrypt"`.  
51// - **Human‑readable output**: `format!("{}", Stage::Encrypt)` → `"encrypt"`.  
52// - **FFI/logging friendly**: we can pass stage names to telemetry snapshots or JSON without stringly‑typed code.  
53// - **Extensible**: add new stages by extending the enum and updating the `Display` match.
54
55// - **Microsecond precision**: expose both microseconds and milliseconds helpers, but keep `Duration` internally (nanosecond precision).  
56// - **Type‑safe iteration**: implement `IntoIterator` so we can loop over `(Stage, Duration)` pairs directly.  
57// - **Cleaner API**: replace `get_ms` with `get_ms` and `get_us`, and add a generic `get_in_unit` if we want flexibility.  
58// - **Extensibility**: we can add new stages without changing the struct internals.
59
60// ### ⚠️ Note on `Duration`
61// `std::time::Duration` already implements `Serialize` and `Deserialize` (via the `serde` crate), so we don’t need to do anything extra there.
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
64pub struct StageTimes {
65    pub times: HashMap<Stage, Duration>,
66}
67// ### 🚀 Summary
68// - Add `#[derive(Serialize, Deserialize)]` to `Stage`.
69// - Keep `StageTimes` deriving `Serialize, Deserialize`.
70// - Now `TelemetrySnapshot` can derive `Serialize, Deserialize` without errors.
71
72impl StageTimes {
73    /// Add duration to a stage (accumulates if already present).
74    // pub fn add(&mut self, stage: Stage, dur: Duration) {
75    //     *self.times.entry(stage).or_insert(Duration::ZERO) += dur;
76    // }
77    pub fn add(&mut self, stage: Stage, dur: Duration) {
78        self.times.insert(stage, dur);
79    }
80
81
82    /// Get total duration for a stage.
83    pub fn get(&self, stage: Stage) -> Duration {
84        self.times.get(&stage).copied().unwrap_or(Duration::ZERO)
85    }
86
87    /// Get duration in milliseconds (f64).
88    pub fn get_ms(&self, stage: Stage) -> f64 {
89        self.get(stage).as_secs_f64() * 1_000.0
90    }
91
92    /// Get duration in microseconds (f64).
93    pub fn get_us(&self, stage: Stage) -> f64 {
94        self.get(stage).as_secs_f64() * 1_000_000.0
95    }
96
97    /// Get duration in nanoseconds (u128).
98    pub fn get_ns(&self, stage: Stage) -> u128 {
99        self.get(stage).as_nanos()
100    }
101
102    /// Sum all stage durations.
103    pub fn total(&self) -> Duration {
104        self.times.values().copied().sum()
105    }
106
107    /// Return all stage times.
108    pub fn all(&self) -> &HashMap<Stage, Duration> {
109        &self.times
110    }
111
112    /// Check if all expected stages are present (non-zero).
113    pub fn has_all(&self, expected: &[Stage]) -> bool {
114        expected.iter().all(|s| self.get(*s) > Duration::ZERO)
115    }
116
117    // ### Example usage
118
119    // ```rust
120    // let mut timer = TelemetryTimer::new();
121    // timer.add_stage_time(Stage::Encrypt, Duration::from_micros(420));
122    // timer.add_stage_time(Stage::Read, Duration::from_micros(100));
123
124    // println!("Total stage time: {} µs", timer.stage_times.total().as_micros());
125
126    // let expected = [Stage::Read, Stage::Encrypt];
127    // assert!(timer.stage_times.has_all(&expected));
128    // ```
129
130    // Output:
131
132    // ```
133    // Total stage time: 520 µs
134    // ```
135
136    // ### ✅ Benefits
137    // - **Telemetry sanity**: compare `StageTimes::total()` against `TelemetrySnapshot.elapsed` to ensure no stage time exceeds total elapsed.  
138    // - **Coverage check**: `has_all()` lets us assert that all critical stages were measured.  
139    // - **Precision**: we can report totals in microseconds or nanoseconds without losing detail.
140
141    pub fn iter(&self) -> impl Iterator<Item = (&Stage, &Duration)> {
142        self.times.iter()
143    }
144
145    /// Merge another StageTimes into this one
146    pub fn merge(&mut self, other: &StageTimes) {
147        for (stage, dur) in &other.times {
148            *self.times.entry(*stage).or_insert(Duration::ZERO) += *dur;
149        }
150    }
151
152    pub fn summary(&self) -> String {
153        let mut out = String::new();
154        out.push_str("=== Stage Times Summary ===\n");
155        for (stage, dur) in &self.times {
156            out.push_str(&format!("{:?}: {:?}\n", stage, dur));
157        }
158        out
159    }
160
161    // println!("{}", st.summary());
162    // or, with Display:
163    // println!("{}", st);
164
165}
166
167// Optional: implement Display so we can just `println!("{}", stage_times)`
168impl fmt::Display for StageTimes {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        writeln!(f, "=== Stage Times Summary ===")?;
171        for (stage, dur) in &self.times {
172            writeln!(f, "{:?}: {:?}", stage, dur)?;
173        }
174        Ok(())
175    }
176}
177
178/// Allow iteration over owned StageTimes.
179impl IntoIterator for StageTimes {
180    type Item = (Stage, Duration);
181    type IntoIter = hash_map::IntoIter<Stage, Duration>;
182
183    fn into_iter(self) -> Self::IntoIter {
184        self.times.into_iter()
185    }
186}
187
188/// Allow iteration over borrowed StageTimes.
189impl<'a> IntoIterator for &'a StageTimes {
190    type Item = (&'a Stage, &'a Duration);
191    type IntoIter = hash_map::Iter<'a, Stage, Duration>;
192
193    fn into_iter(self) -> Self::IntoIter {
194        self.times.iter()
195    }
196}
197
198// ### ✅ Benefits
199// - **Precision**: we can now report in microseconds or nanoseconds, not just milliseconds.  
200// - **Flexibility**: `get_ms`, `get_us`, and `get_ns` cover most telemetry needs.  
201// - **Iteration**: we can loop over all stages easily in tests or telemetry snapshots.  
202// - **Extensibility**: adding new stages only requires updating the `Stage` enum, not this struct.
203
204// Example usage:
205
206// ```rust
207// timer.add_stage_time(Stage::Encrypt, t.elapsed());
208// println!("Encrypt stage took {:.2} µs", timer.stage_times.get_us(Stage::Encrypt));
209
210// for (stage, dur) in &timer.stage_times {
211//     println!("Stage {} took {} ns", stage, dur.as_nanos());
212// }
213// ```
214
215// ### ✅ Benefits
216// - Clean iteration without exposing `HashMap` internals.
217// - Works for both owned (`StageTimes`) and borrowed (`&StageTimes`) contexts.
218// - Plays nicely with our `Display` impl for `Stage`.
219
220
221// ### ✅ TelemetryTimer with Enum
222
223// Now `TelemetryTimer` can use the enum directly:
224
225#[derive(Clone, Debug)]
226pub struct TelemetryTimer {
227    pub start_time: Instant,
228    pub end_time: Option<Instant>,
229    pub stage_times: StageTimes,
230}
231
232impl TelemetryTimer {
233    pub fn new() -> Self {
234        Self {
235            start_time: Instant::now(),
236            end_time: None,
237            stage_times: StageTimes::default(),
238        }
239    }
240
241    pub fn finish(&mut self) {
242        self.end_time = Some(Instant::now());
243    }
244
245    pub fn add_stage_time(&mut self, stage: Stage, dur: Duration) {
246        self.stage_times.add(stage, dur);
247    }
248
249    pub fn elapsed(&self) -> Duration {
250        match self.end_time {
251            Some(end) => end.duration_since(self.start_time),
252            None => Instant::now().duration_since(self.start_time),
253        }
254    }
255
256    /// Merge another StageTimes into the global timer
257    pub fn merge(&mut self, other: &StageTimes) {
258        self.stage_times.merge(other);
259    }
260
261}
262
263// Usage becomes type‑safe:
264
265// ```rust
266// timer.add_stage_time(Stage::Encrypt, res.encrypt);
267// timer.add_stage_time(Stage::Read, t.elapsed());
268// ```
269
270// ### 🚀 Improvements over current design
271// - **Type safety**: no silent typos in stage names.  
272// - **Extensibility**: add new stages by extending the `Stage` enum, no need to touch `StageTimes` internals.  
273// - **Cleaner API**: `get_ms(stage)` instead of separate `read_ms()`, `compress_ms()`, etc.  
274// - **Telemetry snapshot sanity**: we can iterate over `stage_times.all()` to check that all expected stages are present.