molt_shell/
bench.rs

1//! Molt Benchmark Harness
2//!
3//! A Molt benchmark script is a Molt script containing benchmarks of Molt code.  Each
4//! benchmark is a call of the Molt `benchmark` command provided by the
5//! `molt_shell::bench` module.  The benchmarks are executed in the context of the
6//! the application's `molt::Interp` (and so can benchmark application-specific commands).
7//!
8//! The harness executes each benchmark many times and retains the average run-time
9//! in microseconds. The `molt-app` tool provides access to the test harness for a
10//! standard Molt interpreter.
11//!
12//! See the Molt Book (or the Molt benchmark suite) for how to write
13//! benchmarks and examples of benchmark scripts.
14
15use molt::check_args;
16use molt::molt_ok;
17use molt::ContextID;
18use molt::Interp;
19use molt::MoltInt;
20use molt::MoltResult;
21use molt::Value;
22use std::env;
23use std::fs;
24use std::path::PathBuf;
25
26/// Executes the Molt benchmark harness, given the command-line arguments,
27/// in the context of the given interpreter.
28///
29/// The first element of the `args` array must be the name of the benchmark script
30/// to execute.  The remaining elements are benchmark options.  To see the list
31/// of options, see The Molt Book or execute this function with an empty argument
32/// list.
33///
34/// See [`molt::interp`](../molt/interp/index.html) for details on how to configure and
35/// add commands to a Molt interpreter.
36///
37/// # Example
38///
39/// ```
40/// use molt::Interp;
41/// use std::env;
42///
43/// // FIRST, get the command line arguments.
44/// let args: Vec<String> = env::args().collect();
45///
46/// // NEXT, create and initialize the interpreter.
47/// let mut interp = Interp::new();
48///
49/// // NOTE: commands can be added to the interpreter here.
50///
51/// // NEXT, evaluate the file, if any.
52/// if args.len() > 1 {
53///     molt_shell::benchmark(&mut interp, &args[1..]);
54/// } else {
55///     eprintln!("Usage: mybench *filename.tcl");
56/// }
57/// ```
58pub fn benchmark(interp: &mut Interp, args: &[String]) {
59    // FIRST, get the script file name
60    if args.is_empty() {
61        eprintln!("Missing benchmark script.");
62        write_usage();
63        return;
64    }
65
66    // NEXT, parse any options.
67    let mut output_csv = false;
68
69    let mut iter = args[1..].iter();
70    loop {
71        let opt = iter.next();
72        if opt.is_none() {
73            break;
74        }
75
76        let opt = opt.unwrap();
77
78        match opt.as_ref() {
79            "-csv" => {
80                output_csv = true;
81            }
82            _ => {
83                eprintln!("Unknown option: \"{}\"", opt);
84                write_usage();
85                return;
86            }
87        }
88    }
89
90    // NEXT, get the parent folder from the path, if any.  We'll cd into the parent so
91    // the `source` command can find scripts there.
92    let path = PathBuf::from(&args[0]);
93
94    // NEXT, initialize the benchmark context.
95    let context_id = interp.save_context(Context::new());
96
97    // NEXT, install the test commands into the interpreter.
98    interp.add_command("ident", cmd_ident);
99    interp.add_context_command("measure", measure_cmd, context_id);
100    interp.add_command("ok", cmd_ok);
101
102    // NEXT, load the benchmark Tcl library
103    if let Err(exception) = interp.eval(include_str!("bench.tcl")) {
104        panic!(
105            "Error in benchmark Tcl library: {}",
106            exception.value().as_str()
107        );
108    }
109
110    // NEXT, execute the script.
111    match fs::read_to_string(&args[0]) {
112        Ok(script) => {
113            if let Some(parent) = path.parent() {
114                let _ = env::set_current_dir(parent);
115            }
116
117            match interp.eval(&script) {
118                Ok(_) => (),
119                Err(exception) => {
120                    eprintln!("{}", exception.value());
121                    std::process::exit(1);
122                }
123            }
124        }
125        Err(e) => println!("{}", e),
126    }
127
128    // NEXT, output the test results:
129    let ctx = interp.context::<Context>(context_id);
130
131    if output_csv {
132        write_csv(ctx);
133    } else {
134        write_formatted_text(ctx);
135    }
136}
137
138fn write_csv(ctx: &Context) {
139    println!("\"benchmark\",\"description\",\"nanos\",\"norm\"");
140
141    let baseline = ctx.baseline();
142
143    for record in &ctx.measurements {
144        println!(
145            "\"{}\",\"{}\",{},{}",
146            strip_quotes(&record.name),
147            strip_quotes(&record.description),
148            record.nanos,
149            record.nanos as f64 / (baseline as f64),
150        );
151    }
152}
153
154fn strip_quotes(string: &str) -> String {
155    let out: String = string
156        .chars()
157        .map(|ch| if ch == '\"' { '\'' } else { ch })
158        .collect();
159    out
160}
161
162fn write_formatted_text(ctx: &Context) {
163    write_version();
164    println!();
165    println!("{:>8} {:>8} -- Benchmark", "Nanos", "Norm");
166
167    let baseline = ctx.baseline();
168
169    for record in &ctx.measurements {
170        println!(
171            "{:>8} {:>8.2} -- {} {}",
172            record.nanos,
173            record.nanos as f64 / (baseline as f64),
174            record.name,
175            record.description
176        );
177    }
178}
179
180fn write_version() {
181    println!("Molt {} -- Benchmark", env!("CARGO_PKG_VERSION"));
182}
183
184fn write_usage() {
185    write_version();
186    println!();
187    println!("Usage: molt bench filename.tcl [-csv]");
188}
189
190struct Context {
191    // The baseline, in microseconds
192    baseline: Option<MoltInt>,
193
194    // The list of measurements.
195    measurements: Vec<Measurement>,
196}
197
198impl Context {
199    fn new() -> Self {
200        Self {
201            baseline: None,
202            measurements: Vec::new(),
203        }
204    }
205
206    fn baseline(&self) -> MoltInt {
207        self.baseline.unwrap_or(1)
208    }
209}
210
211struct Measurement {
212    // The measurement's symbolic name
213    name: String,
214
215    // The measurement's human-readable description
216    description: String,
217
218    // The average number of nanoseconds per measured iteration
219    nanos: MoltInt,
220}
221
222/// # measure *name* *description* *micros*
223///
224/// Records a benchmark measurement.
225fn measure_cmd(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
226    molt::check_args(1, argv, 4, 4, "name description nanos")?;
227
228    // FIRST, get the arguments
229    let name = argv[1].to_string();
230    let description = argv[2].to_string();
231    let nanos = argv[3].as_int()?;
232
233    // NEXT, get the test context
234    let ctx = interp.context::<Context>(context_id);
235
236    if ctx.baseline.is_none() {
237        ctx.baseline = Some(nanos);
238    }
239
240    let record = Measurement {
241        name,
242        description,
243        nanos,
244    };
245
246    ctx.measurements.push(record);
247
248    molt_ok!()
249}
250
251/// # ident value
252///
253/// Returns its argument.
254fn cmd_ident(_interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
255    check_args(1, argv, 2, 2, "value")?;
256
257    molt_ok!(argv[1].clone())
258}
259
260/// # ok ...
261///
262/// Takes any number of arguments, and returns "".
263fn cmd_ok(_interp: &mut Interp, _: ContextID, _argv: &[Value]) -> MoltResult {
264    molt_ok!()
265}