usls/core/
ts.rs

1#![allow(dead_code)]
2use std::collections::HashMap;
3use std::time::Duration;
4
5/// A macro to measure the execution time of a given code block and optionally log the result.
6#[macro_export]
7#[doc(hidden)]
8macro_rules! elapsed {
9    ($code:expr) => {{
10        let t = std::time::Instant::now();
11        let ret = $code;
12        let duration = t.elapsed();
13        (duration, ret)
14    }};
15    ($label:expr, $ts:expr, $code:expr) => {{
16        let t = std::time::Instant::now();
17        let ret = $code;
18        let duration = t.elapsed();
19        $ts.push($label, duration);
20        ret
21    }};
22}
23
24/// Time series collection for performance measurement and profiling.
25#[derive(aksr::Builder, Debug, Default, Clone, PartialEq)]
26pub struct Ts {
27    // { k1: [d1,d1,d1,..], k2: [d2,d2,d2,..], k3: [d3,d3,d3,..], ..}
28    map: HashMap<String, Vec<Duration>>,
29    names: Vec<String>,
30}
31
32impl std::ops::Index<&str> for Ts {
33    type Output = Vec<Duration>;
34
35    fn index(&self, index: &str) -> &Self::Output {
36        self.map.get(index).unwrap_or_else(|| {
37            let available_keys: Vec<&str> = self.map.keys().map(|s| s.as_str()).collect();
38            panic!(
39                "Key '{}' was not found in Ts. Available keys: {:?}",
40                index, available_keys
41            )
42        })
43    }
44}
45
46impl std::ops::Index<usize> for Ts {
47    type Output = Vec<Duration>;
48
49    fn index(&self, index: usize) -> &Self::Output {
50        self.names
51            .get(index)
52            .and_then(|key| self.map.get(key))
53            .unwrap_or_else(|| {
54                panic!(
55                    "Index {} was not found in Ts. Available indices: 0..{}",
56                    index,
57                    self.names.len()
58                )
59            })
60    }
61}
62
63impl Ts {
64    pub fn summary(&self) {
65        let decimal_places = 4;
66        let place_holder = '-';
67        let width_count = 10;
68        let width_time = 15;
69        let width_task = self
70            .names
71            .iter()
72            .map(|s| s.len())
73            .max()
74            .map(|x| x + 8)
75            .unwrap_or(60);
76
77        let sep = "-".repeat(width_task + 66);
78
79        // cols
80        println!(
81            "\n\n{:<width_task$}{:<width_count$}{:<width_time$}{:<width_time$}{:<width_time$}{:<width_time$}",
82            "Task", "Count", "Mean", "Min", "Max", "Total",
83        );
84        println!("{}", sep);
85
86        if self.is_empty() {
87            println!("No data available");
88        } else {
89            // rows
90            let total_name = "(Total)".to_string();
91            let mut iter = self
92                .names
93                .iter()
94                .chain(std::iter::once(&total_name))
95                .peekable();
96            while let Some(task) = iter.next() {
97                if iter.peek().is_none() {
98                    let avg = self
99                        .avg()
100                        .map_or(place_holder.into(), |x| format!("{:.decimal_places$?}", x));
101                    let total = format!("{:.decimal_places$?}", self.sum());
102                    println!(
103                        "{:<width_task$}{:<width_count$}{:<width_time$}{:<width_time$}{:<width_time$}{:<width_time$}",
104                        task, place_holder, avg, place_holder, place_holder, total
105                    );
106                } else {
107                    let durations = &self.map[task];
108                    let count = durations.len();
109                    let total = format!("{:.decimal_places$?}", self.sum_by_key(task));
110                    let avg = self
111                        .avg_by_key(task)
112                        .map_or(place_holder.into(), |x| format!("{:.decimal_places$?}", x));
113                    let min = durations
114                        .iter()
115                        .min()
116                        .map_or(place_holder.into(), |x| format!("{:.decimal_places$?}", x));
117                    let max = durations
118                        .iter()
119                        .max()
120                        .map_or(place_holder.into(), |x| format!("{:.decimal_places$?}", x));
121
122                    println!(
123                        "{:<width_task$}{:<width_count$}{:<width_time$}{:<width_time$}{:<width_time$}{:<width_time$}",
124                        task, count, avg, min, max, total
125                    );
126                }
127            }
128        }
129    }
130
131    pub fn merge(xs: &[&Ts]) -> Self {
132        let mut names = Vec::new();
133        let mut map: HashMap<String, Vec<Duration>> = HashMap::new();
134        for x in xs.iter() {
135            names.extend_from_slice(x.get_names());
136            map.extend(x.get_map().to_owned());
137        }
138
139        Self { names, map }
140    }
141
142    pub fn push(&mut self, k: &str, v: Duration) {
143        if !self.names.contains(&k.to_string()) {
144            self.names.push(k.to_string());
145        }
146        self.map
147            .entry(k.to_string())
148            .and_modify(|x| x.push(v))
149            .or_insert(vec![v]);
150    }
151
152    pub fn numit(&self) -> anyhow::Result<usize> {
153        // num of iterations
154        if self.names.is_empty() {
155            anyhow::bail!("Empty Ts");
156        }
157
158        let len = self[0].len();
159        for v in self.map.values() {
160            if v.len() != len {
161                anyhow::bail!(
162                    "Invalid Ts: The number of elements in each values entry is inconsistent"
163                );
164            }
165        }
166
167        Ok(len)
168    }
169
170    pub fn is_valid(&self) -> bool {
171        let mut iter = self.map.values();
172        if let Some(first) = iter.next() {
173            let len = first.len();
174            iter.all(|v| v.len() == len)
175        } else {
176            true
177        }
178    }
179
180    pub fn sum_by_index(&self, i: usize) -> Duration {
181        self[i].iter().sum::<Duration>()
182    }
183
184    pub fn sum_by_key(&self, i: &str) -> Duration {
185        self[i].iter().sum::<Duration>()
186    }
187
188    pub fn avg_by_index(&self, i: usize) -> anyhow::Result<Duration> {
189        let len = self[i].len();
190        if len == 0 {
191            anyhow::bail!("Cannot compute average for an empty duration vector.")
192        } else {
193            Ok(self.sum_by_index(i) / len as u32)
194        }
195    }
196
197    pub fn avg_by_key(&self, i: &str) -> anyhow::Result<Duration> {
198        let len = self[i].len();
199        if len == 0 {
200            anyhow::bail!("Cannot compute average for an empty duration vector.")
201        } else {
202            Ok(self.sum_by_key(i) / len as u32)
203        }
204    }
205
206    pub fn sum_column(&self, i: usize) -> Duration {
207        self.map
208            .values()
209            .filter_map(|vec| vec.get(i))
210            .copied()
211            .sum()
212    }
213
214    pub fn sum(&self) -> Duration {
215        self.map.values().flat_map(|vec| vec.iter()).copied().sum()
216    }
217
218    pub fn avg(&self) -> anyhow::Result<Duration> {
219        self.names.iter().map(|x| self.avg_by_key(x)).sum()
220    }
221
222    pub fn skip(mut self, n: usize) -> Self {
223        self.map.iter_mut().for_each(|(_, vec)| {
224            *vec = vec.iter().skip(n).copied().collect();
225        });
226        self
227    }
228
229    pub fn clear(&mut self) {
230        self.names.clear();
231        self.map.clear();
232    }
233
234    pub fn is_empty(&self) -> bool {
235        self.names.is_empty() && self.map.is_empty()
236    }
237
238    /// Get reference to the names vector
239    pub fn get_names(&self) -> &Vec<String> {
240        &self.names
241    }
242
243    /// Get reference to the internal map
244    pub fn get_map(&self) -> &HashMap<String, Vec<Duration>> {
245        &self.map
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::time::Duration;
253
254    #[test]
255    fn test_push_and_indexing() {
256        let mut ts = Ts::default();
257
258        ts.push("task1", Duration::new(1, 0));
259        ts.push("task1", Duration::new(2, 0));
260        ts.push("task2", Duration::new(3, 0));
261
262        assert_eq!(ts["task1"], vec![Duration::new(1, 0), Duration::new(2, 0)]);
263        assert_eq!(ts["task2"], vec![Duration::new(3, 0)]);
264    }
265
266    #[test]
267    fn test_numit() {
268        let mut ts = Ts::default();
269
270        ts.push("task1", Duration::new(1, 0));
271        ts.push("task1", Duration::new(2, 0));
272        ts.push("task2", Duration::new(3, 0));
273        ts.push("task2", Duration::new(4, 0));
274
275        assert_eq!(ts.numit().unwrap(), 2);
276    }
277
278    #[test]
279    fn test_is_valid() {
280        let mut ts = Ts::default();
281
282        ts.push("task1", Duration::new(1, 0));
283        ts.push("task1", Duration::new(2, 0));
284        ts.push("task2", Duration::new(3, 0));
285
286        assert!(!ts.is_valid());
287
288        ts.push("task2", Duration::new(4, 0));
289        ts.push("task3", Duration::new(5, 0));
290
291        assert!(!ts.is_valid());
292
293        ts.push("task3", Duration::new(6, 0));
294        assert!(ts.is_valid());
295    }
296
297    #[test]
298    fn test_sum_by_index() {
299        let mut ts = Ts::default();
300
301        ts.push("task1", Duration::new(1, 0));
302        ts.push("task1", Duration::new(2, 0));
303        ts.push("task2", Duration::new(3, 0));
304        ts.push("task2", Duration::new(3, 0));
305        ts.push("task2", Duration::new(3, 0));
306
307        assert_eq!(ts.sum_by_index(0), Duration::new(3, 0)); // 1 + 2
308        assert_eq!(ts.sum_by_index(1), Duration::new(9, 0)); // 1 + 2
309    }
310
311    #[test]
312    fn test_sum_by_key() {
313        let mut ts = Ts::default();
314
315        ts.push("task1", Duration::new(1, 0));
316        ts.push("task1", Duration::new(2, 0));
317        ts.push("task2", Duration::new(3, 0));
318        ts.push("task2", Duration::new(3, 0));
319        ts.push("task2", Duration::new(3, 0));
320
321        assert_eq!(ts.sum_by_key("task1"), Duration::new(3, 0)); // 1 + 2
322        assert_eq!(ts.sum_by_key("task2"), Duration::new(9, 0)); // 1 + 2
323    }
324
325    #[test]
326    fn test_avg_by_index() {
327        let mut ts = Ts::default();
328
329        ts.push("task1", Duration::new(1, 0));
330        ts.push("task1", Duration::new(2, 0));
331        ts.push("task2", Duration::new(2, 0));
332        ts.push("task2", Duration::new(2, 0));
333        ts.push("task3", Duration::new(2, 0));
334
335        assert_eq!(ts.avg_by_index(0).unwrap(), Duration::new(1, 500_000_000));
336        assert_eq!(ts.avg_by_index(1).unwrap(), Duration::new(2, 0));
337        assert_eq!(ts.avg_by_index(2).unwrap(), Duration::new(2, 0));
338    }
339
340    #[test]
341    fn test_avg_by_key() {
342        let mut ts = Ts::default();
343
344        ts.push("task1", Duration::new(1, 0));
345        ts.push("task1", Duration::new(2, 0));
346
347        let avg = ts.avg_by_key("task1").unwrap();
348        assert_eq!(avg, Duration::new(1, 500_000_000));
349    }
350
351    #[test]
352    fn test_sum_column() {
353        let mut ts = Ts::default();
354
355        ts.push("task1", Duration::new(1, 0));
356        ts.push("task1", Duration::new(2, 0));
357        ts.push("task2", Duration::new(3, 0));
358
359        assert_eq!(ts.sum_column(0), Duration::new(4, 0)); // 1 + 3
360    }
361
362    #[test]
363    fn test_sum() {
364        let mut ts = Ts::default();
365
366        ts.push("task1", Duration::new(1, 0));
367        ts.push("task1", Duration::new(2, 0));
368        ts.push("task2", Duration::new(3, 0));
369
370        assert_eq!(ts.sum(), Duration::new(6, 0));
371    }
372
373    #[test]
374    fn test_avg() {
375        let mut ts = Ts::default();
376
377        ts.push("task1", Duration::new(1, 0));
378        ts.push("task1", Duration::new(2, 0));
379        ts.push("task2", Duration::new(3, 0));
380        ts.push("task2", Duration::new(4, 0));
381
382        assert_eq!(ts.avg().unwrap(), Duration::new(5, 0));
383    }
384
385    #[test]
386    fn test_skip() {
387        let mut ts = Ts::default();
388
389        ts.push("task1", Duration::new(1, 0));
390        ts.push("task1", Duration::new(2, 0));
391        ts.push("task2", Duration::new(3, 0));
392        ts.push("task2", Duration::new(4, 0));
393        ts.push("task2", Duration::new(4, 0));
394
395        let ts_skipped = ts.skip(1);
396
397        assert_eq!(ts_skipped["task1"], vec![Duration::new(2, 0)]);
398        assert_eq!(
399            ts_skipped["task2"],
400            vec![Duration::new(4, 0), Duration::new(4, 0)]
401        );
402
403        let ts_skipped = ts_skipped.skip(1);
404
405        assert!(ts_skipped["task1"].is_empty());
406        assert_eq!(ts_skipped["task2"], vec![Duration::new(4, 0)]);
407    }
408
409    #[test]
410    fn test_clear() {
411        let mut ts = Ts::default();
412
413        ts.push("task1", Duration::new(1, 0));
414        ts.push("task2", Duration::new(2, 0));
415
416        ts.clear();
417        assert!(ts.names.is_empty());
418        assert!(ts.map.is_empty());
419    }
420}