Skip to main content

dudect_bencher/
ctbench.rs

1use crate::stats;
2
3use std::{
4    fs::{File, OpenOptions},
5    hint::black_box,
6    io::{self, Write},
7    path::PathBuf,
8    process,
9    sync::{
10        atomic::{self, AtomicBool},
11        Arc,
12    },
13    time::Instant,
14};
15
16use ctrlc;
17use rand::{Rng, SeedableRng};
18use rand_chacha::ChaChaRng;
19
20/// Just a static str representing the name of a function
21#[derive(Copy, Clone)]
22pub struct BenchName(pub &'static str);
23
24impl BenchName {
25    fn padded(&self, column_count: usize) -> String {
26        let mut name = self.0.to_string();
27        let pad_len = column_count.saturating_sub(name.len());
28        let pad = " ".repeat(pad_len);
29        name.push_str(&pad);
30
31        name
32    }
33}
34
35/// A random number generator implementing [`rand::SeedableRng`]. This is given to every
36/// benchmarking function to use as a source of randomness.
37pub type BenchRng = ChaChaRng;
38
39/// A function that is to be benchmarked. This crate only supports statically-defined functions.
40pub type BenchFn = fn(&mut CtRunner, &mut BenchRng);
41
42// TODO: Consider giving this a lifetime so we don't have to copy names and vecs into it
43#[derive(Clone)]
44enum BenchEvent {
45    ContStart,
46    Begin(Vec<BenchName>),
47    Wait(BenchName),
48    Result(MonitorMsg),
49    Seed(u64, BenchName),
50}
51
52type MonitorMsg = (BenchName, stats::CtSummary);
53
54/// CtBencher is the primary interface for benchmarking. All setup for function inputs should be
55/// doen within the closure supplied to the `iter` method.
56struct CtBencher {
57    samples: (Vec<u64>, Vec<u64>),
58    ctx: Option<stats::CtCtx>,
59    file_out: Option<File>,
60    rng: BenchRng,
61}
62
63impl CtBencher {
64    /// Creates and returns a new empty `CtBencher` whose `BenchRng` is zero-seeded
65    pub fn new() -> CtBencher {
66        CtBencher {
67            samples: (Vec::new(), Vec::new()),
68            ctx: None,
69            file_out: None,
70            rng: BenchRng::seed_from_u64(0u64),
71        }
72    }
73
74    /// Runs the bench function and returns the CtSummary
75    fn go(&mut self, f: BenchFn) -> stats::CtSummary {
76        // This populates self.samples
77        let mut runner = CtRunner::default();
78        f(&mut runner, &mut self.rng);
79        self.samples = runner.runtimes;
80
81        // Replace the old CtCtx with an updated one
82        let old_self = ::std::mem::replace(self, CtBencher::new());
83        let (summ, new_ctx) = stats::update_ct_stats(old_self.ctx, &old_self.samples);
84
85        // Copy the old stuff back in
86        self.samples = old_self.samples;
87        self.file_out = old_self.file_out;
88        self.ctx = Some(new_ctx);
89        self.rng = old_self.rng;
90
91        summ
92    }
93
94    /// Returns a random seed
95    fn rand_seed() -> u64 {
96        rand::rng().next_u64()
97    }
98
99    /// Reseeds the internal RNG with the given seed
100    pub fn seed_with(&mut self, seed: u64) {
101        self.rng = BenchRng::seed_from_u64(seed);
102    }
103
104    /// Clears out all sample and contextual data
105    fn clear_data(&mut self) {
106        self.samples = (Vec::new(), Vec::new());
107        self.ctx = None;
108    }
109}
110
111/// Represents a single benchmark to conduct
112pub struct BenchMetadata {
113    pub name: BenchName,
114    pub seed: Option<u64>,
115    pub benchfn: BenchFn,
116}
117
118/// Benchmarking options.
119///
120/// When `continuous` is set, it will continuously set the first (alphabetically) of the benchmarks
121/// after they have been optionally filtered.
122///
123/// When `filter` is set and `continuous` is not set, only benchmarks whose names contain the
124/// filter string as a substring will be executed.
125///
126/// `file_out` is optionally the filename where CSV output of raw runtime data should be written
127#[derive(Default)]
128pub struct BenchOpts {
129    pub continuous: bool,
130    pub filter: Option<String>,
131    pub file_out: Option<PathBuf>,
132}
133
134struct ConsoleBenchState {
135    // Number of columns to fill when aligning names
136    max_name_len: usize,
137}
138
139impl ConsoleBenchState {
140    fn write_plain(&mut self, s: &str) -> io::Result<()> {
141        let mut stdout = io::stdout();
142        stdout.write_all(s.as_bytes())?;
143        stdout.flush()
144    }
145
146    fn write_bench_start(&mut self, name: &BenchName) -> io::Result<()> {
147        let name = name.padded(self.max_name_len);
148        self.write_plain(&format!("bench {} ... ", name))
149    }
150
151    fn write_seed(&mut self, seed: u64, name: &BenchName) -> io::Result<()> {
152        let name = name.padded(self.max_name_len);
153        self.write_plain(&format!("bench {} seeded with 0x{:016x}\n", name, seed))
154    }
155
156    fn write_run_start(&mut self, len: usize) -> io::Result<()> {
157        let noun = if len != 1 { "benches" } else { "bench" };
158        self.write_plain(&format!("\nrunning {} {}\n", len, noun))
159    }
160
161    fn write_continuous_start(&mut self) -> io::Result<()> {
162        self.write_plain("running 1 benchmark continuously\n")
163    }
164
165    fn write_result(&mut self, summ: &stats::CtSummary) -> io::Result<()> {
166        self.write_plain(&format!(": {}\n", summ.fmt()))
167    }
168
169    fn write_run_finish(&mut self) -> io::Result<()> {
170        self.write_plain("\ndudect benches complete\n\n")
171    }
172}
173
174/// Runs the given benches under the given options and prints the output to the console
175pub fn run_benches_console(opts: BenchOpts, benches: Vec<BenchMetadata>) -> io::Result<()> {
176    // TODO: Consider making this do screen updates in continuous mode
177    // TODO: Consider making this run in its own thread
178    fn callback(event: &BenchEvent, st: &mut ConsoleBenchState) -> io::Result<()> {
179        match (*event).clone() {
180            BenchEvent::ContStart => st.write_continuous_start(),
181            BenchEvent::Begin(ref filtered_benches) => st.write_run_start(filtered_benches.len()),
182            BenchEvent::Wait(ref b) => st.write_bench_start(b),
183            BenchEvent::Result(msg) => {
184                let (_, summ) = msg;
185                st.write_result(&summ)
186            }
187            BenchEvent::Seed(seed, ref name) => st.write_seed(seed, name),
188        }
189    }
190
191    let mut st = ConsoleBenchState {
192        max_name_len: benches.iter().map(|t| t.name.0.len()).max().unwrap_or(0),
193    };
194
195    run_benches(&opts, benches, |x| callback(&x, &mut st))?;
196    st.write_run_finish()
197}
198
199/// Returns an atomic bool that indicates whether Ctrl-C was pressed
200fn setup_kill_bit() -> Arc<AtomicBool> {
201    let x = Arc::new(AtomicBool::new(false));
202    let y = x.clone();
203
204    ctrlc::set_handler(move || y.store(true, atomic::Ordering::SeqCst))
205        .expect("Error setting Ctrl-C handler");
206
207    x
208}
209
210fn run_benches<F>(opts: &BenchOpts, benches: Vec<BenchMetadata>, mut callback: F) -> io::Result<()>
211where
212    F: FnMut(BenchEvent) -> io::Result<()>,
213{
214    let filter = &opts.filter;
215    let filtered_benches = filter_benches(filter, benches);
216    let filtered_names = filtered_benches.iter().map(|b| b.name).collect();
217
218    // Write the CSV header line to the file if the file is defined
219    let mut file_out = opts.file_out.as_ref().map(|filename| {
220        OpenOptions::new()
221            .write(true)
222            .truncate(true)
223            .create(true)
224            .open(filename)
225            .unwrap_or_else(|e| panic!("Could not open file '{:?}' for writing: {e}", filename))
226    });
227    file_out.as_mut().map(|f| {
228        f.write(b"benchname,class,runtime")
229            .expect("Error writing CSV header to file")
230    });
231
232    // Make a bencher with the optional file output specified
233    let mut cb: CtBencher = {
234        let mut d = CtBencher::new();
235        d.file_out = file_out;
236        d
237    };
238
239    if opts.continuous {
240        callback(BenchEvent::ContStart)?;
241
242        if filtered_benches.is_empty() {
243            match *filter {
244                Some(ref f) => panic!("No benchmark matching '{}' was found", f),
245                None => return Ok(()),
246            }
247        }
248
249        // Get a bit that tells us when we've been killed
250        let kill_bit = setup_kill_bit();
251
252        // Continuously run the first matched bench we see
253        let mut filtered_benches = filtered_benches;
254        let bench = filtered_benches.remove(0);
255
256        // If a seed was specified for this bench, use it. Otherwise, use a random seed
257        let seed = bench.seed.unwrap_or_else(CtBencher::rand_seed);
258        cb.seed_with(seed);
259        callback(BenchEvent::Seed(seed, bench.name))?;
260
261        loop {
262            callback(BenchEvent::Wait(bench.name))?;
263            let msg = run_bench_with_bencher(&bench.name, bench.benchfn, &mut cb);
264            callback(BenchEvent::Result(msg))?;
265
266            // Check if the program has been killed. If so, exit
267            if kill_bit.load(atomic::Ordering::SeqCst) {
268                process::exit(0);
269            }
270        }
271    } else {
272        callback(BenchEvent::Begin(filtered_names))?;
273
274        // Run different benches
275        for bench in filtered_benches {
276            // Clear the data out from the previous bench, but keep the CSV file open
277            cb.clear_data();
278
279            // If a seed was specified for this bench, use it. Otherwise, use a random seed
280            let seed = bench.seed.unwrap_or_else(CtBencher::rand_seed);
281            cb.seed_with(seed);
282            callback(BenchEvent::Seed(seed, bench.name))?;
283
284            callback(BenchEvent::Wait(bench.name))?;
285            let msg = run_bench_with_bencher(&bench.name, bench.benchfn, &mut cb);
286            callback(BenchEvent::Result(msg))?;
287        }
288        Ok(())
289    }
290}
291
292fn run_bench_with_bencher(name: &BenchName, benchfn: BenchFn, cb: &mut CtBencher) -> MonitorMsg {
293    let summ = cb.go(benchfn);
294
295    // Write the runtime samples out
296    let samples_iter = cb.samples.0.iter().zip(cb.samples.1.iter());
297    if let Some(f) = cb.file_out.as_mut() {
298        for (x, y) in samples_iter {
299            write!(f, "\n{},0,{}", name.0, x).expect("Error writing data to file");
300            write!(f, "\n{},0,{}", name.0, y).expect("Error writing data to file");
301        }
302    };
303
304    (*name, summ)
305}
306
307fn filter_benches(filter: &Option<String>, bs: Vec<BenchMetadata>) -> Vec<BenchMetadata> {
308    let mut filtered = bs;
309
310    // Remove benches that don't match the filter
311    filtered = match *filter {
312        None => filtered,
313        Some(ref filter) => filtered
314            .into_iter()
315            .filter(|b| b.name.0.contains(&filter[..]))
316            .collect(),
317    };
318
319    // Sort them alphabetically
320    filtered.sort_by(|b1, b2| b1.name.0.cmp(b2.name.0));
321
322    filtered
323}
324
325/// Specifies the distribution that a particular run belongs to
326#[derive(Copy, Clone)]
327pub enum Class {
328    Left,
329    Right,
330}
331
332/// Used for timing single operations at a time
333#[derive(Default)]
334pub struct CtRunner {
335    // Runtimes of left and right distributions in nanoseconds
336    runtimes: (Vec<u64>, Vec<u64>),
337}
338
339impl CtRunner {
340    /// Runs and times a single operation whose constant-timeness is in question
341    pub fn run_one<T, F>(&mut self, class: Class, f: F)
342    where
343        F: Fn() -> T,
344    {
345        let start = Instant::now();
346        black_box(f());
347        let end = Instant::now();
348
349        let runtime = {
350            let dur = end.duration_since(start);
351            dur.as_secs() * 1_000_000_000 + u64::from(dur.subsec_nanos())
352        };
353
354        match class {
355            Class::Left => self.runtimes.0.push(runtime),
356            Class::Right => self.runtimes.1.push(runtime),
357        }
358    }
359}