Skip to content

Commit 51e1d7c

Browse files
committed
Allow benchmarking just one particular compilation/instantiation/execution phase
This avoids recompiling the Wasm module many times when we are only interested in benchmarking instantiation or execution. Note that it is still compiled once per process, but you can compile it exactly once if you do `--processes 1`.
1 parent dccb292 commit 51e1d7c

File tree

4 files changed

+158
-53
lines changed

4 files changed

+158
-53
lines changed

crates/cli/src/benchmark.rs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use sightglass_data::{Format, Measurement, Phase};
55
use sightglass_recorder::bench_api::Engine;
66
use sightglass_recorder::cpu_affinity::bind_to_single_core;
77
use sightglass_recorder::measure::Measurements;
8-
use sightglass_recorder::{bench_api::BenchApi, benchmark::benchmark, measure::MeasureType};
8+
use sightglass_recorder::{bench_api::BenchApi, benchmark, measure::MeasureType};
99
use std::{
1010
fs,
1111
io::{self, BufWriter, Write},
@@ -106,9 +106,10 @@ pub struct BenchmarkCommand {
106106
#[structopt(short("d"), long("working-dir"), parse(from_os_str))]
107107
working_dir: Option<PathBuf>,
108108

109-
/// Stop measuring after the given phase (compilation/instantiation/execution).
110-
#[structopt(long("stop-after"))]
111-
stop_after_phase: Option<Phase>,
109+
/// Benchmark only the given phase (compilation, instantiation, or
110+
/// execution). Benchmarks all phases if omitted.
111+
#[structopt(long("benchmark-phase"))]
112+
benchmark_phase: Option<Phase>,
112113

113114
/// The significance level for confidence intervals. Typical values are 0.01
114115
/// and 0.05, which correspond to 99% and 95% confidence respectively. This
@@ -195,6 +196,8 @@ impl BenchmarkCommand {
195196
let mut measurements = Measurements::new(this_arch(), engine, wasm_file);
196197
let mut measure = self.measure.build();
197198

199+
// Create the bench API engine and cache it for reuse across all
200+
// iterations of this benchmark.
198201
let engine = Engine::new(
199202
&mut bench_api,
200203
&working_dir,
@@ -207,35 +210,68 @@ impl BenchmarkCommand {
207210
);
208211
let mut engine = Some(engine);
209212

213+
// And if we are benchmarking just a post-compilation phase,
214+
// then eagerly compile the Wasm module for reuse.
215+
let mut module = None;
216+
if let Some(Phase::Instantiation | Phase::Execution) = self.benchmark_phase {
217+
module = Some(engine.take().unwrap().compile(&bytes));
218+
}
219+
210220
// Run the benchmark (compilation, instantiation, and execution) several times in
211221
// this process.
212222
for _ in 0..self.iterations_per_process {
213-
let new_engine = benchmark(
214-
engine.take().unwrap(),
215-
&bytes,
216-
self.stop_after_phase.clone(),
217-
)?;
218-
engine = Some(new_engine);
223+
match self.benchmark_phase {
224+
None => {
225+
let new_engine = benchmark::all(engine.take().unwrap(), &bytes)?;
226+
engine = Some(new_engine);
227+
}
228+
Some(Phase::Compilation) => {
229+
let new_engine =
230+
benchmark::compilation(engine.take().unwrap(), &bytes)?;
231+
engine = Some(new_engine);
232+
}
233+
Some(Phase::Instantiation) => {
234+
let new_module = benchmark::instantiation(module.take().unwrap())?;
235+
module = Some(new_module);
236+
}
237+
Some(Phase::Execution) => {
238+
let new_module = benchmark::execution(module.take().unwrap())?;
239+
module = Some(new_module);
240+
}
241+
}
219242

220243
self.check_output(Path::new(wasm_file), stdout, stderr)?;
221-
engine.as_mut().unwrap().measurements().next_iteration();
244+
engine
245+
.as_mut()
246+
.map(|e| e.measurements())
247+
.or_else(|| module.as_mut().map(|m| m.measurements()))
248+
.unwrap()
249+
.next_iteration();
222250
}
223251

224-
drop(engine);
252+
drop((engine, module));
225253
all_measurements.extend(measurements.finish());
226254
}
227255
}
228256

257+
// If we are only benchmarking one phase then filter out any
258+
// measurements for other phases. These get included because we have to
259+
// compile at least once to measure instantiation, for example.
260+
if let Some(phase) = self.benchmark_phase {
261+
all_measurements.retain(|m| m.phase == phase);
262+
}
263+
229264
self.write_results(&all_measurements, &mut output_file)?;
230265
Ok(())
231266
}
232267

233268
/// Assert that our actual `stdout` and `stderr` match our expectations.
234269
fn check_output(&self, wasm_file: &Path, stdout: &Path, stderr: &Path) -> Result<()> {
235-
// If we aren't going through all phases and executing the Wasm, then we
236-
// won't have any actual output to check.
237-
if self.stop_after_phase.is_some() {
238-
return Ok(());
270+
match self.benchmark_phase {
271+
None | Some(Phase::Execution) => {}
272+
// If we aren't executing the Wasm, then we won't have any actual
273+
// output to check.
274+
Some(Phase::Compilation | Phase::Instantiation) => return Ok(()),
239275
}
240276

241277
let wasm_file_dir: PathBuf = if let Some(dir) = wasm_file.parent() {
@@ -328,8 +364,8 @@ impl BenchmarkCommand {
328364
command.env("WASM_BENCH_USE_SMALL_WORKLOAD", "1");
329365
}
330366

331-
if let Some(phase) = self.stop_after_phase {
332-
command.arg("--stop-after").arg(phase.to_string());
367+
if let Some(phase) = self.benchmark_phase {
368+
command.arg("--benchmark-phase").arg(phase.to_string());
333369
}
334370

335371
if let Some(flags) = &self.engine_flags {

crates/cli/tests/all/benchmark.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ use sightglass_data::Measurement;
55
use std::path::PathBuf;
66

77
#[test]
8-
fn benchmark_stop_after_compilation() {
8+
fn benchmark_phase_compilation() {
99
sightglass_cli_benchmark()
1010
.arg("--raw")
1111
.arg("--processes")
1212
.arg("2")
1313
.arg("--iterations-per-process")
1414
.arg("1")
15-
.arg("--stop-after")
15+
.arg("--benchmark-phase")
1616
.arg("compilation")
1717
.arg(benchmark("noop"))
1818
.assert()
@@ -25,25 +25,47 @@ fn benchmark_stop_after_compilation() {
2525
}
2626

2727
#[test]
28-
fn benchmark_stop_after_instantiation() {
28+
fn benchmark_phase_instantiation() {
2929
sightglass_cli_benchmark()
3030
.arg("--raw")
3131
.arg("--processes")
3232
.arg("2")
3333
.arg("--iterations-per-process")
3434
.arg("1")
35-
.arg("--stop-after")
35+
.arg("--benchmark-phase")
3636
.arg("instantiation")
3737
.arg(benchmark("noop"))
3838
.assert()
3939
.success()
4040
.stdout(
4141
predicate::str::contains("Compilation")
42+
.not()
4243
.and(predicate::str::contains("Instantiation"))
4344
.and(predicate::str::contains("Execution").not()),
4445
);
4546
}
4647

48+
#[test]
49+
fn benchmark_phase_execution() {
50+
sightglass_cli_benchmark()
51+
.arg("--raw")
52+
.arg("--processes")
53+
.arg("2")
54+
.arg("--iterations-per-process")
55+
.arg("1")
56+
.arg("--benchmark-phase")
57+
.arg("execution")
58+
.arg(benchmark("noop"))
59+
.assert()
60+
.success()
61+
.stdout(
62+
predicate::str::contains("Compilation")
63+
.not()
64+
.and(predicate::str::contains("Instantiation").not())
65+
.and(predicate::str::contains("Execution")),
66+
);
67+
}
68+
4769
#[test]
4870
fn benchmark_json() {
4971
let assert = sightglass_cli_benchmark()

crates/recorder/src/bench_api.rs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ pub struct Engine<'a, 'b, 'c, M> {
7474
engine: *mut c_void,
7575
}
7676

77-
impl<'a, 'b, 'c, M> Engine<'a, 'b, 'c, M>
78-
where
79-
M: Measure,
80-
{
77+
impl<'a, 'b, 'c, M> Engine<'a, 'b, 'c, M> {
8178
/// Construct a new engine from the given `BenchApi`.
8279
// NB: take a mutable reference to the `BenchApi` so that no one else can
8380
// call its API methods out of order.
@@ -90,7 +87,10 @@ where
9087
measurements: &'a mut Measurements<'c>,
9188
measure: &'a mut M,
9289
execution_flags: Option<&'a str>,
93-
) -> Self {
90+
) -> Self
91+
where
92+
M: Measure,
93+
{
9494
let working_dir = working_dir.display().to_string();
9595
let stdout_path = stdout_path.display().to_string();
9696
let stderr_path = stderr_path.display().to_string();
@@ -148,31 +148,43 @@ where
148148
}
149149

150150
/// Bench API callback for the start of compilation.
151-
extern "C" fn compilation_start(data: *mut u8) {
151+
extern "C" fn compilation_start(data: *mut u8)
152+
where
153+
M: Measure,
154+
{
152155
log::debug!("Starting compilation measurement");
153156
let data = data as *mut (*mut M, *mut Measurements<'b>);
154157
let measure = unsafe { data.as_mut().unwrap().0.as_mut().unwrap() };
155158
measure.start(Phase::Compilation);
156159
}
157160

158161
/// Bench API callback for the start of instantiation.
159-
extern "C" fn instantiation_start(data: *mut u8) {
162+
extern "C" fn instantiation_start(data: *mut u8)
163+
where
164+
M: Measure,
165+
{
160166
log::debug!("Starting instantiation measurement");
161167
let data = data as *mut (*mut M, *mut Measurements<'b>);
162168
let measure = unsafe { data.as_mut().unwrap().0.as_mut().unwrap() };
163169
measure.start(Phase::Instantiation);
164170
}
165171

166172
/// Bench API callback for the start of execution.
167-
extern "C" fn execution_start(data: *mut u8) {
173+
extern "C" fn execution_start(data: *mut u8)
174+
where
175+
M: Measure,
176+
{
168177
log::debug!("Starting execution measurement");
169178
let data = data as *mut (*mut M, *mut Measurements<'b>);
170179
let measure = unsafe { data.as_mut().unwrap().0.as_mut().unwrap() };
171180
measure.start(Phase::Execution);
172181
}
173182

174183
/// Bench API callback for the end of compilation.
175-
extern "C" fn compilation_end(data: *mut u8) {
184+
extern "C" fn compilation_end(data: *mut u8)
185+
where
186+
M: Measure,
187+
{
176188
let data = data as *mut (*mut M, *mut Measurements<'b>);
177189
let (measure, measurements) = unsafe {
178190
let data = data.as_mut().unwrap();
@@ -183,7 +195,10 @@ where
183195
}
184196

185197
/// Bench API callback for the end of instantiation.
186-
extern "C" fn instantiation_end(data: *mut u8) {
198+
extern "C" fn instantiation_end(data: *mut u8)
199+
where
200+
M: Measure,
201+
{
187202
let data = data as *mut (*mut M, *mut Measurements<'b>);
188203
let (measure, measurements) = unsafe {
189204
let data = data.as_mut().unwrap();
@@ -194,7 +209,10 @@ where
194209
}
195210

196211
/// Bench API callback for the end of execution.
197-
extern "C" fn execution_end(data: *mut u8) {
212+
extern "C" fn execution_end(data: *mut u8)
213+
where
214+
M: Measure,
215+
{
198216
let data = data as *mut (*mut M, *mut Measurements<'b>);
199217
let (measure, measurements) = unsafe {
200218
let data = data.as_mut().unwrap();
@@ -225,6 +243,11 @@ impl<'a, 'b, 'c, M> Module<'a, 'b, 'c, M> {
225243
self.engine
226244
}
227245

246+
/// Get this engine's measurements.
247+
pub fn measurements(&mut self) -> &mut Measurements<'c> {
248+
self.engine.measurements()
249+
}
250+
228251
/// Instantiate this module, returning the resulting `Instance`.
229252
pub fn instantiate(self) -> Instance<'a, 'b, 'c, M> {
230253
let result = unsafe { (self.engine.bench_api.wasm_bench_instantiate)(self.engine.engine) };

crates/recorder/src/benchmark.rs

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
use crate::bench_api::Engine;
1+
use crate::bench_api::{Engine, Module};
22
use crate::measure::Measure;
33
use anyhow::Result;
44
use log::info;
5-
use sightglass_data::Phase;
6-
7-
/// Measure various phases of a Wasm module's lifetime.
8-
///
9-
/// Provide paths to files created for logging the Wasm's `stdout` and `stderr`
10-
/// and (optionally) a file read and piped into the Wasm execution as `stdin`.
11-
///
12-
/// Optionally stop after the given `stop_after_phase`, rather than running all
13-
/// phases.
14-
pub fn benchmark<'a, 'b, 'c, M: Measure>(
5+
6+
/// Measure all phases of a Wasm module's lifetime: compilation, instantiation,
7+
/// and execution.
8+
pub fn all<'a, 'b, 'c, M: Measure>(
159
engine: Engine<'a, 'b, 'c, M>,
1610
wasm_bytes: &[u8],
17-
stop_after_phase: Option<Phase>,
1811
) -> Result<Engine<'a, 'b, 'c, M>> {
1912
#[cfg(target_os = "linux")]
2013
info!("Benchmark scheduled on CPU: {}", unsafe {
@@ -25,20 +18,51 @@ pub fn benchmark<'a, 'b, 'c, M: Measure>(
2518
let module = engine.compile(wasm_bytes);
2619
info!("Compiled successfully");
2720

28-
if stop_after_phase == Some(Phase::Compilation) {
29-
return Ok(module.into_engine());
30-
}
31-
3221
// Measure the module instantiation.
3322
let instance = module.instantiate();
3423
info!("Instantiated successfully");
3524

36-
if stop_after_phase == Some(Phase::Instantiation) {
37-
return Ok(instance.into_module().into_engine());
38-
}
39-
4025
let module = instance.execute();
4126
info!("Executed successfully");
4227

4328
Ok(module.into_engine())
4429
}
30+
31+
/// Measure just the compilation phase of a Wasm module's lifetime.
32+
pub fn compilation<'a, 'b, 'c, M: Measure>(
33+
engine: Engine<'a, 'b, 'c, M>,
34+
wasm_bytes: &[u8],
35+
) -> Result<Engine<'a, 'b, 'c, M>> {
36+
#[cfg(target_os = "linux")]
37+
info!("Benchmark scheduled on CPU: {}", unsafe {
38+
libc::sched_getcpu()
39+
});
40+
41+
let module = engine.compile(wasm_bytes);
42+
info!("Compiled successfully");
43+
44+
Ok(module.into_engine())
45+
}
46+
47+
/// Measure just the instantiation phase of a Wasm module's lifetime.
48+
pub fn instantiation<'a, 'b, 'c, M: Measure>(
49+
module: Module<'a, 'b, 'c, M>,
50+
) -> Result<Module<'a, 'b, 'c, M>> {
51+
let instance = module.instantiate();
52+
info!("Instantiated successfully");
53+
54+
Ok(instance.into_module())
55+
}
56+
57+
/// Measure just the execution phase of a Wasm module's lifetime.
58+
pub fn execution<'a, 'b, 'c, M: Measure>(
59+
module: Module<'a, 'b, 'c, M>,
60+
) -> Result<Module<'a, 'b, 'c, M>> {
61+
let instance = module.instantiate();
62+
info!("Instantiated successfully");
63+
64+
let module = instance.execute();
65+
info!("Executed successfully");
66+
67+
Ok(module)
68+
}

0 commit comments

Comments
 (0)