use std::process::Command;
use std::time::Instant;
use crate::ast::TopLevel;
use crate::bench::manifest::{BenchTarget, Manifest};
use crate::bench::report::{BackendInfo, BenchReport, HostInfo, IterationStats, ScenarioMetadata};
use crate::ir::{PipelineConfig, PipelineStage, TypecheckMode};
use crate::nan_value::Arena;
use crate::source::parse_source;
use crate::vm;
#[derive(Debug)]
pub enum RunError {
Read(String),
Parse(String),
Typecheck(String),
Compile(String),
Runtime(String),
Setup(String),
}
impl std::fmt::Display for RunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read(m)
| Self::Parse(m)
| Self::Typecheck(m)
| Self::Compile(m)
| Self::Runtime(m)
| Self::Setup(m) => f.write_str(m),
}
}
}
pub fn run_scenario(manifest: &Manifest, target: BenchTarget) -> Result<BenchReport, RunError> {
match target {
BenchTarget::Vm => run_vm(manifest),
BenchTarget::WasmLocal => run_wasm_local(manifest),
BenchTarget::Rust => run_rust(manifest),
}
}
fn run_vm(manifest: &Manifest) -> Result<BenchReport, RunError> {
let entry_str = manifest.entry.to_string_lossy().into_owned();
let module_root = manifest
.entry
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let source = std::fs::read_to_string(&manifest.entry)
.map_err(|e| RunError::Read(format!("{}: {}", entry_str, e)))?;
let mut items: Vec<TopLevel> = parse_source(&source).map_err(RunError::Parse)?;
let passes_applied = std::cell::RefCell::new(Vec::<String>::new());
let pipeline_result = crate::ir::pipeline::run(
&mut items,
PipelineConfig {
typecheck: Some(TypecheckMode::Full {
base_dir: Some(&module_root),
}),
on_after_pass: Some(Box::new(|stage: PipelineStage, _| {
passes_applied.borrow_mut().push(stage.name().to_string());
})),
..Default::default()
},
);
let tc_result = pipeline_result.typecheck.expect("typecheck was requested");
if !tc_result.errors.is_empty() {
let msg = tc_result
.errors
.iter()
.map(|err| format!("error[{}:{}]: {}", err.line, err.col, err.message))
.collect::<Vec<_>>()
.join("\n");
return Err(RunError::Typecheck(msg));
}
let mut arena = Arena::new();
vm::register_service_types(&mut arena);
let (code, globals) = vm::compile_program_with_modules(
&items,
&mut arena,
Some(&module_root),
&entry_str,
pipeline_result.analysis.as_ref(),
)
.map_err(|e| RunError::Compile(format!("VM compile: {}", e)))?;
let mut samples: Vec<f64> = Vec::with_capacity(manifest.iterations);
for _ in 0..manifest.warmup {
run_one_vm(&code, &globals, &arena, &manifest.args)?;
}
for _ in 0..manifest.iterations {
let t = Instant::now();
run_one_vm(&code, &globals, &arena, &manifest.args)?;
samples.push(t.elapsed().as_secs_f64() * 1000.0);
}
Ok(build_report(
manifest,
BenchTarget::Vm,
&samples,
passes_applied.into_inner(),
))
}
fn run_one_vm(
code: &vm::CodeStore,
globals: &[crate::nan_value::NanValue],
arena: &Arena,
args: &[String],
) -> Result<(), RunError> {
let mut machine = vm::VM::new(code.clone(), globals.to_vec(), arena.clone());
machine.set_silent_console(true);
machine.set_cli_args(args.to_vec());
machine
.run()
.map_err(|e| RunError::Runtime(format!("{}", e)))?;
Ok(())
}
#[cfg(feature = "wasm")]
fn run_wasm_local(manifest: &Manifest) -> Result<BenchReport, RunError> {
use wasmtime::{Caller, Engine, Linker, Module, Store};
let temp = tempfile::tempdir()
.map_err(|e| RunError::Setup(format!("create wasm bench tempdir: {}", e)))?;
let out_dir = temp.path().join("out");
let aver_bin = std::env::current_exe()
.map_err(|e| RunError::Setup(format!("locate current aver binary: {}", e)))?;
let status = Command::new(&aver_bin)
.arg("compile")
.arg(&manifest.entry)
.arg("--target")
.arg("wasm")
.arg("--bridge")
.arg("wasip1")
.arg("--name")
.arg(&manifest.name)
.arg("-o")
.arg(&out_dir)
.status()
.map_err(|e| RunError::Setup(format!("spawn aver compile --target wasm: {}", e)))?;
if !status.success() {
return Err(RunError::Compile(format!(
"aver compile --target wasm exited with {}",
status
)));
}
let wasm_path = out_dir.join(format!("{}.wasm", manifest.name));
let bytes = std::fs::read(&wasm_path)
.map_err(|e| RunError::Setup(format!("read {}: {}", wasm_path.display(), e)))?;
let engine = Engine::default();
let module = Module::new(&engine, &bytes)
.map_err(|e| RunError::Setup(format!("wasmtime compile module: {}", e)))?;
let run_one = |module: &Module, engine: &Engine| -> Result<(), RunError> {
let mut store = Store::new(engine, ());
let mut linker = Linker::new(engine);
let ws = "wasi_snapshot_preview1";
linker
.func_wrap(
ws,
"fd_write",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32, _: i32| -> i32 { 0 },
)
.and_then(|l| {
l.func_wrap(
ws,
"fd_read",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| l.func_wrap(ws, "fd_close", |_: Caller<'_, ()>, _: i32| -> i32 { 0 }))
.and_then(|l| {
l.func_wrap(
ws,
"fd_seek",
|_: Caller<'_, ()>, _: i32, _: i64, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"fd_fdstat_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"fd_prestat_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 8 },
)
}) .and_then(|l| {
l.func_wrap(
ws,
"fd_prestat_dir_name",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_open",
|_: Caller<'_, ()>,
_: i32,
_: i32,
_: i32,
_: i32,
_: i32,
_: i64,
_: i64,
_: i32,
_: i32|
-> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_filestat_get",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_remove_directory",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_unlink_file",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_create_directory",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"path_rename",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32, _: i32, _: i32, _: i32| -> i32 {
0
},
)
})
.and_then(|l| {
l.func_wrap(
ws,
"fd_filestat_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"fd_readdir",
|_: Caller<'_, ()>, _: i32, _: i32, _: i32, _: i64, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"args_sizes_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(ws, "args_get", |_: Caller<'_, ()>, _: i32, _: i32| -> i32 {
0
})
})
.and_then(|l| {
l.func_wrap(
ws,
"environ_sizes_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"environ_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"clock_time_get",
|_: Caller<'_, ()>, _: i32, _: i64, _: i32| -> i32 { 0 },
)
})
.and_then(|l| {
l.func_wrap(
ws,
"random_get",
|_: Caller<'_, ()>, _: i32, _: i32| -> i32 { 0 },
)
})
.and_then(|l| l.func_wrap(ws, "proc_exit", |_: Caller<'_, ()>, _: i32| {}))
.and_then(|l| l.func_wrap(ws, "sched_yield", |_: Caller<'_, ()>| -> i32 { 0 }))
.map_err(|e| RunError::Setup(format!("stub wasi imports: {}", e)))?;
let instance = linker
.instantiate(&mut store, module)
.map_err(|e| RunError::Runtime(format!("instantiate: {}", e)))?;
let start = instance
.get_typed_func::<(), ()>(&mut store, "_start")
.map_err(|e| RunError::Runtime(format!("_start export: {}", e)))?;
start
.call(&mut store, ())
.map_err(|e| RunError::Runtime(format!("invoke _start: {}", e)))?;
Ok(())
};
let mut samples: Vec<f64> = Vec::with_capacity(manifest.iterations);
for _ in 0..manifest.warmup {
run_one(&module, &engine)?;
}
for _ in 0..manifest.iterations {
let t = Instant::now();
run_one(&module, &engine)?;
samples.push(t.elapsed().as_secs_f64() * 1000.0);
}
let passes = canonical_passes();
Ok(build_report(
manifest,
BenchTarget::WasmLocal,
&samples,
passes,
))
}
#[cfg(not(feature = "wasm"))]
fn run_wasm_local(_manifest: &Manifest) -> Result<BenchReport, RunError> {
Err(RunError::Setup(
"wasm-local target requires the `wasm` feature; rebuild with `cargo build --features wasm`"
.to_string(),
))
}
fn run_rust(manifest: &Manifest) -> Result<BenchReport, RunError> {
let temp = tempfile::tempdir()
.map_err(|e| RunError::Setup(format!("create rust bench tempdir: {}", e)))?;
let out_dir = temp.path().join("out");
let aver_bin = std::env::current_exe()
.map_err(|e| RunError::Setup(format!("locate current aver binary: {}", e)))?;
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(std::path::PathBuf::from)
.ok();
let mut compile_cmd = Command::new(&aver_bin);
compile_cmd
.arg("compile")
.arg(&manifest.entry)
.arg("--name")
.arg(&manifest.name)
.arg("-o")
.arg(&out_dir);
if let Some(root) = manifest_dir.as_ref() {
compile_cmd.env("AVER_RUNTIME_PATH", root.join("aver-rt"));
}
let status = compile_cmd
.status()
.map_err(|e| RunError::Setup(format!("spawn aver compile --target rust: {}", e)))?;
if !status.success() {
return Err(RunError::Compile(format!(
"aver compile (rust) exited with {}",
status
)));
}
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(&out_dir)
.status()
.map_err(|e| RunError::Setup(format!("spawn cargo build: {}", e)))?;
if !status.success() {
return Err(RunError::Compile(format!(
"cargo build (rust) exited with {}",
status
)));
}
let binary = out_dir.join("target/release").join(&manifest.name);
if !binary.exists() {
return Err(RunError::Setup(format!(
"rust target binary not found at {}",
binary.display()
)));
}
let run_one = |bin: &std::path::Path, args: &[String]| -> Result<(), RunError> {
let output = Command::new(bin)
.args(args)
.output()
.map_err(|e| RunError::Runtime(format!("spawn {}: {}", bin.display(), e)))?;
if !output.status.success() {
return Err(RunError::Runtime(format!(
"{} exited with {}: {}",
bin.display(),
output.status,
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
};
let mut samples: Vec<f64> = Vec::with_capacity(manifest.iterations);
for _ in 0..manifest.warmup {
run_one(&binary, &manifest.args)?;
}
for _ in 0..manifest.iterations {
let t = Instant::now();
run_one(&binary, &manifest.args)?;
samples.push(t.elapsed().as_secs_f64() * 1000.0);
}
let passes = canonical_passes();
Ok(build_report(manifest, BenchTarget::Rust, &samples, passes))
}
fn canonical_passes() -> Vec<String> {
[
"tco",
"typecheck",
"interp_lower",
"buffer_build",
"resolve",
"last_use",
"analyze",
]
.iter()
.map(|s| s.to_string())
.collect()
}
fn build_report(
manifest: &Manifest,
target: BenchTarget,
samples: &[f64],
passes_applied: Vec<String>,
) -> BenchReport {
let stats = IterationStats::from_samples(samples);
BenchReport {
scenario: ScenarioMetadata {
name: manifest.name.clone(),
entry: manifest.entry.to_string_lossy().into_owned(),
target: target.name().to_string(),
iterations_count: manifest.iterations,
warmup_count: manifest.warmup,
},
backend: BackendInfo::for_target(target),
host: HostInfo::capture(),
iterations: stats,
response_bytes: None,
expected_match: None,
passes_applied,
compiler_visible_allocs: None,
}
}