dudect_bencher/
ctbench.rs

1use crate::stats;
2
3use std::{
4    fs::{File, OpenOptions},
5    io::{self, Write},
6    iter::repeat,
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 fill = column_count.saturating_sub(name.len());
28        let pad = repeat(" ").take(fill).collect::<String>();
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    BContStart,
46    BBegin(Vec<BenchName>),
47    BWait(BenchName),
48    BResult(MonitorMsg),
49    BSeed(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::thread_rng().gen()
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
134#[derive(Default)]
135struct ConsoleBenchState {
136    // Number of columns to fill when aligning names
137    max_name_len: usize,
138}
139
140impl ConsoleBenchState {
141    fn write_plain(&mut self, s: &str) -> io::Result<()> {
142        let mut stdout = io::stdout();
143        stdout.write_all(s.as_bytes())?;
144        stdout.flush()
145    }
146
147    fn write_bench_start(&mut self, name: &BenchName) -> io::Result<()> {
148        let name = name.padded(self.max_name_len);
149        self.write_plain(&format!("bench {} ... ", name))
150    }
151
152    fn write_seed(&mut self, seed: u64, name: &BenchName) -> io::Result<()> {
153        let name = name.padded(self.max_name_len);
154        self.write_plain(&format!("bench {} seeded with 0x{:016x}\n", name, seed))
155    }
156
157    fn write_run_start(&mut self, len: usize) -> io::Result<()> {
158        let noun = if len != 1 { "benches" } else { "bench" };
159        self.write_plain(&format!("\nrunning {} {}\n", len, noun))
160    }
161
162    fn write_continuous_start(&mut self) -> io::Result<()> {
163        self.write_plain("running 1 benchmark continuously\n")
164    }
165
166    fn write_result(&mut self, summ: &stats::CtSummary) -> io::Result<()> {
167        self.write_plain(&format!(": {}\n", summ.fmt()))
168    }
169
170    fn write_run_finish(&mut self) -> io::Result<()> {
171        self.write_plain("\ndudect benches complete\n\n")
172    }
173}
174
175/// Runs the given benches under the given options and prints the output to the console
176pub fn run_benches_console(opts: BenchOpts, benches: Vec<BenchMetadata>) -> io::Result<()> {
177    // TODO: Consider making this do screen updates in continuous mode
178    // TODO: Consider making this run in its own thread
179    fn callback(event: &BenchEvent, st: &mut ConsoleBenchState) -> io::Result<()> {
180        match (*event).clone() {
181            BenchEvent::BContStart => st.write_continuous_start(),
182            BenchEvent::BBegin(ref filtered_benches) => st.write_run_start(filtered_benches.len()),
183            BenchEvent::BWait(ref b) => st.write_bench_start(b),
184            BenchEvent::BResult(msg) => {
185                let (_, summ) = msg;
186                st.write_result(&summ)
187            }
188            BenchEvent::BSeed(seed, ref name) => st.write_seed(seed, name),
189        }
190    }
191
192    let mut st = ConsoleBenchState::default();
193    st.max_name_len = benches.iter().map(|t| t.name.0.len()).max().unwrap_or(0);
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    use self::BenchEvent::*;
215
216    let filter = &opts.filter;
217    let filtered_benches = filter_benches(filter, benches);
218    let filtered_names = filtered_benches.iter().map(|b| b.name).collect();
219
220    // Write the CSV header line to the file if the file is defined
221    let mut file_out = opts.file_out.as_ref().map(|filename| {
222        OpenOptions::new()
223            .write(true)
224            .truncate(true)
225            .create(true)
226            .open(filename)
227            .expect(&*format!(
228                "Could not open file '{:?}' for writing",
229                filename
230            ))
231    });
232    file_out.as_mut().map(|f| {
233        f.write(b"benchname,class,runtime")
234            .expect("Error writing CSV header to file")
235    });
236
237    // Make a bencher with the optional file output specified
238    let mut cb: CtBencher = {
239        let mut d = CtBencher::new();
240        d.file_out = file_out;
241        d
242    };
243
244    if opts.continuous {
245        callback(BContStart)?;
246
247        if filtered_benches.is_empty() {
248            match *filter {
249                Some(ref f) => panic!("No benchmark matching '{}' was found", f),
250                None => return Ok(()),
251            }
252        }
253
254        // Get a bit that tells us when we've been killed
255        let kill_bit = setup_kill_bit();
256
257        // Continuously run the first matched bench we see
258        let mut filtered_benches = filtered_benches;
259        let bench = filtered_benches.remove(0);
260
261        // If a seed was specified for this bench, use it. Otherwise, use a random seed
262        let seed = bench.seed.unwrap_or_else(CtBencher::rand_seed);
263        cb.seed_with(seed);
264        callback(BSeed(seed, bench.name))?;
265
266        loop {
267            callback(BWait(bench.name))?;
268            let msg = run_bench_with_bencher(&bench.name, bench.benchfn, &mut cb);
269            callback(BResult(msg))?;
270
271            // Check if the program has been killed. If so, exit
272            if kill_bit.load(atomic::Ordering::SeqCst) {
273                process::exit(0);
274            }
275        }
276    } else {
277        callback(BBegin(filtered_names))?;
278
279        // Run different benches
280        for bench in filtered_benches {
281            // Clear the data out from the previous bench, but keep the CSV file open
282            cb.clear_data();
283
284            // If a seed was specified for this bench, use it. Otherwise, use a random seed
285            let seed = bench.seed.unwrap_or_else(CtBencher::rand_seed);
286            cb.seed_with(seed);
287            callback(BSeed(seed, bench.name))?;
288
289            callback(BWait(bench.name))?;
290            let msg = run_bench_with_bencher(&bench.name, bench.benchfn, &mut cb);
291            callback(BResult(msg))?;
292        }
293        Ok(())
294    }
295}
296
297fn run_bench_with_bencher(name: &BenchName, benchfn: BenchFn, cb: &mut CtBencher) -> MonitorMsg {
298    let summ = cb.go(benchfn);
299
300    // Write the runtime samples out
301    let samples_iter = cb.samples.0.iter().zip(cb.samples.1.iter());
302    if let Some(f) = cb.file_out.as_mut() {
303        for (x, y) in samples_iter {
304            write!(f, "\n{},0,{}", name.0, x).expect("Error writing data to file");
305            write!(f, "\n{},0,{}", name.0, y).expect("Error writing data to file");
306        }
307    };
308
309    (*name, summ)
310}
311
312fn filter_benches(filter: &Option<String>, bs: Vec<BenchMetadata>) -> Vec<BenchMetadata> {
313    let mut filtered = bs;
314
315    // Remove benches that don't match the filter
316    filtered = match *filter {
317        None => filtered,
318        Some(ref filter) => filtered
319            .into_iter()
320            .filter(|b| b.name.0.contains(&filter[..]))
321            .collect(),
322    };
323
324    // Sort them alphabetically
325    filtered.sort_by(|b1, b2| b1.name.0.cmp(&b2.name.0));
326
327    filtered
328}
329
330// NOTE: We don't have a proper black box in stable Rust. This is a workaround implementation,
331// that may have a too big performance overhead, depending on operation, or it may fail to
332// properly avoid having code optimized out. It is good enough that it is used by default.
333//
334// A function that is opaque to the optimizer, to allow benchmarks to pretend to use outputs to
335// assist in avoiding dead-code elimination.
336#[cfg(not(feature = "core-hint-black-box"))]
337fn black_box<T>(dummy: T) -> T {
338    unsafe {
339        let ret = ::std::ptr::read_volatile(&dummy);
340        ::std::mem::forget(dummy);
341        ret
342    }
343}
344
345#[cfg(feature = "core-hint-black-box")]
346#[inline]
347fn black_box<T>(dummy: T) -> T {
348    ::core::hint::black_box(dummy)
349}
350
351/// Specifies the distribution that a particular run belongs to
352#[derive(Copy, Clone)]
353pub enum Class {
354    Left,
355    Right,
356}
357
358/// Used for timing single operations at a time
359#[derive(Default)]
360pub struct CtRunner {
361    // Runtimes of left and right distributions in nanoseconds
362    runtimes: (Vec<u64>, Vec<u64>),
363}
364
365impl CtRunner {
366    /// Runs and times a single operation whose constant-timeness is in question
367    pub fn run_one<T, F>(&mut self, class: Class, f: F)
368    where
369        F: Fn() -> T,
370    {
371        let start = Instant::now();
372        black_box(f());
373        let end = Instant::now();
374
375        let runtime = {
376            let dur = end.duration_since(start);
377            dur.as_secs() * 1_000_000_000 + u64::from(dur.subsec_nanos())
378        };
379
380        match class {
381            Class::Left => self.runtimes.0.push(runtime),
382            Class::Right => self.runtimes.1.push(runtime),
383        }
384    }
385}