From be50791ef57b89d49191b7b0c974fb80bf12d739 Mon Sep 17 00:00:00 2001 From: Doehyun Baek Date: Mon, 18 Dec 2023 17:33:35 +0900 Subject: [PATCH] port replay generation to rust --- .gitignore | 3 +- crates/Cargo.lock | 7 + crates/replay_gen/Cargo.toml | 1 + crates/replay_gen/src/jsgen.rs | 831 +++++++++++++++++++++++++++++++++ crates/replay_gen/src/lib.rs | 1 + crates/replay_gen/src/main.rs | 27 +- crates/replay_gen/src/trace.rs | 165 +++---- package.json | 3 +- src/benchmark.cts | 23 +- src/cli/options.cts | 7 +- src/instrumenter.cts | 2 +- tests/run-tests.cts | 32 +- 12 files changed, 984 insertions(+), 118 deletions(-) create mode 100644 crates/replay_gen/src/jsgen.rs diff --git a/.gitignore b/.gitignore index 25f9b1b0..3a559bde 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules/** .DS_Store performance.db.ndjson performance.ndjson -target/ \ No newline at end of file +target/ +benchmark*/ \ No newline at end of file diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 7c4df238..c89dc6db 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "bitflags" version = "1.3.2" @@ -61,6 +67,7 @@ dependencies = [ name = "replay_gen" version = "0.1.0" dependencies = [ + "anyhow", "tempfile", ] diff --git a/crates/replay_gen/Cargo.toml b/crates/replay_gen/Cargo.toml index 414fd790..26b36d01 100644 --- a/crates/replay_gen/Cargo.toml +++ b/crates/replay_gen/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.75" tempfile = "3.2.0" diff --git a/crates/replay_gen/src/jsgen.rs b/crates/replay_gen/src/jsgen.rs new file mode 100644 index 00000000..42790a52 --- /dev/null +++ b/crates/replay_gen/src/jsgen.rs @@ -0,0 +1,831 @@ +use std::{ + collections::{BTreeMap, HashMap}, + io::{Seek, SeekFrom}, +}; + +use crate::{ + jsgen::js::generate_javascript, + trace::{ + self, ExportCall, ImportCall, ImportFunc, ImportGlobal, ImportMemory, ImportTable, Trace, + ValType, WasmEvent, F64, + }, +}; + +pub struct Generator { + pub code: Replay, + state: State, +} + +pub struct Replay { + func_imports: BTreeMap, + mem_imports: BTreeMap, + table_imports: BTreeMap, + global_imports: BTreeMap, + calls: Vec<(String, Vec)>, + initialization: Vec, + modules: Vec, +} + +struct State { + import_call_stack: Vec, + last_func: i32, + global_scope: bool, + last_func_return: bool, + import_call_stack_function: Vec, +} + +#[derive(Clone)] +pub enum EventType { + Call, + TableCall, + Store, + MemGrow, + TableSet, + TableGrow, + GlobalGet, +} + +#[derive(Clone, Debug)] +pub struct Call { + name: String, + params: Vec, +} +#[derive(Clone, Debug)] +pub struct TableCall { + table_name: String, + funcidx: i32, + params: Vec, +} +#[derive(Clone, Debug)] +pub struct Store { + addr: i32, + data: Vec, + import: bool, + name: String, +} +#[derive(Clone, Debug)] +pub struct MemGrow { + amount: i32, + import: bool, + name: String, +} +#[derive(Clone, Debug)] +pub struct TableSet { + idx: i32, + func_import: bool, + func_name: String, + import: bool, + name: String, +} +#[derive(Clone, Debug)] +pub struct TableGrow { + idx: i32, + amount: i32, + import: bool, + name: String, +} +#[derive(Clone, Debug)] +pub struct GlobalGet { + value: F64, + big_int: bool, + import: bool, + name: String, +} + +#[derive(Clone, Debug)] +pub enum Event { + Call(Call), + TableCall(TableCall), + Store(Store), + MemGrow(MemGrow), + TableSet(TableSet), + TableGrow(TableGrow), + GlobalGet(GlobalGet), +} + +pub struct Import { + module: String, + name: String, +} + +#[derive(Clone, Debug)] +pub struct WriteResult { + results: Vec, + reps: i32, +} + +pub type Context = Vec; + +#[derive(Clone, Debug)] +pub struct Function { + pub module: String, + pub name: String, + bodys: Vec, + results: Vec, +} + +pub struct Memory { + pub module: String, + pub name: String, + pub initial: F64, + pub maximum: Option, +} + +pub struct Table { + pub module: String, + pub name: String, + // enum is better + pub element: String, + pub initial: F64, + pub maximum: Option, +} + +pub struct Global { + pub module: String, + pub name: String, + pub mutable: bool, + pub initial: F64, + pub value: ValType, +} + +impl Generator { + pub fn new() -> Self { + let mut func_imports = BTreeMap::new(); + func_imports.insert( + -1, + Function { + module: "wasm-r3".to_string(), + name: "initialization".to_string(), + bodys: vec![vec![]], + results: vec![], + }, + ); + Self { + code: Replay { + func_imports, + mem_imports: BTreeMap::new(), + table_imports: BTreeMap::new(), + global_imports: BTreeMap::new(), + calls: Vec::new(), + initialization: Vec::new(), + modules: Vec::new(), + }, + state: State { + import_call_stack: vec![-1], + last_func: -1, + global_scope: true, + last_func_return: false, + import_call_stack_function: Vec::new(), + }, + } + } + + pub fn generate_replay(&mut self, trace: &Trace) -> &Replay { + for event in trace.iter() { + self.consume_event(event); + } + &self.code + } + + fn consume_event(&mut self, event: &WasmEvent) { + match event { + WasmEvent::ExportCall(ExportCall { name, params }) => { + self.push_event(Event::Call(Call { + name: name.clone(), + params: params.clone(), + })); + } + WasmEvent::TableCall(trace::TableCall { + tablename, + funcidx, + params, + }) => { + self.push_event(Event::TableCall(TableCall { + table_name: tablename.clone(), + funcidx: *funcidx, + params: params.clone(), + })); + } + WasmEvent::ExportReturn => { + self.state.global_scope = true; + } + WasmEvent::ImportCall(ImportCall { idx, name }) => { + self.state.global_scope = false; + self.code + .func_imports + .get_mut(idx) + .unwrap() + .bodys + .push(vec![]); + self.state.import_call_stack.push(*idx); + self.state.last_func = *idx; + let value = self.code.func_imports.get(&idx).unwrap().clone(); + self.state.import_call_stack_function.push(*idx); + self.state.last_func_return = false; + } + WasmEvent::ImportReturn(trace::ImportReturn { idx, name, results }) => 'label: { + let current_fn_idx = self.state.import_call_stack_function.last().unwrap(); + let r = &mut self + .code + .func_imports + .get_mut(¤t_fn_idx) + .unwrap() + .results; + if !r.is_empty() { + let tmp = r.last().unwrap(); + if (!tmp.results.is_empty() && tmp.results[0] == results[0]) + || (tmp.results.is_empty() && results.is_empty()) + { + self.state.last_func_return = true; + r.last_mut().unwrap().reps += 1; + self.state.import_call_stack.pop(); + self.state.import_call_stack_function.pop(); + break 'label; + } + } + self.state.last_func_return = true; + r.push(WriteResult { + results: results.clone(), + reps: 1, + }); + self.state.import_call_stack.pop(); + self.state.import_call_stack_function.pop(); + } + WasmEvent::Load(trace::Load { + idx, + name, + offset, + data, + }) => { + self.push_event(Event::Store(Store { + import: self.code.mem_imports.contains_key(&idx), + name: name.clone(), + addr: *offset, + data: data.clone(), + })); + } + WasmEvent::MemGrow(trace::MemGrow { idx, name, amount }) => { + self.push_event(Event::MemGrow(MemGrow { + import: self.code.mem_imports.contains_key(idx), + name: name.clone(), + amount: *amount, + })); + } + WasmEvent::TableGet(trace::TableGet { + tableidx, + name, + idx, + funcidx, + funcname, + }) => { + self.push_event(Event::TableSet(TableSet { + import: self.code.table_imports.contains_key(&tableidx), + name: name.clone(), + idx: *idx, + func_import: self.code.func_imports.contains_key(funcidx), + func_name: funcname.clone(), + })); + } + WasmEvent::TableGrow(trace::TableGrow { idx, name, amount }) => { + self.push_event(Event::TableGrow(TableGrow { + import: self.code.table_imports.contains_key(idx), + name: name.clone(), + idx: *idx, + amount: *amount, + })); + } + WasmEvent::ImportMemory(ImportMemory { + idx, + module, + name, + initial, + maximum, + }) => { + self.add_module(module); + self.code.mem_imports.insert( + *idx, + Memory { + module: module.clone(), + name: name.clone(), + initial: *initial, + maximum: *maximum, + }, + ); + } + WasmEvent::GlobalGet(trace::GlobalGet { + idx, + name, + value, + valtype, + }) => { + self.push_event(Event::GlobalGet(GlobalGet { + import: self.code.global_imports.contains_key(&idx), + name: name.clone(), + value: *value, + big_int: *valtype == ValType::I64, + })); + } + WasmEvent::ImportTable(ImportTable { + idx, + module, + name, + initial, + maximum, + element, + }) => { + self.add_module(module); + self.code.table_imports.insert( + *idx, + Table { + module: module.clone(), + name: name.clone(), + initial: initial.clone(), + maximum: maximum.clone(), + element: element.clone(), + }, + ); + } + WasmEvent::ImportGlobal(ImportGlobal { + idx, + module, + name, + mutable, + initial, + value, + }) => { + self.add_module(module); + self.code.global_imports.insert( + *idx, + Global { + module: module.clone(), + name: name.clone(), + value: value.clone(), + initial: initial.clone(), + mutable: *mutable, + }, + ); + } + WasmEvent::ImportFunc(ImportFunc { idx, module, name }) => { + self.add_module(module); + self.code.func_imports.insert( + *idx, + Function { + module: module.clone(), + name: name.clone(), + bodys: vec![], + results: vec![], + }, + ); + } + WasmEvent::FuncEntry(_) | WasmEvent::FuncReturn(_) => (), + } + } + fn push_event(&mut self, event: Event) { + match event { + Event::Call(_) | Event::TableCall(_) => { + let idx = self.state.import_call_stack.last().unwrap(); + if *idx == -1 { + self.code.initialization.push(event.clone()); + } + let current_context = self + .code + .func_imports + .get_mut(idx) + .unwrap() + .bodys + .last_mut() + .unwrap(); + current_context.push(event.clone()); + return; + } + _ => { + let idx = self.state.import_call_stack.last().unwrap(); + let current_context = if self.state.global_scope { + &mut self.code.initialization + } else { + self.code + .func_imports + .get_mut(idx) + .unwrap() + .bodys + .last_mut() + .unwrap() + }; + match current_context.last() { + Some(Event::Call(_)) | Some(Event::TableCall(_)) => { + if !self.state.last_func_return { + current_context.insert(current_context.len() - 1, event); + } else { + let idx = self.state.last_func; + let current_context = self + .code + .func_imports + .get_mut(&idx) + .unwrap() + .bodys + .last_mut() + .unwrap(); + current_context.push(event.clone()); + } + } + _ => { + current_context.push(event.clone()); + } + } + } + } + } + fn add_module(&mut self, module: &String) { + if !self.code.modules.contains(module) { + self.code.modules.push(module.clone()); + } + } +} + +pub mod js { + use std::fs::File; + use std::io::Write; + + use crate::trace::F64; + + use super::Context; + use super::Event; + use super::Replay; + use super::WriteResult; + + pub fn generate_javascript(stream: &mut File, code: &Replay) -> std::io::Result<()> { + write(stream, "import fs from 'fs'\n")?; + write(stream, "import path from 'path'\n")?; + write(stream, "let instance\n")?; + write(stream, "let imports = {}\n")?; + + // Init modules + for module in &code.modules { + write(stream, &format!("{}\n", write_module(module)))?; + } + // Init memories + for (i, mem) in &code.mem_imports { + write( + stream, + &format!( + "const {} = new WebAssembly.Memory({{ initial: {}, maximum: {} }})\n", + mem.name, + mem.initial, + match mem.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + } + ), + )?; + write( + stream, + &format!("{}{}\n", write_import(&mem.module, &mem.name), mem.name), + )?; + } + // Init globals + for (i, global) in &code.global_imports { + if global.initial.0.is_nan() || !global.initial.0.is_finite() { + if global.name.to_lowercase() == "infinity" { + write( + stream, + &format!("{}Infinity\n", write_import(&global.module, &global.name)), + )?; + } else if global.name.to_lowercase() == "nan" { + write( + stream, + &format!("{}NaN\n", write_import(&global.module, &global.name)), + )?; + } else { + panic!("Could not generate javascript code for the global initialisation, the initial value is NaN. The website you where recording did some weired stuff that I was not considering during the implementation of Wasm-R3. Tried to genereate global:"); + } + } else { + write( + stream, + &format!( + "const {} = new WebAssembly.Global({{ value: '{:?}', mutable: {}}}, {})\n", + global.name, global.value, global.mutable, global.initial + ), + )?; + write( + stream, + &format!( + "{}{}\n", + write_import(&global.module, &global.name), + global.name + ), + )?; + } + } + // Init tables + for (i, table) in &code.table_imports { + write(stream, &format!("const {} = new WebAssembly.Table({{ initial: {}, maximum: {}, element: '{}'}})\n", table.name, table.initial, match table.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + }, table.element))?; + write( + stream, + &format!( + "{}{}\n", + write_import(&table.module, &table.name), + table.name + ), + )?; + } + // Imported functions + for (funcidx, func) in &code.func_imports { + // FIXME: this is a hack to avoid the initialization function + if *funcidx == -1 { + continue; + } + write( + stream, + &format!("let {} = -1\n", write_func_global(funcidx)), + )?; + write( + stream, + &format!("{}() => {{\n", write_import(&func.module, &func.name)), + )?; + write(stream, &format!("{}++\n", write_func_global(funcidx)))?; + if !func.bodys.is_empty() { + write( + stream, + &format!("switch ({}) {{\n", write_func_global(funcidx)), + )?; + for (i, body) in func.bodys.iter().enumerate() { + write_body(stream, body, i)?; + } + write(stream, "}\n")?; + } + write_results(stream, &func.results, &write_func_global(funcidx))?; + write(stream, "}\n")?; + } + write(stream, "export function replay(wasm) {")?; + write(stream, "instance = wasm.instance\n")?; + for (name, params) in &code.calls { + write( + stream, + &format!( + "instance.exports.${}(${}) \n", + name, + write_params_string(params) + ), + )?; + } + for event in &code.initialization { + match event { + Event::Call(event) => write!(stream, "{}", call_event(event))?, + Event::TableCall(event) => write!(stream, "{}", table_call_event(event))?, + Event::Store(event) => write!(stream, "{}", store_event(event))?, + Event::MemGrow(event) => write!(stream, "{}", mem_grow_event(event))?, + Event::TableSet(event) => write!(stream, "{}", table_set_event(event))?, + Event::TableGrow(event) => write!(stream, "{}", table_grow_event(event))?, + Event::GlobalGet(event) => write!(stream, "{}", global_get(event))?, + } + } + write(stream, "}\n")?; + write(stream, "export function instantiate(wasmBinary) {\n")?; + write( + stream, + "return WebAssembly.instantiate(wasmBinary, imports)\n", + )?; + write(stream, "}\n")?; + write(stream, "if (process.argv[2] === 'run') {\n")?; + write(stream, "const p = path.join(path.dirname(import.meta.url).replace(/^file:/, ''), 'index.wasm')\n")?; + write(stream, "const wasmBinary = fs.readFileSync(p)\n")?; + write( + stream, + "instantiate(wasmBinary).then((wasm) => replay(wasm))\n", + )?; + write(stream, "}\n")?; + Ok(()) + } + + fn write_body(stream: &mut File, b: &Context, i: usize) -> std::io::Result<()> { + if !b.is_empty() { + writeln!(stream, "case {}:", i)?; + for event in b { + match event { + Event::Call(event) => write!(stream, "{}", call_event(event))?, + Event::TableCall(event) => write!(stream, "{}", table_call_event(event))?, + Event::Store(event) => write!(stream, "{}", store_event(event))?, + Event::MemGrow(event) => write!(stream, "{}", mem_grow_event(event))?, + Event::TableSet(event) => write!(stream, "{}", table_set_event(event))?, + Event::TableGrow(event) => write!(stream, "{}", table_grow_event(event))?, + Event::GlobalGet(event) => write!(stream, "{}", global_get(event))?, + } + } + writeln!(stream, "break")?; + } + Ok(()) + } + + fn write_results( + stream: &mut File, + results: &[WriteResult], + func_global: &str, + ) -> std::io::Result<()> { + let mut current = 0; + for r in results { + let new_c = current + r.reps; + writeln!( + stream, + "if (({} >= {}) && {} < {}) {{", + func_global, current, func_global, new_c + )?; + let res = match r.results.get(0) { + Some(r) => r.to_string(), + None => "undefined".to_string(), + }; + writeln!(stream, "return {} }}", res)?; + current = new_c; + } + Ok(()) + } + + fn write(stream: &mut File, s: &str) -> std::io::Result<()> { + if stream.write_all(s.as_bytes()).is_err() { + // In Rust, we don't have an equivalent to Node.js's 'drain' event. + // We'll just flush the stream instead. + stream.flush()?; + } + Ok(()) + } + + fn store_event(event: &super::Store) -> String { + let mut js_string = String::new(); + for (j, byte) in event.data.iter().enumerate() { + if event.import { + js_string += &format!( + "new Uint8Array({}.buffer)[{}] = {}\n", + event.name, + event.addr + j as i32, + byte + ); + } else { + js_string += &format!( + "new Uint8Array(instance.exports.{}.buffer)[{}] = {}\n", + event.name, + event.addr + j as i32, + byte + ); + } + } + js_string + } + + fn call_event(event: &super::Call) -> String { + format!( + "instance.exports.{}({})\n", + event.name, + write_params_string(&event.params) + ) + } + + fn table_call_event(event: &super::TableCall) -> String { + format!( + "instance.exports.{}.get({})({})\n", + event.table_name, + event.funcidx, + write_params_string(&event.params) + ) + } + + fn mem_grow_event(event: &super::MemGrow) -> String { + if event.import { + format!("{}.grow({})\n", event.name, event.amount) + } else { + format!("instance.exports.{}.grow({})\n", event.name, event.amount) + } + } + + fn table_set_event(event: &super::TableSet) -> String { + let mut js_string = if event.import { + format!("{}.set({}, ", event.name, event.idx) + } else { + format!("instance.exports.{}.set({}, ", event.name, event.idx) + }; + if event.func_import { + js_string.push_str(&event.func_name); + } else { + js_string.push_str(&format!("instance.exports.{}", event.func_name)); + } + js_string.push_str(")\n"); + js_string + } + + fn table_grow_event(event: &super::TableGrow) -> String { + if event.import { + format!("{}.grow({})\n", event.name, event.amount) + } else { + format!("instance.exports.{}.grow({})\n", event.name, event.amount) + } + } + + fn global_get(event: &super::GlobalGet) -> String { + if event.import { + format!("{}.value = {}\n", event.name, event.value) + } else { + format!( + "instance.exports.{}.value = {}\n", + event.name, + if event.big_int { + format!("BigInt({})", event.value) + } else { + event.value.to_string() + } + ) + } + } + + fn write_func_global(funcidx: &i32) -> String { + format!("global_{}", funcidx.to_string()) + } + + fn write_module(module: &str) -> String { + format!("imports['{}'] = {{}}", module) + } + + fn write_import(module: &str, name: &str) -> String { + format!("imports['{}']['{}'] = ", module, name) + } + + fn write_params_string(params: &[F64]) -> String { + params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + } +} + +// TODO: factor out with test_encode_decode +#[test] +fn test_generate_javascript() -> std::io::Result<()> { + use super::*; + use std::fs; + use std::io; + use std::io::BufRead; + use std::io::Read; + use std::path::Path; + use tempfile::tempfile; + + fn visit_dirs(dir: &Path) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path)?; + } else { + if path.file_name().and_then(|s| s.to_str()) == Some("trace.r3") // node format + || path.file_name().and_then(|s| s.to_str()) == Some("trace-ref.r3") // web format + { + if path.display().to_string().contains("kittygame") || // floating point precision + path.display().to_string().contains("pathfinding") // slow + { + println!("skipping problematic case {}", path.display()); + continue; + } + println! ("Testing {}", path.display()); + // Read replay.js file + let replay_path = path.with_file_name("replay.js"); + let replay_file = fs::File::open(replay_path.clone())?; + let mut reader = io::BufReader::new(replay_file); + let mut old_js = String::new(); + reader.read_to_string(&mut old_js)?; + + let trace_file = fs::File::open(&path)?; + let reader = io::BufReader::new(trace_file); + let mut trace = trace::Trace::new(); + for line in reader.lines() { + let line = line?; + let event = line.parse()?; + trace.push(event); + } + println!("trace read complete"); + let mut generator = Generator::new(); + generator.generate_replay(&trace); + println!("generate_replay complete"); + let mut temp_file = tempfile()?; + generate_javascript(&mut temp_file, &generator.code)?; + println!("generate_javascript complete"); + temp_file.seek(SeekFrom::Start(0))?; + + let mut reader = io::BufReader::new(temp_file); + let mut new_js = String::new(); + reader.read_to_string(&mut new_js)?; + assert!( + old_js == new_js, + "Generated JS does not match for {}, original js: {}", + path.display(), + replay_path.display() + ); + } + } + } + } + Ok(()) + } + visit_dirs(Path::new("../../tests"))?; + Ok(()) +} diff --git a/crates/replay_gen/src/lib.rs b/crates/replay_gen/src/lib.rs index 11ec9166..32b49d84 100644 --- a/crates/replay_gen/src/lib.rs +++ b/crates/replay_gen/src/lib.rs @@ -1 +1,2 @@ +pub mod jsgen; pub mod trace; diff --git a/crates/replay_gen/src/main.rs b/crates/replay_gen/src/main.rs index 42dd5d46..39097729 100644 --- a/crates/replay_gen/src/main.rs +++ b/crates/replay_gen/src/main.rs @@ -3,27 +3,28 @@ use std::fs::File; use std::io::{self, BufRead}; use std::path::Path; +use replay_gen::jsgen::js::generate_javascript; +use replay_gen::jsgen::Generator; use replay_gen::trace; fn main() -> io::Result<()> { // FIXME: use clap to parse args. currently just panics. let args: Vec = env::args().collect(); - let newline = &args[1]; - let path = Path::new(newline); - let file = File::open(&path)?; + let trace_path = Path::new(&args[1]); + let out_path = Path::new(&args[2]); + let file = File::open(&trace_path)?; let reader = io::BufReader::new(file); - let mut lines = reader.lines().peekable(); - - while let Some(line) = lines.next() { + let mut trace = trace::Trace::new(); + for line in reader.lines() { let line = line?; - let event = line.parse::()?; - // hack to print the last event without a newline that matches the current behavior. - if lines.peek().is_some() { - println!("{:?}", event); - } else { - print!("{:?}", event); - } + let event = line.parse()?; + trace.push(event); } + let mut generator = Generator::new(); + generator.generate_replay(&trace); + let path = Path::new(out_path); + let mut file = File::create(&path)?; + generate_javascript(&mut file, &generator.code)?; Ok(()) } diff --git a/crates/replay_gen/src/trace.rs b/crates/replay_gen/src/trace.rs index 433e83f0..17548839 100644 --- a/crates/replay_gen/src/trace.rs +++ b/crates/replay_gen/src/trace.rs @@ -1,10 +1,11 @@ -use std::fmt; use std::fmt::Debug; +use std::fmt::{self, Write}; use std::io::{Error, ErrorKind}; use std::str::FromStr; // FIXME: this is a hack to get around the fact that the trace generated by js. Remove when we discard js based trace. -struct F64(f64); +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct F64(pub f64); impl fmt::Display for F64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -27,6 +28,7 @@ impl std::str::FromStr for F64 { } } +#[derive(Clone, PartialEq)] pub enum ValType { I32, I64, @@ -68,28 +70,15 @@ impl std::str::FromStr for ValType { } } -pub struct Tracer { - trace: Vec, -} - -impl Tracer { - pub fn new() -> Self { - Self { trace: Vec::new() } - } - pub fn push(&mut self, event: WasmEvent) { - self.trace.push(event); - } -} +pub type Trace = Vec; -impl Debug for Tracer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for event in &self.trace { - write!(f, "{:?}\n", event)?; - } - Ok(()) +fn decode_trace(trace: Trace) -> Result { + let mut s = String::new(); + for event in trace { + write!(&mut s, "{:?}\n", event)?; } + Ok(s) } - pub enum WasmEvent { Load(Load), MemGrow(MemGrow), @@ -110,87 +99,88 @@ pub enum WasmEvent { } pub struct Load { - idx: i32, - name: String, - offset: i32, - data: Vec, + pub idx: i32, + pub name: String, + pub offset: i32, + pub data: Vec, } pub struct MemGrow { - idx: i32, - name: String, - amount: i32, + pub idx: i32, + pub name: String, + pub amount: i32, } pub struct TableGet { - tableidx: i32, - name: String, - idx: i32, - funcidx: i32, - funcname: String, + pub tableidx: i32, + pub name: String, + pub idx: i32, + pub funcidx: i32, + pub funcname: String, } pub struct TableGrow { - idx: i32, - name: String, - amount: i32, + pub idx: i32, + pub name: String, + pub amount: i32, } pub struct GlobalGet { - idx: i32, - name: String, - value: F64, - valtype: ValType, + pub idx: i32, + pub name: String, + pub value: F64, + pub valtype: ValType, } pub struct ExportCall { - name: String, - params: Vec, + pub name: String, + pub params: Vec, } pub struct TableCall { - tablename: String, - funcidx: i32, - params: Vec, + pub tablename: String, + pub funcidx: i32, + pub params: Vec, } pub struct ImportCall { - idx: i32, - name: String, + pub idx: i32, + pub name: String, } pub struct ImportReturn { - idx: i32, - name: String, - results: Vec, + pub idx: i32, + pub name: String, + pub results: Vec, } pub struct ImportMemory { - idx: i32, - module: String, - name: String, - initial: F64, - maximum: Option, + pub idx: i32, + pub module: String, + pub name: String, + pub initial: F64, + pub maximum: Option, } pub struct ImportTable { - idx: i32, - module: String, - name: String, - initial: F64, - maximum: Option, + pub idx: i32, + pub module: String, + pub name: String, + pub element: String, + pub initial: F64, + pub maximum: Option, } pub struct ImportGlobal { - idx: i32, - module: String, - name: String, - mutable: bool, - initial: F64, - value: ValType, + pub idx: i32, + pub module: String, + pub name: String, + pub mutable: bool, + pub initial: F64, + pub value: ValType, } pub struct ImportFunc { - idx: i32, - module: String, - name: String, + pub idx: i32, + pub module: String, + pub name: String, } pub struct FuncEntry { - idx: i32, - args: Vec, + pub idx: i32, + pub args: Vec, } pub struct FuncReturn { - idx: i32, - values: Vec, + pub idx: i32, + pub values: Vec, } fn join_vec(args: &Vec) -> String { @@ -324,6 +314,7 @@ impl FromStr for WasmEvent { } else { Some(components[5].parse().unwrap()) }, + element: components[6].parse().unwrap(), })), "FE" => Ok(WasmEvent::FuncEntry(FuncEntry { idx: components[1].parse().unwrap(), @@ -407,6 +398,7 @@ impl Debug for WasmEvent { name, initial, maximum, + element, }) => { let temp = match maximum { Some(f) => f.0.to_string(), @@ -415,7 +407,12 @@ impl Debug for WasmEvent { write!( f, "IT;{};{};{};{};{};{}", - idx, module, name, initial, temp, "anyfunc" + idx, + module, + name, + initial, + temp, + "anyfunc" // // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') ) } WasmEvent::ImportGlobal(ImportGlobal { @@ -470,7 +467,7 @@ fn test_encode_decode() -> std::io::Result<()> { visit_dirs(&path)?; } else { if path.extension().and_then(|s| s.to_str()) == Some("r3") { - if path.display().to_string().contains("pathfinding") { + if path.display().to_string().contains("pathfinding") { // floating point precision println!("skipping problematic case {}", path.display()); continue; } @@ -480,14 +477,24 @@ fn test_encode_decode() -> std::io::Result<()> { let file = fs::File::open(&path)?; let reader = io::BufReader::new(file); - let mut tracer = trace::Tracer::new(); + let mut trace = trace::Trace::new(); for line in reader.lines() { let line = line?; - let event = line.parse()?; - tracer.push(event); + let event = match line.parse() { + Ok(e) => e, + Err(_) => panic!("error parsing file: {}", path.display()), + }; + trace.push(event); } let mut newfile = tempfile()?; - write!(newfile, "{:?}", tracer)?; + let trace_str = match decode_trace(trace) { + Ok(s) => s, + Err(e) => { + println!("error decoding trace: {}", e); + continue; + } + }; + write!(newfile, "{}", trace_str)?; newfile.seek(SeekFrom::Start(0))?; let mut new_contents = Vec::new(); diff --git a/package.json b/package.json index 474e97d1..fd0083c1 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ }, "scripts": { "build": "tsc && node ./build.js", + "build-rust": "cd crates/ && cargo build --release", "build-wasabi": "cd wasabi/crates/wasabi_js && npm run build && wasm-pack build --target web && wasm-pack build --target nodejs --out-dir ../../../dist/wasabi && cd ../../..", - "build-full": "npm run build-wasabi && npm run build", + "build-full": "npm run build-rust && npm run build-wasabi && npm run build", "test": "npm run build && node ./dist/tests/run-tests.cjs", "clean-tests": "node ./dist/tests/clean-tests.cjs", "start": "npm run build && node ./dist/src/cli/main.cjs" diff --git a/src/benchmark.cts b/src/benchmark.cts index 52ff0e46..c959cbc1 100644 --- a/src/benchmark.cts +++ b/src/benchmark.cts @@ -4,6 +4,7 @@ import path from 'path' import Generator from "./replay-generator.cjs" import { AnalysisResult } from "./analyser.cjs" import { Trace } from "./tracer.cjs" +import { execSync } from 'child_process'; //@ts-ignore import { instrument_wasm } from '../wasabi/wasabi_js.js' import { createMeasure } from './performance.cjs' @@ -17,7 +18,7 @@ export default class Benchmark { private record: Record private constructor() { } - async save(benchmarkPath: string, options = { trace: false }) { + async save(benchmarkPath: string, options = { trace: false, rustBackend: false }) { const p_measureSave = createMeasure('save', { phase: 'replay-generation', description: 'The time it takes to save the benchmark to the disk. This means generating the intermediate representation code from the trace and streaming it to the file, as well as saving the wasm binaries.' }) await fs.mkdir(benchmarkPath) await Promise.all(this.record.map(async ({ binary, trace }, i) => { @@ -26,14 +27,20 @@ export default class Benchmark { if (options.trace === true) { await fs.writeFile(path.join(binPath, 'trace.r3'), trace.toString()) } - const diskSave = `temp-trace-${i}.r3` + const diskSave = path.join(binPath, `temp-trace-${i}.r3`) await fs.writeFile(diskSave, trace.toString()) - const p_measureCodeGen = createMeasure('ir-gen', { phase: 'replay-generation', description: `The time it takes to generate the IR code for subbenchmark ${i}` }) - const code = await new Generator().generateReplayFromStream(fss.createReadStream(diskSave)) - p_measureCodeGen() - const p_measureJSWrite = createMeasure('string-gen', { phase: 'replay-generation', description: `The time it takes to stream the replay code to the file for subbenchmark ${i}` }) - await generateJavascript(fss.createWriteStream(path.join(binPath, 'replay.js')), code) - p_measureJSWrite() + if (options.rustBackend === true) { + const p_measureCodeGen = createMeasure('rust-backend', { phase: 'replay-generation', description: `The time it takes for rust backend to generate javascript` }) + execSync(`./crates/target/release/replay_gen ${diskSave} ${path.join(binPath, 'replay.js')}`); + p_measureCodeGen() + } else { + const p_measureCodeGen = createMeasure('ir-gen', { phase: 'replay-generation', description: `The time it takes to generate the IR code for subbenchmark ${i}` }) + const code = await new Generator().generateReplayFromStream(fss.createReadStream(diskSave)) + p_measureCodeGen() + const p_measureJSWrite = createMeasure('string-gen', { phase: 'replay-generation', description: `The time it takes to stream the replay code to the file for subbenchmark ${i}` }) + await generateJavascript(fss.createWriteStream(path.join(binPath, 'replay.js')), code) + p_measureJSWrite() + } await fs.writeFile(path.join(binPath, 'index.wasm'), Buffer.from(binary)) await fs.rm(diskSave) })) diff --git a/src/cli/options.cts b/src/cli/options.cts index 29bd6005..839ebee0 100644 --- a/src/cli/options.cts +++ b/src/cli/options.cts @@ -9,6 +9,7 @@ export type Options = { file: string, extended: boolean, noRecord: boolean, + rustBackend: boolean } export default function getOptions() { @@ -20,12 +21,16 @@ export default function getOptions() { { name: 'headless', alias: 'h', type: Boolean }, { name: 'file', alias: 'f', type: String }, { name: 'extended', alias: 'e', type: Boolean }, - { name: 'no-record', alias: 'n', type: Boolean} + { name: 'no-record', alias: 'n', type: Boolean }, + { name: 'rustBackend', alias: 'r', type: Boolean } ] const options: Options & { url: string } = commandLineArgs(optionDefinitions) if (options.headless === undefined) { options.headless = false } + if (options.rustBackend) { + console.log('CAUTION: Using experimental Rust backend') + } if (fs.existsSync(options.benchmarkPath)) { throw new Error(`EEXIST: Directory at path ${options.benchmarkPath} does already exist`) } diff --git a/src/instrumenter.cts b/src/instrumenter.cts index 25f1141d..c77ede20 100644 --- a/src/instrumenter.cts +++ b/src/instrumenter.cts @@ -20,5 +20,5 @@ export default async function run(url: string, options: Options) { console.log(`Record stopped. Downloading...`) const results = await analyser.stop() console.log('Download done. Generating Benchmark...') - Benchmark.fromAnalysisResult(results).save(options.benchmarkPath, { trace: options.dumpTrace }) + Benchmark.fromAnalysisResult(results).save(options.benchmarkPath, { trace: options.dumpTrace, rustBackend: options.rustBackend }) } \ No newline at end of file diff --git a/tests/run-tests.cts b/tests/run-tests.cts index 9cdb9fdd..370e85e0 100644 --- a/tests/run-tests.cts +++ b/tests/run-tests.cts @@ -165,7 +165,7 @@ function compareResults(testPath: string, traceString: string, replayTraceString return { testPath, success: true } } -async function runOnlineTests(names: string[]) { +async function runOnlineTests(names: string[], options) { if (names.length > 0) { console.log('================') console.log('Run online tests') @@ -178,25 +178,26 @@ async function runOnlineTests(names: string[]) { 'visual6502remix', // takes so long and is not automated yet 'heatmap', // takes so long 'image-convolute', // out of memory + 'kittygame', // too slow for rust backend ] names = names.filter((n) => !filter.includes(n)) for (let name of names) { const spinner = startSpinner(name) const testPath = path.join(process.cwd(), 'tests', 'online', name) const cleanUpPerformance = await initPerformance(name, 'online-auto-test', path.join(testPath, 'performance.ndjson')) - const report = await runOnlineTest(testPath) + const report = await runOnlineTest(testPath, options) stopSpinner(spinner) cleanUpPerformance() await writeReport(name, report) } } -async function runOnlineTest(testPath: string) { +async function runOnlineTest(testPath: string, options) { await cleanUp(testPath) - return testWebPage(testPath) + return testWebPage(testPath, options) } -async function runOfflineTests(names: string[]) { +async function runOfflineTests(names: string[], options) { if (names.length > 0) { console.log('=================') console.log('Run offline tests') @@ -209,7 +210,7 @@ async function runOfflineTests(names: string[]) { names = names.filter((n) => !filter.includes(n)) for (let name of names) { const spinner = startSpinner(name) - const report = await runOfflineTest(name) + const report = await runOfflineTest(name, options) stopSpinner(spinner) await writeReport(name, report) } @@ -247,17 +248,17 @@ function startServer(websitePath: string): Promise { }) } -async function runOfflineTest(name: string): Promise { +async function runOfflineTest(name: string, options): Promise { const testPath = path.join(process.cwd(), 'tests', 'offline', name) const websitePath = path.join(testPath, 'website') await cleanUp(testPath) const server = await startServer(websitePath) - let report = await testWebPage(testPath) + let report = await testWebPage(testPath, options) server.close() return report } -async function testWebPage(testPath: string): Promise { +async function testWebPage(testPath: string, options): Promise { const testJsPath = path.join(testPath, 'test.js') const benchmarkPath = path.join(testPath, 'benchmark') let analysisResult: AnalysisResult @@ -275,7 +276,7 @@ async function testWebPage(testPath: string): Promise { } // process.stdout.write(` -e not available`) const benchmark = Benchmark.fromAnalysisResult(analysisResult) - await benchmark.save(benchmarkPath, { trace: true }) + await benchmark.save(benchmarkPath, { trace: true, rustBackend: options.rustBackend }) let subBenchmarkNames = await getDirectoryNames(benchmarkPath) if (subBenchmarkNames.length === 0) { return { testPath, success: false, reason: 'no benchmark was generated' } @@ -326,10 +327,13 @@ async function testWebPage(testPath: string): Promise { const optionDefinitions = [ { name: 'extended', alias: 'e', type: Boolean }, { name: 'category', type: String, multiple: true, defaultOption: true }, - { name: 'testcases', alias: 't', type: String, multiple: true } + { name: 'testcases', alias: 't', type: String, multiple: true }, + { name: 'rustBackend', alias: 'r', type: Boolean } ] const options = commandLineArgs(optionDefinitions) - extended = options.extended + if (options.rustBackend) { + console.log('CAUTION: Using experimental Rust backend') + } if (options.category === undefined || options.category.includes('node')) { let testNames = await getDirectoryNames(path.join(process.cwd(), 'tests', 'node')); if (options.testcases !== undefined) { @@ -342,14 +346,14 @@ async function testWebPage(testPath: string): Promise { if (options.testcases !== undefined) { testNames = testNames.filter(n => options.testcases.includes(n)); } - await runOfflineTests(testNames) + await runOfflineTests(testNames, options) } if (options.category === undefined || options.category.includes('online')) { let testNames = await getDirectoryNames(path.join(process.cwd(), 'tests', 'online')); if (options.testcases !== undefined) { testNames = testNames.filter(n => options.testcases.includes(n)); } - await runOnlineTests(testNames) + await runOnlineTests(testNames, options) } // process.stdout.write(`done running ${nodeTestNames.length + webTestNames.length} tests\n`); })()