benchman/
lib.rs

1//! benchman is a RAII-style benchmark tool that
2//! focuses on old fashioned one-shot benchmark rather than statistical benchmark.
3
4use colored::*;
5use indexmap::{IndexMap, IndexSet};
6use std::collections::HashMap;
7use std::fmt;
8use std::sync::Arc;
9use std::sync::RwLock;
10use std::time::{Duration, Instant};
11
12#[derive(Clone, Debug)]
13struct BenchResult {
14    list: Vec<Duration>,
15}
16impl BenchResult {
17    fn new() -> Self {
18        Self { list: vec![] }
19    }
20    fn n(&self) -> usize {
21        self.list.len()
22    }
23    fn add_result(&mut self, du: Duration) {
24        self.list.push(du);
25    }
26    fn average(&self) -> Duration {
27        let n = self.list.len();
28        let mut sum = Duration::from_secs(0);
29        for &du in &self.list {
30            sum += du;
31        }
32        sum / (n as u32)
33    }
34    fn percentile(&self, p: u64) -> Duration {
35        assert!(p > 0);
36        let mut list = self.list.clone();
37        list.sort();
38        let p = p as f64 / 100.;
39        let n = self.list.len() as f64;
40        let i = f64::ceil(p * n) as usize;
41        list[i - 1]
42    }
43}
44impl fmt::Display for BenchResult {
45    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46        let p50 = self.percentile(50);
47        let p95 = self.percentile(95);
48        let p99 = self.percentile(99);
49        writeln!(f, "[ave.] {:?}", self.average())?;
50        writeln!(f, "{:?} (>50%), {:?} (>95%), {:?} (>99%)", p50, p95, p99)?;
51        Ok(())
52    }
53}
54#[derive(Debug)]
55struct ResultSet {
56    tag_indices: IndexSet<String>,
57    h: HashMap<String, BenchResult>,
58}
59impl ResultSet {
60    fn new() -> Self {
61        Self {
62            tag_indices: IndexSet::new(),
63            h: HashMap::new(),
64        }
65    }
66    fn reserve_tag(&mut self, tag: String) {
67        self.tag_indices.insert(tag);
68    }
69    fn add_result(&mut self, tag: String, du: Duration) {
70        self.h
71            .entry(tag)
72            .or_insert(BenchResult::new())
73            .add_result(du);
74    }
75}
76/// Benchman who collects the result from stopwatches.
77///
78/// ```rust
79/// use benchman::*;
80/// let bm = BenchMan::new("bm_tag");
81/// let sw = bm.get_stopwatch("sw_tag");
82/// let mut sum = 0;
83/// for i in 1..10 { sum += i; }
84/// drop(sw);
85/// eprintln!("{}", bm);
86/// ```
87#[derive(Clone)]
88pub struct BenchMan {
89    tag: Arc<String>,
90    result_set: Arc<RwLock<ResultSet>>,
91}
92impl BenchMan {
93    /// Create a benchman.
94    pub fn new(tag: &str) -> Self {
95        let result_set = Arc::new(RwLock::new(ResultSet::new()));
96        Self {
97            tag: Arc::new(tag.to_owned()),
98            result_set,
99        }
100    }
101    /// Get a stopwatch from benchman.
102    pub fn get_stopwatch(&self, tag: &str) -> Stopwatch {
103        self.result_set.write().unwrap().reserve_tag(tag.to_owned());
104        Stopwatch::new(tag.to_owned(), self.result_set.clone())
105    }
106    /// Get an immutable view of the benchman.
107    ///
108    /// If a tag in the list isn't found in the current result, the tag is ignored.
109    pub fn slice<'a>(&'a self, sw_tags: impl IntoIterator<Item = &'a str>) -> BenchManSlice<'a> {
110        let result_set_reader = &self.result_set.read().unwrap();
111        let mut m = IndexMap::new();
112        for sw_tag in sw_tags {
113            if let Some(br) = result_set_reader.h.get(sw_tag) {
114                m.insert(sw_tag, br.clone());
115            }
116        }
117        BenchManSlice {
118            bm_tag: &self.tag,
119            slices: m,
120        }
121    }
122}
123impl fmt::Display for BenchMan {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        let bench_tag = &self.tag;
126        writeln!(f, "{}", bench_tag.blue())?;
127        let result_set_reader = &self.result_set.read().unwrap();
128        for sw_tag in &result_set_reader.tag_indices {
129            if let Some(v) = result_set_reader.h.get(sw_tag) {
130                let tag = format!("{} ({} samples)", sw_tag, v.n());
131                writeln!(f, "{}", tag.yellow())?;
132                writeln!(f, "{}", v)?;
133            }
134        }
135        Ok(())
136    }
137}
138/// Immutable view of the benchman.
139pub struct BenchManSlice<'a> {
140    bm_tag: &'a str,
141    slices: IndexMap<&'a str, BenchResult>,
142}
143impl<'a> fmt::Display for BenchManSlice<'a> {
144    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145        let bench_tag = &self.bm_tag;
146        writeln!(f, "{}", bench_tag.blue())?;
147        for (sw_tag, br) in &self.slices {
148            let tag = format!("{} ({} samples)", sw_tag, br.n());
149            writeln!(f, "{}", tag.yellow())?;
150            writeln!(f, "{}", br)?;
151        }
152        Ok(())
153    }
154}
155
156/// On drop, it sends a result to the benchman.
157pub struct Stopwatch {
158    tag: Option<String>,
159    t: Instant,
160    result_set: Arc<RwLock<ResultSet>>,
161}
162impl Stopwatch {
163    fn new(tag: String, result_set: Arc<RwLock<ResultSet>>) -> Self {
164        Self {
165            tag: Some(tag),
166            t: Instant::now(),
167            result_set,
168        }
169    }
170}
171impl Drop for Stopwatch {
172    fn drop(&mut self) {
173        let elapsed = self.t.elapsed();
174        let sw_tag = self.tag.take().unwrap();
175        self.result_set.write().unwrap().add_result(sw_tag, elapsed);
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_benchman_spawn() {
185        let benchman = BenchMan::new("spawn");
186        for _ in 0..1 {
187            let bm = benchman.clone();
188            std::thread::spawn(move || {
189                let _sw = bm.get_stopwatch("loop1");
190                let mut _sum: u64 = 0;
191                for i in 0..1000000 {
192                    _sum += i;
193                }
194            });
195        }
196        for _ in 0..100 {
197            let bm = benchman.clone();
198            std::thread::spawn(move || {
199                let _sw = bm.get_stopwatch("loop2");
200                let mut _sum: u64 = 0;
201                for i in 0..1000000 {
202                    _sum += i;
203                }
204            });
205        }
206        std::thread::sleep(Duration::from_secs(1));
207        println!("{}", benchman);
208    }
209
210    #[test]
211    fn test_benchman_nested() {
212        let benchman = BenchMan::new("nested");
213        let mut _sum: u64 = 0;
214        let sw1 = benchman.get_stopwatch("outer");
215        for i in 0..1000 {
216            let _sw2 = benchman.get_stopwatch("inner");
217            for j in 0..100000 {
218                _sum += i * j;
219            }
220        }
221        println!("{}", benchman.slice(["inner", "outer"]));
222        drop(sw1);
223        println!("{}", benchman.slice(["inner", "outer"]));
224        println!("{}", benchman);
225    }
226}