petr_profiling/
lib.rs

1//! Basic tools and abstractions for profiling performance and timing data and then displaying it.
2
3//! TODO/wishlist:
4//! use a .with(fn) pattern to ensure starts and stops always match
5//! keep a stack to know which events are sub-events of others, and show that in the
6//! table with indentation
7
8use std::{collections::HashMap, time::Duration};
9
10use cli_table::{format::Justify, Cell, Row, RowStruct, Style};
11
12#[derive(Default)]
13pub struct Timings {
14    entries: HashMap<&'static str, Vec<ProfileEntry>>,
15    pending: HashMap<&'static str, Vec<std::time::Instant>>,
16}
17
18pub struct ProfileEntry {
19    time: Duration,
20}
21
22impl Timings {
23    pub fn start(
24        &mut self,
25        key: &'static str,
26    ) {
27        self.pending.entry(key).or_default().push(std::time::Instant::now());
28    }
29
30    pub fn end(
31        &mut self,
32        key: &'static str,
33    ) {
34        let Some(entry) = self.pending.get_mut(key) else {
35            eprintln!("Profiling error: tried to end event {key} which didn't exist");
36            return;
37        };
38        let Some(start) = entry.pop() else {
39            eprintln!("Profiling error: tried to end event {key} which didn't exist");
40            return;
41        };
42        self.entries.entry(key).or_default().push(ProfileEntry { time: start.elapsed() });
43    }
44
45    pub fn render(&self) -> String {
46        let unended = self.pending.iter().filter(|(_k, v)| !v.is_empty()).collect::<Vec<_>>();
47        if !unended.is_empty() {
48            eprintln!("Profiling error: some events were not ended");
49            eprintln!("{:?}", unended);
50        }
51
52        // TODO render outliers, median, average, etc etc
53
54        use cli_table::Table;
55        use num_format::{Locale, ToFormattedString};
56
57        let mut table = vec![];
58
59        for (key, entries) in &self.entries {
60            let total_duration: Duration = entries.iter().map(|e| e.time).sum();
61            let duration = match total_duration.as_millis() {
62                x if x < 10 => format!("{} ns", total_duration.as_nanos().to_formatted_string(&Locale::en)),
63                otherwise => format!("{} ms", otherwise.to_formatted_string(&Locale::en)),
64            };
65            let duration = duration.cell().justify(Justify::Right);
66
67            let row: RowStruct = vec![key.to_string().cell().bold(true), duration].row();
68            table.push(row);
69        }
70
71        let table = table
72            .table()
73            .title(vec!["Event".cell().bold(true), "Total Duration".cell().bold(true)])
74            .display()
75            .expect("failed to render profiles table");
76
77        format!("{}", table)
78    }
79}