Skip to main content

runex_core/
timings.rs

1use std::time::{Duration, Instant};
2
3/// A recorded phase with its name and duration.
4#[derive(Debug, Clone)]
5pub struct Phase {
6    pub name: String,
7    pub duration: Duration,
8}
9
10/// A recorded `command_exists` call with result and duration.
11#[derive(Debug, Clone)]
12pub struct CommandExistsCall {
13    pub command: String,
14    pub found: bool,
15    pub duration: Duration,
16    /// Whether this result came from the precache hint layer.
17    pub cached: bool,
18}
19
20/// Collects timing data for expand phases and command_exists calls.
21#[derive(Debug, Default)]
22pub struct Timings {
23    phases: Vec<Phase>,
24    command_exists_calls: Vec<CommandExistsCall>,
25}
26
27/// Lightweight timer — just wraps `Instant::now()`.
28pub struct PhaseTimer {
29    start: Instant,
30}
31
32impl PhaseTimer {
33    pub fn start() -> Self {
34        Self { start: Instant::now() }
35    }
36
37    pub fn elapsed(&self) -> Duration {
38        self.start.elapsed()
39    }
40}
41
42impl Timings {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    pub fn record_phase(&mut self, name: &str, duration: Duration) {
48        self.phases.push(Phase {
49            name: name.to_string(),
50            duration,
51        });
52    }
53
54    pub fn record_command_exists(&mut self, command: &str, found: bool, duration: Duration, cached: bool) {
55        self.command_exists_calls.push(CommandExistsCall {
56            command: command.to_string(),
57            found,
58            duration,
59            cached,
60        });
61    }
62
63    pub fn phases(&self) -> &[Phase] {
64        &self.phases
65    }
66
67    pub fn command_exists_calls(&self) -> &[CommandExistsCall] {
68        &self.command_exists_calls
69    }
70
71    pub fn total_duration(&self) -> Duration {
72        self.phases.iter().map(|p| p.duration).sum()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn timings_new_is_empty() {
82        let t = Timings::new();
83        assert!(t.phases().is_empty());
84        assert!(t.command_exists_calls().is_empty());
85        assert_eq!(t.total_duration(), Duration::ZERO);
86    }
87
88    #[test]
89    fn timings_record_phase() {
90        let mut t = Timings::new();
91        t.record_phase("config_load", Duration::from_micros(1230));
92        assert_eq!(t.phases().len(), 1);
93        assert_eq!(t.phases()[0].name, "config_load");
94        assert_eq!(t.phases()[0].duration, Duration::from_micros(1230));
95    }
96
97    #[test]
98    fn timings_record_command_exists_call() {
99        let mut t = Timings::new();
100        t.record_command_exists("git", true, Duration::from_micros(2340), false);
101        t.record_command_exists("lsd", false, Duration::from_micros(3120), true);
102        assert_eq!(t.command_exists_calls().len(), 2);
103        assert_eq!(t.command_exists_calls()[0].command, "git");
104        assert!(t.command_exists_calls()[0].found);
105        assert!(!t.command_exists_calls()[0].cached);
106        assert_eq!(t.command_exists_calls()[1].command, "lsd");
107        assert!(!t.command_exists_calls()[1].found);
108        assert!(t.command_exists_calls()[1].cached);
109    }
110
111    #[test]
112    fn timings_total_duration() {
113        let mut t = Timings::new();
114        t.record_phase("a", Duration::from_micros(100));
115        t.record_phase("b", Duration::from_micros(200));
116        assert_eq!(t.total_duration(), Duration::from_micros(300));
117    }
118
119    #[test]
120    fn phase_timer_elapsed_is_positive() {
121        let timer = PhaseTimer::start();
122        std::thread::sleep(Duration::from_millis(1));
123        assert!(timer.elapsed() >= Duration::from_millis(1));
124    }
125}