rem_interface/
metrics.rs

1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::sync::Mutex;
4use std::time::Instant;
5
6use crate::vscode::{Envelope, Timing};
7
8/// Global recorder
9static REC: Lazy<Mutex<Recorder>> = Lazy::new(|| Mutex::new(Recorder::default()));
10
11#[derive(Default)]
12struct Recorder {
13    /// Ordered list of (id, name, time).
14    markers: Vec<Marker>,
15    /// First-seen id per marker name (stable).
16    name_to_id: HashMap<String, u32>,
17    /// Next id to assign.
18    next_id: u32,
19
20    /// Ad-hoc spans recorded explicitly (label -> nanos).
21    spans: Vec<(String, u128)>,
22}
23
24#[derive(Clone)]
25struct Marker {
26    #[allow(unused)]
27    id: u32,
28    name: String,
29    t: Instant,
30}
31
32impl Recorder {
33    fn assign_id(&mut self, name: &str) -> u32 {
34        if let Some(&id) = self.name_to_id.get(name) {
35            return id;
36        }
37        let id = self.next_id;
38        self.next_id += 1;
39        self.name_to_id.insert(name.to_string(), id);
40        id
41    }
42
43    fn find_marker_index_by_name(&self, name: &str) -> Option<usize> {
44        self.markers.iter().position(|m| m.name == name)
45    }
46}
47
48/* ============================ Public API ============================ */
49
50/// Record a timing marker with a human-readable name.
51/// The first call is considered the "start" marker for cumulative timings.
52pub fn mark(name: impl Into<String>) {
53    let name = name.into();
54    let mut r = REC.lock().unwrap();
55    let id = r.assign_id(&name);
56    r.markers.push(Marker { id, name, t: Instant::now() });
57}
58
59/// Start an ad-hoc span; recorded when the guard is dropped.
60pub fn span(label: impl Into<String>) -> SpanGuard {
61    SpanGuard {
62        label: label.into(),
63        start: Instant::now(),
64        committed: false,
65    }
66}
67
68/// Compute (and optionally record) a span between two named markers.
69/// Returns `Some(seconds)` if both markers exist and end is after start.
70pub fn span_between(start_name: &str, end_name: &str, record_phase: bool) -> Option<u128> {
71    let mut r = REC.lock().unwrap();
72    let si = r.find_marker_index_by_name(start_name)?;
73    let ei = r.find_marker_index_by_name(end_name)?;
74    if ei <= si {
75        return None;
76    }
77    let s = r.markers[si].t;
78    let e = r.markers[ei].t;
79    let nanos = (e - s).as_nanos();
80    if record_phase {
81        r.spans.push((format!("span:{start_name}->{end_name}"), nanos));
82    }
83    Some(nanos)
84}
85
86/// List marker names in order of recording
87pub fn list_markers() -> Vec<String> {
88    let r = REC.lock().unwrap();
89    r.markers.iter().map(|m| m.name.clone()).collect()
90}
91
92/// Record a custom-named span between two *named* markers (first occurrence of each)
93/// Returns seconds if both markers exist and order is valid.
94pub fn span_between_markers(
95    start_name: &str,
96    end_name: &str,
97    label: impl Into<String>,
98) -> Option<u128> {
99    let mut r = REC.lock().unwrap();
100    let si = r.find_marker_index_by_name(start_name)?;
101    let ei = r.find_marker_index_by_name(end_name)?;
102    if ei <= si { return None; }
103    let nanos = (r.markers[ei].t - r.markers[si].t).as_nanos();
104    r.spans.push((label.into(), nanos));
105    Some(nanos)
106}
107
108/// Record a custom-named span between two markers by *index* in the recorded order.
109/// Example: 0-based indices; span_between_indices(3, 5, "apply_total")
110pub fn span_between_indices(
111    start_idx: usize,
112    end_idx: usize,
113    label: impl Into<String>,
114) -> Option<u128> {
115    let mut r = REC.lock().unwrap();
116    if start_idx >= r.markers.len() || end_idx >= r.markers.len() || end_idx <= start_idx {
117        return None;
118    }
119    let nanos = (r.markers[end_idx].t - r.markers[start_idx].t).as_nanos();
120    r.spans.push((label.into(), nanos));
121    Some(nanos)
122}
123
124/// Drain the current recorder as a list of `Timing` entries.
125/// Includes:
126///  - cumulative phases: `cum:<first>-><marker_i>`
127///  - incremental phases: `inc:<marker_i-1>-><marker_i>`
128///  - explicit spans: `span:<label>` and any `span:<A->B>` recorded
129pub fn take_as_timings() -> Vec<Timing> {
130    let mut r = REC.lock().unwrap();
131
132    let mut out: Vec<Timing> = Vec::new();
133
134    if !r.markers.is_empty() {
135        // Cumulative: first -> each
136        let first = &r.markers[0];
137        for (i, m) in r.markers.iter().enumerate() {
138            let nanos = (m.t - first.t).as_nanos();
139            let name = if i == 0 {
140                format!("cum:{}->{}", m.name, m.name)
141            } else {
142                format!("cum:{}->{}", first.name, m.name)
143            };
144            out.push(Timing { name, nanos });
145        }
146
147        // Incremental: prev -> current
148        for w in r.markers.windows(2) {
149            let a = &w[0];
150            let b = &w[1];
151            let nanos = (b.t - a.t).as_nanos();
152            out.push(Timing {
153                name: format!("inc:{}->{}", a.name, b.name),
154                nanos,
155            });
156        }
157    }
158
159    // Ad-hoc spans
160    for (label, nanos) in r.spans.drain(..) {
161        out.push(Timing { name: label, nanos });
162    }
163
164    // Reset for next run
165    r.markers.clear();
166    r.name_to_id.clear();
167    r.next_id = 0;
168
169    out
170}
171
172/// Convenience: attach current metrics to an Envelope (appends to `timings`)
173pub fn cd <T>(mut env: Envelope<T>) -> Envelope<T> {
174    let timings = take_as_timings();
175    if !timings.is_empty() {
176        if env.timings.is_empty() {
177            env.timings = timings;
178        } else {
179            env.timings.extend(timings);
180        }
181    }
182    env
183}
184
185/*  RAII guard  */
186
187pub struct SpanGuard {
188    label: String,
189    start: Instant,
190    committed: bool,
191}
192
193impl SpanGuard {
194    /// Manually end the span now and record it.
195    pub fn end(mut self) {
196        if !self.committed {
197            let nanos = self.start.elapsed().as_nanos();
198            REC.lock().unwrap().spans.push((format!("span:{}", self.label), nanos));
199            self.committed = true;
200        }
201    }
202}
203
204impl Drop for SpanGuard {
205    fn drop(&mut self) {
206        if !self.committed {
207            let nanos = self.start.elapsed().as_nanos();
208            REC.lock().unwrap().spans.push((format!("span:{}", self.label), nanos));
209            self.committed = true;
210        }
211    }
212}