From 03fe3fc42f071c01f2ed2ed7f55433257f7f23eb Mon Sep 17 00:00:00 2001 From: Doehyun Baek Date: Mon, 5 Feb 2024 15:50:54 +0900 Subject: [PATCH] wasm-merge backed replay generation --- README.md | 6 +- crates/replay_gen/src/codegen.rs | 646 ------------------------------- crates/replay_gen/src/irgen.rs | 408 ++++++++++++------- crates/replay_gen/src/jsgen.rs | 321 +++++++++++++++ crates/replay_gen/src/lib.rs | 20 +- crates/replay_gen/src/main.rs | 24 +- crates/replay_gen/src/opt.rs | 78 +++- crates/replay_gen/src/trace.rs | 37 +- crates/replay_gen/src/wasmgen.rs | 500 ++++++++++++++++++++++++ package.json | 5 +- src/benchmark.cts | 22 +- src/cli/options.cts | 7 +- src/instrumenter.cts | 2 +- src/tracer.cts | 19 +- src/wasm-generator.cts | 13 - tests/.gitignore | 1 + tests/run-tests.cts | 70 ++-- 17 files changed, 1287 insertions(+), 892 deletions(-) delete mode 100644 crates/replay_gen/src/codegen.rs create mode 100644 crates/replay_gen/src/jsgen.rs create mode 100644 crates/replay_gen/src/wasmgen.rs delete mode 100644 src/wasm-generator.cts create mode 100644 tests/.gitignore diff --git a/README.md b/README.md index cec1e798..977dfe54 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,13 @@ This folder contains the source code for the Wasm-R3 tool. +## Prerequisites + +- wasm-tools + ## Building -When first cloning the repository you want to install all dependencies. From the root of this repository run: +When first cloning the repository you want to install all dependencies. From the root of this repository run: ``` npm install ``` diff --git a/crates/replay_gen/src/codegen.rs b/crates/replay_gen/src/codegen.rs deleted file mode 100644 index 977aa31c..00000000 --- a/crates/replay_gen/src/codegen.rs +++ /dev/null @@ -1,646 +0,0 @@ -use std::io::Write; -use std::{fs::File, path::Path}; - -use crate::irgen::HostEvent; -use crate::trace::ValType; -use crate::{ - irgen::{Context, Replay, WriteResult}, - trace::F64, -}; - -pub fn generate_javascript(out_path: &Path, code: &Replay) -> std::io::Result<()> { - let mut file = File::create(&out_path)?; - let stream = &mut file; - 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 { - // TODO: better handling of initialization - if *funcidx == -1 { - continue; - } - write(stream, &format!("let {} = 0\n", write_func_global(funcidx)))?; - write( - stream, - &format!("{}() => {{\n", write_import(&func.module, &func.name)), - )?; - 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(stream, &format!("{}++\n", write_func_global(funcidx)))?; - write_results(stream, &func.results, &write_func_global(funcidx))?; - write(stream, "}\n")?; - } - write(stream, "export function replay(wasm) {")?; - write(stream, "instance = wasm.instance\n")?; - let initialization = code.func_imports.get(&-1).unwrap().bodys.last().unwrap(); - for event in initialization { - let str = hostevent_to_js(event); - writeln!(stream, "{}", str)?; - } - 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 valty_to_const(valty: &ValType) -> String { - match valty { - ValType::I32 => "i32.const", - ValType::I64 => "i64.const", - ValType::F32 => "f32.const", - ValType::F64 => "f64.const", - ValType::V128 => todo!(), - ValType::Anyref => todo!(), - ValType::Externref => todo!(), - } - .to_string() -} - -pub fn generate_standalone(wasm_path: &Path, code: &Replay) -> std::io::Result<()> { - let orig_wat = wasmprinter::print_file(wasm_path).unwrap(); - let orig_wat = orig_wat.split('\n').collect::>(); - let mut iter = orig_wat.iter().peekable(); - - let canned = wasm_path.parent().unwrap().join("canned.wat"); - let mut file = File::create(&canned).unwrap(); - let stream = &mut file; - while let Some(line) = iter.next() { - if line.trim_start().starts_with("(import") { - let import_type = if line.contains("(func") { - "(func" - } else if line.contains("(memory") { - "(memory" - } else if line.contains("(global") { - "(global" - } else if line.contains("(table") { - "(table" - } else { - unreachable!("Unknown import: {}", line); - }; - let start = line.find(import_type).unwrap_or(0); - let end = line.rfind(")").unwrap_or_else(|| line.len()); - - if import_type == "(func" { - let start_idx = line.find("(;").unwrap_or(0) + 2; - let end_idx = line.find(";)").unwrap_or_else(|| line.len()); - let funcidx: i32 = line[start_idx..end_idx].parse().unwrap(); - let global_idx = format!("$global_{}", funcidx.to_string()); - write(stream, &(line[start..=end - 2].to_string() + "\n"))?; - let func = code.func_imports.get(&funcidx).unwrap(); - for (i, body) in func.bodys.iter().enumerate() { - let mut bodystr = String::new(); - let _body = for event in body { - bodystr += &hostevent_to_wat(event, code) - }; - write( - stream, - &format!("(i64.eq (global.get {global_idx}) (i64.const {i}))\n"), - )?; - write(stream, &format!("(if(then {bodystr}))\n"))?; - } - write( - stream, - &format!( - "(global.get {global_idx}) (i64.const 1) (i64.add) (global.set {global_idx})\n" - ), - )?; - let mut current = 0; - for r in func.results.iter() { - let ty = code.func_idx_to_ty.get(&(funcidx as usize)).unwrap(); - let _param_tys = ty.params.clone(); - let new_c = current + r.reps; - let res = match r.results.get(0) { - Some(v) => format!( - "(return ({} {v}))", - valty_to_const(ty.results.get(0).unwrap()) - ), - None => "(return)".to_owned(), - }; - write( - stream, - &format!( - " (if - (i32.and - (i64.ge_s (global.get {global_idx}) (i64.const {current})) - (i64.lt_s (global.get {global_idx}) (i64.const {new_c})) - ) - (then - {res} - ) - )" - ), - )?; - current = new_c; - } - let ty = code.func_idx_to_ty.get(&(funcidx as usize)).unwrap(); - let _param_tys = ty.params.clone(); - let default_return = match ty.results.get(0) { - Some(v) => match v { - ValType::I32 => "(i32.const 0)", - ValType::I64 => "(i64.const 0)", - ValType::F32 => "(f32.const 0)", - ValType::F64 => "(f64.const 0)", - ValType::V128 => todo!(), - ValType::Anyref => todo!(), - ValType::Externref => todo!(), - }, - None => "", - }; - write(stream, &format!("(return {})", default_return))?; - write(stream, ")\n")?; - } else if import_type == "(global" { - let valtype = if line.contains("i32") { - "i32" - } else if line.contains("i64") { - "i64" - } else if line.contains("f32") { - "f32" - } else if line.contains("f64") { - "f64" - } else { - unreachable!("Unknown global type: {}", line); - }; - let replaced = if !line.contains("mut") { - line.replace( - &format!("{valtype}"), - &format!("{valtype} ({valtype}.const 0)"), - ) - } else { - line.replace( - &format!("(mut {valtype})"), - &format!("(mut {valtype}) ({valtype}.const 0)"), - ) - }; - write(stream, &&(replaced[start..replaced.len() - 1]))?; - } else { - write(stream, &(line[start..=end - 1].to_string() + "\n"))?; - } - // handle the following - // error: initializer expression can only reference an imported global - } else if (line.trim_start().starts_with("(global") - || line.trim_start().starts_with("(elem")) - && line.contains("global.get") - { - let parts: Vec<&str> = line.split("global.get").collect(); - let first_part = parts[0]; - let remaining_parts: Vec<&str> = parts[1].trim().split(')').collect(); - let third_part = remaining_parts[0].trim(); - let fourth_part = remaining_parts[1].trim(); - - let global = code - .global_imports - .get(&third_part.parse::().unwrap()) - .unwrap(); - - // TODO: make this more elegant - let hack_for_diff_format = if line.trim_start().starts_with("(elem") { - "" - } else { - "(" - }; - - let to_write = first_part.to_string() - + &format!( - "{}{} {:?}){})", - hack_for_diff_format, - &valty_to_const(&global.value), - &global.initial, - fourth_part - ); - write(stream, &to_write)?; - write(stream, "\n")?; - } - // handle error: duplicate export "main" - else if line.contains("export \"main\"") { - continue; - } - // _start function - else if iter.peek().is_none() { - for (i, _f) in &code.func_imports { - if *i == -1 { - continue; - } - let global_idx = format!("$global_{}", i.to_string()); - write( - stream, - &format!("(global {global_idx} (mut i64) (i64.const 0))\n"), - )?; - } - write(stream, "(func (export \"_start\") (export \"main\")\n")?; - let initialization = code.func_imports.get(&-1).unwrap().bodys.last().unwrap(); - for event in initialization { - write(stream, &format!("{}", hostevent_to_wat(event, code)))? - } - write(stream, "(return)\n)")?; - write(stream, line)?; - } else { - write(stream, line)?; - write(stream, "\n")?; - } - } - let binary = wat::parse_file(canned.clone()).unwrap(); - let canned_wasm = wasm_path.parent().unwrap().join("canned.wasm"); - let mut file = File::create(&canned_wasm).unwrap(); - file.write_all(&binary).unwrap(); - - 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 { - let str = hostevent_to_js(event); - writeln!(stream, "{}", str)?; - } - writeln!(stream, "break")?; - } - Ok(()) -} - -fn hostevent_to_js(event: &HostEvent) -> String { - fn write_params_string(params: &[F64]) -> String { - params - .iter() - .map(|p| p.to_string()) - .collect::>() - .join(",") - } - let str = match event { - HostEvent::ExportCall { - idx: _, - name, - params, - } => { - format!( - "instance.exports.{}({})\n", - name, - write_params_string(¶ms) - ) - } - HostEvent::ExportCallTable { - idx: _, - table_name, - funcidx, - params, - } => { - format!( - "instance.exports.{}.get({})({})\n", - table_name, - funcidx, - write_params_string(¶ms) - ) - } - HostEvent::MutateMemory { - addr, - data, - import, - name, - } => { - let mut js_string = String::new(); - for (j, byte) in data.iter().enumerate() { - if *import { - js_string += &format!( - "new Uint8Array({}.buffer)[{}] = {}\n", - name, - addr + j as i32, - byte - ); - } else { - js_string += &format!( - "new Uint8Array(instance.exports.{}.buffer)[{}] = {}\n", - name, - addr + j as i32, - byte - ); - } - } - js_string - } - HostEvent::GrowMemory { - amount, - import, - name, - } => { - if *import { - format!("{}.grow({})\n", name, amount) - } else { - format!("instance.exports.{}.grow({})\n", name, amount) - } - } - HostEvent::MutateTable { - tableidx: _, - funcidx: _, - idx, - func_import, - func_name, - import, - name, - } => { - let mut js_string = if *import { - format!("{}.set({}, ", name, idx) - } else { - format!("instance.exports.{}.set({}, ", name, idx) - }; - if *func_import { - js_string.push_str(&func_name); - } else { - js_string.push_str(&format!("instance.exports.{}", func_name)); - } - js_string.push_str(")\n"); - js_string - } - HostEvent::GrowTable { - idx: _, - amount, - import, - name, - } => { - if *import { - format!("{}.grow({})\n", name, amount) - } else { - format!("instance.exports.{}.grow({})\n", name, amount) - } - } - HostEvent::MutateGlobal { - idx: _, - value, - valtype, - import, - name, - } => { - if *import { - format!("{}.value = {}\n", name, value) - } else { - format!( - "instance.exports.{}.value = {}\n", - name, - if *valtype == ValType::I64 { - format!("BigInt({})", value) - } else { - value.to_string() - } - ) - } - } - }; - str -} - -fn hostevent_to_wat(event: &HostEvent, code: &Replay) -> String { - let str = match event { - HostEvent::ExportCall { - idx, - name: _, - params, - } => { - let ty = code.func_idx_to_ty.get(&(*idx as usize)).unwrap(); - let param_tys = ty.params.clone(); - let result_count = ty.results.len(); - let idx = idx; - let params = params - .iter() - .zip(param_tys.clone()) - .map(|(p, p_ty)| format!("({} {p})", valty_to_const(&p_ty))) - .collect::>() - .join("\n"); - params + &format!("(call {idx})") + &("(drop)".repeat(result_count)) - } - HostEvent::ExportCallTable { - idx, - table_name: _, - funcidx: _, - params, - } => { - let ty = code.func_idx_to_ty.get(&(*idx as usize)).unwrap(); - let param_tys = ty.params.clone(); - let result_count = ty.results.len(); - let idx = idx; - let params = params - .iter() - .zip(param_tys.clone()) - .map(|(p, p_ty)| format!("({} {p})", valty_to_const(&p_ty))) - .collect::>() - .join("\n"); - params + &format!("(call {idx})",) + &("(drop)".repeat(result_count)) - } - HostEvent::MutateMemory { - addr, - data, - import: _, - name: _, - } => { - let mut js_string = String::new(); - for (j, byte) in data.iter().enumerate() { - js_string += &format!("i32.const {}\n", addr + j as i32); - js_string += &format!("i32.const {}\n", byte); - js_string += &format!("i32.store\n",); - } - js_string - } - HostEvent::GrowMemory { - amount, - import: _, - name: _, - } => { - format!("(memory.grow (i32.const {})) (drop)\n", amount) - } - HostEvent::MutateTable { - tableidx, - funcidx, - idx, - func_import: _, - func_name: _, - import: _, - name: _, - } => { - format!( - "(table.set {} (i32.const {}) (ref.func {}))", - tableidx, idx, funcidx - ) - } - HostEvent::GrowTable { - idx: _, - amount, - import: _, - name: _, - } => { - format!("(table.grow (i32.const {})) (drop)\n", amount) - } - HostEvent::MutateGlobal { - idx, - value, - valtype, - import: _, - name: _, - } => { - let valtype = match valtype { - ValType::I32 => "i32.const", - ValType::I64 => "i64.const", - ValType::F32 => "f32.const", - ValType::F64 => "f64.const", - ValType::V128 => todo!(), - ValType::Anyref => todo!(), - ValType::Externref => todo!(), - }; - let value = value; - let globalidx = idx; - format!("({valtype} {value})\n") + &format!("(global.set {globalidx})") - } - }; - str -} - -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_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 + 1, - func_global, - new_c + 1 - )?; - let res = match r.results.get(0) { - Some(r) => r.to_string(), - None => "undefined".to_string(), - }; - writeln!(stream, "return {} }}", res)?; - current = new_c; - } - Ok(()) -} - -pub 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(()) -} diff --git a/crates/replay_gen/src/irgen.rs b/crates/replay_gen/src/irgen.rs index f5c39d2e..4bcf16b1 100644 --- a/crates/replay_gen/src/irgen.rs +++ b/crates/replay_gen/src/irgen.rs @@ -3,42 +3,116 @@ //! Its main job is to put the right HostEvent into a right spot. //! HostEvent corresponds to some event in the host context, which is classified into the effect //! it has on wasm state. They get translated into different host code depending on the backend. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use walrus::Module; -use crate::trace::{Trace, ValType, WasmEvent, F64}; +use crate::trace::{RefType, Trace, ValType, WasmEvent, F64}; + +const MAX_WASM_FUNCTIONS: usize = 1000000; +pub const INIT_INDEX: usize = MAX_WASM_FUNCTIONS + 1; pub struct IRGenerator { pub replay: Replay, state: State, + flag: bool, } pub struct Replay { - // original index is usize but we use i32 to handle host initialization code - // TODO: more elegant solution - pub func_imports: BTreeMap, - pub func_idx_to_ty: BTreeMap, - pub mem_imports: BTreeMap, - pub table_imports: BTreeMap, - pub global_imports: BTreeMap, - pub modules: Vec, + pub funcs: BTreeMap, + pub tables: BTreeMap, + pub mems: BTreeMap, + pub globals: BTreeMap, + pub module: Module, +} + +impl Replay { + pub fn imported_funcs(&self) -> BTreeMap { + self.funcs + .iter() + .filter(|(_, function)| function.import.is_some()) + .map(|(key, function)| (*key, function.clone())) + .collect() + } + pub fn exported_funcs(&self) -> BTreeMap { + self.funcs + .iter() + .filter(|(_, function)| function.export.is_some()) + .map(|(key, function)| (*key, function.clone())) + .collect() + } + + pub fn imported_tables(&self) -> BTreeMap { + self.tables + .iter() + .filter(|(_, table)| table.import.is_some()) + .map(|(key, table)| (*key, table.clone())) + .collect() + } + pub fn exported_tables(&self) -> BTreeMap { + self.tables + .iter() + .filter(|(_, table)| table.export.is_some()) + .map(|(key, table)| (*key, table.clone())) + .collect() + } + pub fn imported_mems(&self) -> BTreeMap { + self.mems + .iter() + .filter(|(_, mem)| mem.import.is_some()) + .map(|(key, mem)| (*key, mem.clone())) + .collect() + } + pub fn exported_mems(&self) -> BTreeMap { + self.mems + .iter() + .filter(|(_, mem)| mem.export.is_some()) + .map(|(key, mem)| (*key, mem.clone())) + .collect() + } + pub fn imported_globals(&self) -> BTreeMap { + self.globals + .iter() + .filter(|(_, global)| global.import.is_some()) + .map(|(key, global)| (*key, global.clone())) + .collect() + } + pub fn exported_globals(&self) -> BTreeMap { + self.globals + .iter() + .filter(|(_, global)| global.export.is_some()) + .map(|(key, global)| (*key, global.clone())) + .collect() + } + pub fn imported_modules(&self) -> Vec { + let mut vec: Vec = self + .module + .imports + .iter() + .map(|i| i.module.clone()) + .collect(); + + // delete duplicate + let set: HashSet = vec.drain(..).collect(); + vec.extend(set.into_iter()); + vec + } } struct State { - host_call_stack: Vec, - last_func: i32, + host_call_stack: Vec, + last_func: usize, } #[derive(Clone, Debug)] pub enum HostEvent { ExportCall { - idx: i32, + idx: usize, name: String, params: Vec, }, ExportCallTable { - idx: i32, + idx: usize, table_name: String, funcidx: i32, params: Vec, @@ -69,7 +143,7 @@ pub enum HostEvent { }, MutateTable { tableidx: usize, - funcidx: i32, + funcidx: usize, idx: i32, func_import: bool, func_name: String, @@ -87,11 +161,23 @@ pub struct WriteResult { pub type Context = Vec; #[derive(Clone, Debug)] -pub struct Function { +pub struct Import { pub module: String, pub name: String, - pub bodys: Vec, +} +#[derive(Clone, Debug)] + +pub struct Export { + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct Function { + pub import: Option, + pub export: Option, + pub bodys: Vec>, pub results: Vec, + pub ty: FunctionTy, } #[derive(Clone, Debug)] @@ -100,114 +186,176 @@ pub struct FunctionTy { pub results: Vec, } +#[derive(Clone, Debug)] pub struct Memory { - pub module: String, - pub name: String, + pub import: Option, + pub export: Option, pub initial: u32, pub maximum: Option, } +#[derive(Clone, Debug)] pub struct Table { - pub module: String, - pub name: String, - // enum is better - pub element: String, + pub import: Option, + pub export: Option, + pub reftype: RefType, pub initial: u32, pub maximum: Option, } +#[derive(Clone, Debug)] pub struct Global { - pub module: String, - pub name: String, + pub import: Option, + pub export: Option, pub mutable: bool, pub initial: F64, - pub value: ValType, + pub valtype: ValType, } impl IRGenerator { pub fn new(module: Module) -> Self { - let mut func_imports = BTreeMap::new(); - func_imports.insert( - -1, + let mut funcs = BTreeMap::new(); + let mut mems = BTreeMap::new(); + let mut tables = BTreeMap::new(); + let mut globals = BTreeMap::new(); + funcs.insert( + INIT_INDEX, Function { - module: "wasm-r3".to_string(), - name: "initialization".to_string(), - bodys: vec![vec![]], + import: Some(Import { + module: "wasm-r3".to_string(), + name: "initialization".to_string(), + }), + export: None, + ty: FunctionTy { + params: vec![], + results: vec![], + }, + bodys: vec![Some(vec![])], results: vec![], }, ); - let mut mem_imports = BTreeMap::new(); - let mut table_imports = BTreeMap::new(); - let mut func_idx_to_ty = BTreeMap::new(); for f in module.funcs.iter() { - let ty = module.types.get(f.ty()); - func_idx_to_ty.insert( + let import = match &f.kind { + walrus::FunctionKind::Import(i) => Some(Import { + module: module.imports.get(i.import).module.to_string(), + name: module.imports.get(i.import).name.to_string(), + }), + _ => None, + }; + let export = module.exports.get_exported_func(f.id()).map(|e| Export { + name: e.name.to_string(), + }); + let ty = FunctionTy { + params: module + .types + .get(f.ty()) + .params() + .iter() + .map(|p| (*p).into()) + .collect(), + results: module + .types + .get(f.ty()) + .results() + .iter() + .map(|p| (*p).into()) + .collect(), + }; + funcs.insert( f.id().index(), - FunctionTy { - params: ty.params().iter().map(|p| (*p).into()).collect(), - results: ty.results().iter().map(|p| (*p).into()).collect(), + Function { + import, + export, + ty, + bodys: vec![], + results: vec![], }, ); } - for i in module.imports.iter() { - match i.kind { - walrus::ImportKind::Function(f) => { - let _ty = module.types.get(module.funcs.get(f).ty()); - func_imports.insert( - f.index() as i32, - Function { - module: i.module.to_string(), - name: i.name.to_string(), - bodys: vec![], - results: vec![], - }, - ); - } - walrus::ImportKind::Table(tid) => { - let table = module.tables.get(tid); - table_imports.insert( - tid.index(), - Table { - module: i.module.to_string(), - name: i.name.to_string(), - // want to replace anyfunc through t.refType but it holds the wrong string ('funcref') - element: "anyfunc".to_string(), - initial: table.initial, - maximum: table.maximum, - }, - ); - } - walrus::ImportKind::Memory(mid) => { - let m = module.memories.get(mid); - mem_imports.insert( - mid.index(), - Memory { - module: i.module.to_string(), - name: i.name.to_string(), - initial: m.initial, - maximum: m.maximum, - }, - ); - } - // Global is handled by the trace. - walrus::ImportKind::Global(_) => {} - } + for g in module.globals.iter() { + let import = match g.kind { + walrus::GlobalKind::Import(i) => Some(Import { + module: module.imports.get(i).module.to_string(), + name: module.imports.get(i).name.to_string(), + }), + walrus::GlobalKind::Local(_) => None, + }; + let export = module.exports.get_exported_global(g.id()).map(|e| Export { + name: e.name.to_string(), + }); + globals.insert( + g.id().index(), + Global { + import, + export, + valtype: g.ty.into(), + mutable: g.mutable, + // this is wrong, which will be updated by ImportGlobal + // TODO: more elegant solution + initial: F64(0.0), + }, + ); + } + + for t in module.tables.iter() { + let import = match t.import { + Some(i) => Some(Import { + module: module.imports.get(i).module.to_string(), + name: module.imports.get(i).name.to_string(), + }), + None => None, + }; + let export = module.exports.get_exported_table(t.id()).map(|e| Export { + name: e.name.to_string(), + }); + tables.insert( + t.id().index(), + Table { + import, + export, + reftype: RefType::Anyref, + initial: t.initial, + maximum: t.maximum, + }, + ); + } + + for m in module.memories.iter() { + let import = match m.import { + Some(i) => Some(Import { + module: module.imports.get(i).module.to_string(), + name: module.imports.get(i).name.to_string(), + }), + None => None, + }; + let export = module.exports.get_exported_memory(m.id()).map(|e| Export { + name: e.name.to_string(), + }); + mems.insert( + m.id().index(), + Memory { + import, + export, + initial: m.initial, + maximum: m.maximum, + }, + ); } Self { replay: Replay { - func_imports, - mem_imports, - table_imports, - func_idx_to_ty, - global_imports: BTreeMap::new(), - modules: Vec::new(), + funcs, + tables, + mems, + globals, + module, }, - // -1 is the _start function + // INIT_INDEX is the _start function state: State { - host_call_stack: vec![-1], // - last_func: -1, + host_call_stack: vec![INIT_INDEX], // + last_func: INIT_INDEX, }, + flag: true, } } @@ -219,10 +367,13 @@ impl IRGenerator { } fn push_call(&mut self, event: HostEvent) { + self.flag = true; let idx = self.state.host_call_stack.last().unwrap(); let current_context = self.idx_to_cxt(*idx); - current_context.push(event.clone()); + if let Some(current_context) = current_context { + current_context.push(event.clone()) + } } fn consume_event(&mut self, event: &WasmEvent) { @@ -255,7 +406,7 @@ impl IRGenerator { data, } => { self.splice_event(HostEvent::MutateMemory { - import: self.replay.mem_imports.contains_key(&idx), + import: self.replay.imported_mems().contains_key(&idx), name: name.clone(), addr: *offset, data: data.clone(), @@ -263,7 +414,7 @@ impl IRGenerator { } WasmEvent::MemGrow { idx, name, amount } => { self.splice_event(HostEvent::GrowMemory { - import: self.replay.mem_imports.contains_key(idx), + import: self.replay.imported_mems().contains_key(idx), name: name.clone(), amount: *amount, }); @@ -278,16 +429,16 @@ impl IRGenerator { self.splice_event(HostEvent::MutateTable { tableidx: *tableidx, funcidx: *funcidx, - import: self.replay.table_imports.contains_key(&tableidx), + import: self.replay.imported_tables().contains_key(&tableidx), name: name.clone(), idx: *idx, - func_import: self.replay.func_imports.contains_key(funcidx), + func_import: self.replay.imported_funcs().contains_key(funcidx), func_name: funcname.clone(), }); } WasmEvent::TableGrow { idx, name, amount } => { self.splice_event(HostEvent::GrowTable { - import: self.replay.table_imports.contains_key(idx), + import: self.replay.imported_tables().contains_key(idx), name: name.clone(), idx: *idx, amount: *amount, @@ -301,7 +452,7 @@ impl IRGenerator { } => { self.splice_event(HostEvent::MutateGlobal { idx: *idx, - import: self.replay.global_imports.contains_key(&idx), + import: self.replay.imported_globals().contains_key(&idx), name: name.clone(), value: *value, valtype: valtype.clone(), @@ -310,11 +461,11 @@ impl IRGenerator { WasmEvent::ImportCall { idx, name: _name } => { self.replay - .func_imports + .funcs .get_mut(idx) .unwrap() .bodys - .push(vec![]); + .push(Some(vec![])); self.state.host_call_stack.push(*idx); self.state.last_func = *idx; } @@ -323,13 +474,9 @@ impl IRGenerator { name: _name, results, } => { + self.flag = false; let current_fn_idx = self.state.host_call_stack.last().unwrap(); - let r = &mut self - .replay - .func_imports - .get_mut(¤t_fn_idx) - .unwrap() - .results; + let r = &mut self.replay.funcs.get_mut(¤t_fn_idx).unwrap().results; r.push(WriteResult { results: results.clone(), reps: 1, @@ -338,44 +485,39 @@ impl IRGenerator { } WasmEvent::ImportGlobal { idx, - module, - name, - mutable, + module: _, + name: _, + mutable: _, initial, - value, - } => { - self.replay.global_imports.insert( - *idx, - Global { - module: module.clone(), - name: name.clone(), - value: value.clone(), - initial: initial.clone(), - mutable: *mutable, - }, - ); - } + value: _, + } => match self.replay.globals.get_mut(idx) { + Some(g) => g.initial = *initial, + None => todo!(), + }, } } fn splice_event(&mut self, event: HostEvent) { + let flag = self.flag; let idx = self.state.host_call_stack.last().unwrap(); - let last_idx = self.state.last_func; - let last_import_call = *idx == last_idx; let current_context = self.idx_to_cxt(*idx); - if last_import_call { - current_context.insert(current_context.len() - 1, event); + if flag { + if let Some(current_context) = current_context { + current_context.insert(current_context.len() - 1, event) + } } else { let last_idx = &self.state.last_func; let last_context = self.idx_to_cxt(*last_idx); - last_context.push(event.clone()); + if let Some(last_context) = last_context { + last_context.push(event.clone()) + } } } - fn idx_to_cxt(&mut self, idx: i32) -> &mut Vec { + fn idx_to_cxt(&mut self, idx: usize) -> &mut Option> { let current_context = self .replay - .func_imports + .funcs .get_mut(&idx) .unwrap() .bodys @@ -383,10 +525,4 @@ impl IRGenerator { .unwrap(); current_context } - - // fn add_module(&mut self, module: &String) { - // if !self.replay.modules.contains(module) { - // self.replay.modules.push(module.clone()); - // } - // } } diff --git a/crates/replay_gen/src/jsgen.rs b/crates/replay_gen/src/jsgen.rs new file mode 100644 index 00000000..bdc5ca7f --- /dev/null +++ b/crates/replay_gen/src/jsgen.rs @@ -0,0 +1,321 @@ +use std::io::Write; +use std::{fs::File, path::Path}; + +use crate::irgen::{HostEvent, Import, INIT_INDEX}; +use crate::trace::ValType; +use crate::{ + irgen::{Context, Replay, WriteResult}, + trace::F64, + write, +}; + +pub fn generate_replay_javascript(out_path: &Path, code: &Replay) -> std::io::Result<()> { + let mut file = File::create(&out_path)?; + let stream = &mut file; + 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.imported_modules() { + write(stream, &format!("{}\n", write_module(module)))?; + } + // Init memories + for (_i, mem) in &code.imported_mems() { + let Import { module, name } = mem.import.clone().unwrap(); + write( + stream, + &format!( + "const {name} = new WebAssembly.Memory({{ initial: {}, maximum: {} }})\n", + mem.initial, + match mem.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + } + ), + )?; + write(stream, &format!("{}{name}\n", write_import(&module, &name)))?; + } + // Init globals + for (_i, global) in &code.imported_globals() { + let Import { module, name } = global.import.clone().unwrap(); + if global.initial.0.is_nan() || !global.initial.0.is_finite() { + if name.to_lowercase() == "infinity" { + write( + stream, + &format!("{}Infinity\n", write_import(&module, &name)), + )?; + } else if name.to_lowercase() == "nan" { + write(stream, &format!("{}NaN\n", write_import(&module, &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 {name} = new WebAssembly.Global({{ value: '{}', mutable: {}}}, {})\n", + global.valtype, global.mutable, global.initial + ), + )?; + write(stream, &format!("{}{name}\n", write_import(&module, &name)))?; + } + } + // Init tables + for (_i, table) in &code.imported_tables() { + let Import { module, name } = table.import.clone().unwrap(); + write( + stream, + &format!( + "const {name} = new WebAssembly.Table({{ initial: {}, maximum: {}, element: '{}'}})\n", + table.initial, + match table.maximum { + Some(max) => max.to_string(), + None => "undefined".to_string(), + }, + table.reftype + ), + )?; + write( + stream, + &format!("{}{name}\n", write_import(&module, &name),), + )?; + } + // Imported functions + for (funcidx, func) in &code.imported_funcs() { + let Import { module, name } = func.import.clone().unwrap(); + // TODO: better handling of initialization + if *funcidx == INIT_INDEX { + continue; + } + write(stream, &format!("let {} = 0\n", write_func_global(funcidx)))?; + write( + stream, + &format!("{}() => {{\n", write_import(&module, &name)), + )?; + if !func.bodys.is_empty() { + write( + stream, + &format!("switch ({}) {{\n", write_func_global(funcidx)), + )?; + for (i, body) in func.bodys.iter().enumerate() { + if let Some(body) = body { + write_body(stream, body, i)? + } + } + write(stream, "}\n")?; + } + write(stream, &format!("{}++\n", write_func_global(funcidx)))?; + write_results(stream, &func.results, &write_func_global(funcidx))?; + write(stream, "}\n")?; + } + write(stream, "export function replay(wasm) {")?; + write(stream, "instance = wasm.instance\n")?; + let initialization = code.funcs.get(&INIT_INDEX).unwrap().bodys.last().unwrap(); + if let Some(initialization) = initialization { + for event in initialization { + let str = hostevent_to_js(&event); + writeln!(stream, "{}", str)?; + } + } + 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_module(module: &str) -> String { + format!("imports['{}'] = {{}}", module) +} + +fn write_func_global(funcidx: &usize) -> String { + format!("global_{}", funcidx.to_string()) +} + +fn write_import(module: &str, name: &str) -> String { + format!("imports['{}']['{}'] = ", module, name) +} + +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 + 1, + func_global, + new_c + 1 + )?; + 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_body(stream: &mut File, b: &Context, i: usize) -> std::io::Result<()> { + if !b.is_empty() { + writeln!(stream, "case {}:", i)?; + for event in b { + let str = hostevent_to_js(event); + writeln!(stream, "{}", str)?; + } + writeln!(stream, "break")?; + } + Ok(()) +} + +fn hostevent_to_js(event: &HostEvent) -> String { + fn write_params_string(params: &[F64]) -> String { + params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + } + let str = match event { + HostEvent::ExportCall { + idx: _, + name, + params, + } => { + format!( + "instance.exports.{}({})\n", + name, + write_params_string(¶ms) + ) + } + HostEvent::ExportCallTable { + idx: _, + table_name, + funcidx, + params, + } => { + format!( + "instance.exports.{}.get({})({})\n", + table_name, + funcidx, + write_params_string(¶ms) + ) + } + HostEvent::MutateMemory { + addr, + data, + import, + name, + } => { + let mut js_string = String::new(); + for (j, byte) in data.iter().enumerate() { + if *import { + js_string += &format!( + "new Uint8Array({}.buffer)[{}] = {}\n", + name, + addr + j as i32, + byte + ); + } else { + js_string += &format!( + "new Uint8Array(instance.exports.{}.buffer)[{}] = {}\n", + name, + addr + j as i32, + byte + ); + } + } + js_string + } + HostEvent::GrowMemory { + amount, + import, + name, + } => { + if *import { + format!("{}.grow({})\n", name, amount) + } else { + format!("instance.exports.{}.grow({})\n", name, amount) + } + } + HostEvent::MutateTable { + tableidx: _, + funcidx: _, + idx, + func_import, + func_name, + import, + name, + } => { + let mut js_string = if *import { + format!("{}.set({}, ", name, idx) + } else { + format!("instance.exports.{}.set({}, ", name, idx) + }; + if *func_import { + js_string.push_str(&func_name); + } else { + js_string.push_str(&format!("instance.exports.{}", func_name)); + } + js_string.push_str(")\n"); + js_string + } + HostEvent::GrowTable { + idx: _, + amount, + import, + name, + } => { + if *import { + format!("{}.grow({})\n", name, amount) + } else { + format!("instance.exports.{}.grow({})\n", name, amount) + } + } + HostEvent::MutateGlobal { + idx: _, + value, + valtype, + import, + name, + } => { + if *import { + format!("{}.value = {}\n", name, value) + } else { + format!( + "instance.exports.{}.value = {}\n", + name, + if *valtype == ValType::I64 { + format!("BigInt({})", value) + } else { + value.to_string() + } + ) + } + } + }; + str +} diff --git a/crates/replay_gen/src/lib.rs b/crates/replay_gen/src/lib.rs index 1adade99..5b98c8c0 100644 --- a/crates/replay_gen/src/lib.rs +++ b/crates/replay_gen/src/lib.rs @@ -1,15 +1,25 @@ -pub mod codegen; +use std::{fs::File, io::Write}; + pub mod irgen; +pub mod jsgen; pub mod opt; pub mod trace; +pub mod wasmgen; + +pub 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(()) +} #[cfg(test)] mod tests { use core::panic; - use crate::{ - trace::encode_trace, - }; + use crate::trace::encode_trace; #[test] fn trace_decode_encode_same() -> std::io::Result<()> { @@ -97,10 +107,8 @@ mod tests { use std::io; use std::io::BufRead; use std::io::Read; - use std::path::Path; - fn visit_dirs(dir: &Path) -> io::Result<()> { if dir.is_dir() { diff --git a/crates/replay_gen/src/main.rs b/crates/replay_gen/src/main.rs index b95ee29d..984a6edd 100644 --- a/crates/replay_gen/src/main.rs +++ b/crates/replay_gen/src/main.rs @@ -4,10 +4,11 @@ use std::path::Path; use std::{env, fs}; -use replay_gen::codegen::{generate_javascript, generate_standalone}; use replay_gen::irgen::IRGenerator; -use replay_gen::opt::merge_fn_results; +use replay_gen::jsgen::generate_replay_javascript; +use replay_gen::opt::{discard_empty_body, merge_fn_results, split_big_body}; use replay_gen::trace; +use replay_gen::wasmgen::generate_replay_wasm; use walrus::Module; fn main() -> io::Result<()> { @@ -16,7 +17,7 @@ fn main() -> io::Result<()> { let trace_path = Path::new(&args[1]); let wasm_path = Path::new(&args[2]); let binding = args.get(3); - let js_path = match &binding { + let replay_path = match &binding { Some(str) => Some(Path::new(str)), None => None, }; @@ -36,19 +37,24 @@ fn main() -> io::Result<()> { trace.push(event); } + // irgen phase let buffer = &fs::read(wasm_path).unwrap(); let module = Module::from_buffer(buffer).unwrap(); let mut generator = IRGenerator::new(module); generator.generate_replay(&trace); - // opt paths + // opt phase merge_fn_results(&mut generator.replay); - - let is_standalone = js_path.is_none(); - if is_standalone { - generate_standalone(wasm_path, &generator.replay)?; + discard_empty_body(&mut generator.replay); + + // codegen phase + let is_standalone = replay_path.is_none(); + let is_replay_wasm = !is_standalone && replay_path.unwrap().extension().unwrap() == "wasm"; + if is_replay_wasm { + split_big_body(&mut generator.replay); // works only for wasm + generate_replay_wasm(replay_path.unwrap(), &generator.replay)?; } else { - generate_javascript(js_path.unwrap(), &generator.replay)?; + generate_replay_javascript(replay_path.unwrap(), &generator.replay)?; } Ok(()) diff --git a/crates/replay_gen/src/opt.rs b/crates/replay_gen/src/opt.rs index 3c1bfa98..874abca1 100644 --- a/crates/replay_gen/src/opt.rs +++ b/crates/replay_gen/src/opt.rs @@ -1,7 +1,9 @@ -use crate::irgen::{Replay, WriteResult}; +use std::collections::BTreeMap; + +use crate::irgen::{Context, Function, FunctionTy, HostEvent, Import, Replay, WriteResult}; pub fn merge_fn_results(replay: &mut Replay) { - for (_i, f) in &mut replay.func_imports { + for (_i, f) in &mut replay.funcs { let mut new_results: Vec = vec![]; for v in &f.results { match new_results.last() { @@ -12,3 +14,75 @@ pub fn merge_fn_results(replay: &mut Replay) { f.results = new_results; } } + +pub fn discard_empty_body(replay: &mut Replay) { + for (_i, f) in &mut replay.funcs { + let mut new_bodys = vec![]; + for context in &f.bodys { + match context { + Some(context) if context.len() == 0 => new_bodys.push(None), + Some(context) => new_bodys.push(Some(context.clone())), + None => new_bodys.push(None), + }; + } + f.bodys = new_bodys; + } +} + +pub fn split_big_body(replay: &mut Replay) { + let mut new_funcs: BTreeMap = BTreeMap::new(); + + let last_key_value = replay.funcs.last_key_value().unwrap().0; + let mut idx = 1; + for (i, f) in &replay.funcs { + let mut new_bodys: Vec> = vec![]; + for context in &f.bodys { + match context { + Some(context) if context.len() > 100000 => { + let mut sliced_context = vec![]; + context.chunks(100000).for_each(|chunk| { + let unused_key = last_key_value + idx; + idx += 1; + + sliced_context.push(HostEvent::ExportCall { + idx: unused_key, + name: unused_key.to_string(), + params: vec![], + }); + new_funcs.insert( + unused_key, + Function { + import: Some(Import { + module: f.import.clone().unwrap().module, + name: unused_key.to_string(), + }), + export: None, + ty: FunctionTy { + params: vec![], + results: vec![], + }, + results: vec![], + bodys: vec![Some(chunk.to_vec())], + }, + ); + }); + + new_bodys.push(Some(sliced_context)) + } + _ => new_bodys.push(context.clone()), + }; + } + new_funcs.insert( + *i, + Function { + import: f.import.clone(), + export: f.export.clone(), + ty: f.ty.clone(), + results: f.results.clone(), + bodys: new_bodys, + }, + ); + } + + replay.funcs = new_funcs; +} diff --git a/crates/replay_gen/src/trace.rs b/crates/replay_gen/src/trace.rs index feac1b88..f26fe1fe 100644 --- a/crates/replay_gen/src/trace.rs +++ b/crates/replay_gen/src/trace.rs @@ -3,8 +3,8 @@ //! Most usually corresponds to one wasm instruction, e.g. WasmEvent::Load corresponds to one wasm load, //! but some of them are not. e.g. FuncEntry and FuncReturn correspond to the entry and return of a wasm function. //! There are also some events that are not part of the execution like Import*, which can be removed later. -use std::fmt::Debug; use std::fmt::{self, Write}; +use std::fmt::{Debug, Display}; use std::str::FromStr; pub type Trace = Vec; @@ -26,7 +26,7 @@ pub enum WasmEvent { tableidx: usize, name: String, idx: i32, - funcidx: i32, + funcidx: usize, funcname: String, }, TableGrow { @@ -41,7 +41,7 @@ pub enum WasmEvent { valtype: ValType, }, ImportCall { - idx: i32, + idx: usize, name: String, }, ImportReturn { @@ -51,12 +51,12 @@ pub enum WasmEvent { }, // These do not correspond to a wasm instruction, but used to track control flow FuncEntry { - idx: i32, + idx: usize, name: String, params: Vec, }, FuncEntryTable { - idx: i32, + idx: usize, tablename: String, tableidx: i32, params: Vec, @@ -89,7 +89,7 @@ impl fmt::Display for F64 { if self.0.is_infinite() { write!(f, "Infinity") } else if self.0.is_nan() { - write!(f, "nan") + write!(f, "NaN") } else { write!(f, "{}", self.0) } @@ -120,7 +120,7 @@ impl std::str::FromStr for F64 { } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub enum ValType { I32, I64, @@ -131,7 +131,24 @@ pub enum ValType { Externref, } -impl Debug for ValType { +#[derive(Clone, PartialEq)] +pub enum RefType { + Anyref, +} +impl fmt::Display for RefType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "anyfunc") + } +} +// TODO: make this more elegant +// This is currently done for outputting to WAT. +impl fmt::Debug for RefType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "funcref") + } +} + +impl Display for ValType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::I32 => write!(f, "i32"), @@ -328,7 +345,7 @@ impl Debug for WasmEvent { value, valtype, } => { - write!(f, "G;{};{};{};{:?}", idx, name, value, valtype) + write!(f, "G;{};{};{};{}", idx, name, value, valtype) } WasmEvent::FuncEntry { name, params, idx } => { write!(f, "EC{};{};{}", idx, name, join_vec(params)) @@ -361,7 +378,7 @@ impl Debug for WasmEvent { } => { write!( f, - "IG;{};{};{};{:?};{};{}", + "IG;{};{};{};{};{};{}", idx, module, name, diff --git a/crates/replay_gen/src/wasmgen.rs b/crates/replay_gen/src/wasmgen.rs new file mode 100644 index 00000000..e2b75a17 --- /dev/null +++ b/crates/replay_gen/src/wasmgen.rs @@ -0,0 +1,500 @@ +use std::collections::{BTreeMap, HashSet}; +use std::io::Write; +use std::process::Command; +use std::vec; +use std::{fs::File, path::Path}; + +use crate::irgen::{FunctionTy, HostEvent, INIT_INDEX}; +use crate::trace::{ValType, F64}; +use crate::{irgen::Replay, write}; + +pub fn generate_replay_wasm(replay_path: &Path, code: &Replay) -> std::io::Result<()> { + let mut module_set: HashSet<&String> = code + .module + .imports + .iter() + .map(|import| &import.module) + .collect(); + let binding = "main".to_string(); + module_set.insert(&binding); + for current_module in module_set.clone() { + let module_wat_path = replay_path + .parent() + .unwrap() + .join(&format!("{current_module}.wat")); + let mut module_wat_file = File::create(&module_wat_path)?; + let stream = &mut module_wat_file; + write(stream, "(module\n")?; + + let mut data_segments: Vec> = vec![]; + + // Import part + for (_memidx, memory) in &code.exported_mems() { + let name = memory.export.clone().unwrap().name.clone(); + let initial = memory.initial; + let maximum = match memory.maximum { + Some(max) => max.to_string(), + None => "".to_string(), + }; + write( + stream, + &format!("(import \"index\" \"{name}\" (memory {initial} {maximum}))\n",), + )?; + } + + for (_globalidx, global) in &code.exported_globals() { + let name = global.export.clone().unwrap().name.clone(); + let valtype = global.valtype.clone(); + let typedecl = match global.mutable { + true => format!("(mut {valtype})"), + false => format!("{valtype}"), + }; + + write( + stream, + &format!("(import \"index\" \"{name}\" (global ${name} {typedecl}))\n",), + )?; + } + + for (_tableidx, table) in &code.exported_tables() { + let name = table.export.clone().unwrap().name.clone(); + let initial = table.initial; + let reftype = &table.reftype; + + write( + stream, + &format!("(import \"index\" \"{name}\" (table ${name} {initial} {reftype:?}))\n",), + )?; + } + + for (_funcidx, func) in &code.exported_funcs() { + let name = func.export.clone().unwrap().name.clone(); + + let paramtys: Vec = func.ty.params.iter().map(|v| format!("{}", v)).collect(); + let paramtys = paramtys.join(" "); + let resulttys: Vec = func.ty.results.iter().map(|v| format!("{}", v)).collect(); + let resulttys = resulttys.join(" "); + write( + stream, + &format!("(import \"index\" \"{name}\" (func ${name} (param {paramtys}) (result {resulttys})))\n",), + )?; + } + let func_names = code + .exported_funcs() + .iter() + .map(|(_i, f)| format!("${}", f.export.clone().unwrap().name.clone())) + .collect::>() + .join(" "); + write(stream, &format!("(elem declare func {func_names})\n",))?; + + // Export part + // memories + for (_i, memory) in &code.imported_mems() { + let import = memory.import.clone().unwrap(); + let module = import.module.clone(); + let name = import.name.clone(); + let initial = memory.initial; + let maximum = match memory.maximum { + Some(max) => max.to_string(), + None => "".to_string(), + }; + if module == *current_module { + write( + stream, + &format!("(memory (export \"{name}\") {initial} {maximum})\n",), + )?; + } else { + write( + stream, + &format!("(import \"{module}\" \"{name}\" (memory {initial} {maximum}))\n",), + )?; + } + } + for (_i, global) in &code.imported_globals() { + let import = global.import.clone().unwrap(); + if import.module != *current_module { + continue; + } + let name = import.name.clone(); + let valtype = global.valtype.clone(); + let initial = global.initial; + write( + stream, + &format!("(global (export \"{name}\") {valtype} ({valtype}.const {initial:?}))\n",), + )?; + } + // tables + for (_i, table) in &code.imported_tables() { + let import = table.import.clone().unwrap(); + if import.module != *current_module { + continue; + } + let name = import.name.clone(); + let initial = table.initial; + let maximum = match table.maximum { + Some(max) => max.to_string(), + None => "".to_string(), + }; + let reftype = table.reftype.clone(); + write( + stream, + &format!("(table (export \"{name}\") {initial} {maximum} {reftype:?})\n",), + )?; + } + // functions + + for (funcidx, func) in &code.imported_funcs() { + let import = func.import.clone().unwrap(); + if import.module != *current_module { + continue; + } + // TODO: better handling of initialization + if *funcidx == INIT_INDEX { + continue; + } + let funcidx = *funcidx; + let name = func.import.clone().unwrap().name.clone(); + let global_idx = format!("$global_{}", funcidx.to_string()); + let func = code.funcs.get(&funcidx).unwrap(); + write( + stream, + &format!("(global {global_idx} (mut i64) (i64.const 0))\n"), + )?; + let tystr = get_functy_strs(&func.ty); + write( + stream, + &format!("(func ${name} (export \"{name}\") {tystr}\n",), + )?; + for (i, body) in func.bodys.iter().enumerate() { + if let Some(body) = body { + let mut bodystr = String::new(); + let mut memory_writes = BTreeMap::new(); + for event in body { + match event { + HostEvent::MutateMemory { + addr, + data, + import: _, + name: _, + } => { + memory_writes.insert(addr, data); + } + _ => bodystr += &hostevent_to_wat(event, code), + } + } + if memory_writes.len() > 0 { + merge_memory_writes(&mut bodystr, memory_writes, &mut data_segments); + } + write( + stream, + &format!( + "(if + (i64.eq (global.get {global_idx}) (i64.const {i})) + (then {bodystr}))\n" + ), + )?; + } + } + write( + stream, + &format!( + "(global.get {global_idx}) (i64.const 1) (i64.add) (global.set {global_idx})\n" + ), + )?; + let mut current = 0; + for r in func.results.iter() { + let ty = &code.funcs.get(&funcidx).unwrap().ty; + let _param_tys = ty.params.clone(); + let new_c = current + r.reps; + let c1 = current + 1; + let c2 = new_c + 1; + let res = match r.results.get(0) { + Some(v) => format!( + "(return ({} {v}))", + valty_to_const(ty.results.get(0).unwrap()) + ), + None => "(return)".to_owned(), + }; + write( + stream, + &format!( + " (if + (i32.and + (i64.ge_s (global.get {global_idx}) (i64.const {c1})) + (i64.lt_s (global.get {global_idx}) (i64.const {c2})) + ) + (then + {res} + ) + )" + ), + )?; + current = new_c; + } + let ty = &code.funcs.get(&funcidx).unwrap().ty; + let _param_tys = ty.params.clone(); + let default_return = match ty.results.get(0) { + Some(v) => match v { + ValType::I32 => "(i32.const 0)", + ValType::I64 => "(i64.const 0)", + ValType::F32 => "(f32.const 0)", + ValType::F64 => "(f64.const 0)", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + }, + None => "", + }; + write(stream, &format!("(return {})", default_return))?; + write(stream, ")\n")?; + } + for data_segment in data_segments { + write(stream, "(data \"")?; + for byte in data_segment { + let byte = byte.0 as usize; + write(stream, &format!("\\{byte:02x}",))?; + } + write(stream, "\")\n")?; + } + + if current_module == "main" { + let initialization = code.funcs.get(&INIT_INDEX).unwrap().bodys.last().unwrap(); + write(stream, "(func (export \"_start\") (export \"main\")\n")?; + if let Some(initialization) = initialization { + for event in initialization { + write(stream, &format!("{}", hostevent_to_wat(&event, code)))? + } + } + write(stream, "(return)\n)")?; + } + + write(stream, ")\n")?; + + let binary = wat::parse_file(module_wat_path.clone()).unwrap(); + let module_wasm_path = replay_path + .parent() + .unwrap() + .join(&format!("{current_module}.wasm")); + let mut modle_wasm_file = File::create(&module_wasm_path).unwrap(); + modle_wasm_file.write_all(&binary).unwrap(); + } + let module_args = module_set + .iter() + .map(|module| vec![format!("{}.wasm", module), module.to_string()]) + .flatten() + .collect::>(); + let args = [ + "--rename-export-conflicts", + "--enable-reference-types", + "--enable-multimemory", + "--enable-bulk-memory", + "index.wasm", + "index", + ] + .iter() + .cloned() + .chain(module_args.iter().map(|s| s.as_str())) + .chain(["-o", "merged.wasm"]); + let _output = Command::new("wasm-merge") + .current_dir(replay_path.parent().unwrap()) + .args(args) + .output() + .expect("Failed to execute wasm-merge"); + let _output = Command::new("wasm-opt") + .current_dir(replay_path.parent().unwrap()) + .args([ + "--enable-reference-types", + "--enable-gc", + "--enable-bulk-memory", + // for handling inlining of imported globals. Without this glob-merge node test will fail. + "--simplify-globals", + "merged.wasm", + "-o", + replay_path.to_str().unwrap(), + ]) + .output() + .expect("Failed to execute wasm-opt"); + + Ok(()) +} + +fn merge_memory_writes( + bodystr: &mut String, + memory_writes: BTreeMap<&i32, &Vec>, + data_segments: &mut Vec>, +) { + let mut partitions: Vec<(i32, Vec)> = Vec::new(); + let mut current_partition: Vec = Vec::new(); + let mut last_key: Option = None; + let mut start_key: Option = None; + + for (key, value) in memory_writes { + match last_key { + Some(last_key) if last_key + 1 == *key => { + current_partition.extend(value); + } + _ => { + if !current_partition.is_empty() { + partitions.push((start_key.unwrap(), current_partition)); + } + current_partition = value.clone(); + start_key = Some(*key); + } + } + last_key = Some(*key); + } + + if !current_partition.is_empty() { + partitions.push((start_key.unwrap(), current_partition)); + } + + for (start_addr, data) in partitions { + let memoryinit_threshold = 9; + if data.len() >= memoryinit_threshold { + let data_segment_idx = data_segments.len(); + let data_len = data.len(); + bodystr.push_str(&format!( + "(memory.init {data_segment_idx} (i32.const {start_addr}) (i32.const 0) (i32.const {data_len}))\n", + )); + data_segments.push(data); + } else { + // merging 4 bytes and 8 bytes is also possible + for (j, byte) in data.iter().enumerate() { + let addr = start_addr + j as i32; + bodystr.push_str(&format!( + "(i32.store8 (i32.const {addr}) (i32.const {byte}))\n", + )); + } + } + } +} + +fn valty_to_const(valty: &ValType) -> String { + match valty { + ValType::I32 => "i32.const", + ValType::I64 => "i64.const", + ValType::F32 => "f32.const", + ValType::F64 => "f64.const", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + } + .to_string() +} + +fn hostevent_to_wat(event: &HostEvent, code: &Replay) -> String { + let str = match event { + HostEvent::ExportCall { idx, name, params } => { + let func = code.funcs.get(idx).unwrap(); + + let param_tys = func.ty.params.clone(); + let result_count = func.ty.results.len(); + let params = params + .iter() + .zip(param_tys.clone()) + .map(|(p, p_ty)| format!("({} {p:?})", valty_to_const(&p_ty))) + .collect::>() + .join("\n"); + params + &format!("(call ${name})") + &("(drop)".repeat(result_count)) + } + HostEvent::ExportCallTable { + idx, + table_name: _, + funcidx, + params, + } => { + let func = code.funcs.get(idx).unwrap(); + + let param_tys = func.ty.params.clone(); + let result_tys = func.ty.results.clone(); + let tystr = get_functy_strs(&func.ty); + let params = params + .iter() + .zip(param_tys.clone()) + .map(|(p, p_ty)| format!("({} {p})", valty_to_const(&p_ty))) + .collect::>() + .join("\n"); + params + + &format!("(call_indirect {tystr} (i32.const {funcidx}))",) + + &("(drop)".repeat(result_tys.len())) + } + HostEvent::MutateMemory { + addr, + data, + import: _, + name: _, + } => { + let mut js_string = String::new(); + for (j, byte) in data.iter().enumerate() { + js_string += &format!("i32.const {}\n", addr + j as i32); + js_string += &format!("i32.const {}\n", byte); + js_string += &format!("i32.store8\n",); + } + js_string + } + HostEvent::GrowMemory { + amount, + import: _, + name: _, + } => { + format!("(memory.grow (i32.const {})) (drop)\n", amount) + } + HostEvent::MutateTable { + tableidx, + funcidx: _, + idx, + func_import: _, + func_name, + import: _, + name: _, + } => { + format!("(table.set {tableidx} (i32.const {idx}) (ref.func ${func_name}))",) + } + HostEvent::GrowTable { + idx, + amount, + import: _, + name: _, + } => { + // TODO: check if (ref.null func) is correct + format!("(table.grow {idx} (ref.null func) (i32.const {amount})) (drop)\n") + } + HostEvent::MutateGlobal { + idx, + value, + valtype, + import: _, + name, + } => { + let valtype = match valtype { + ValType::I32 => "i32.const", + ValType::I64 => "i64.const", + ValType::F32 => "f32.const", + ValType::F64 => "f64.const", + ValType::V128 => todo!(), + ValType::Anyref => todo!(), + ValType::Externref => todo!(), + }; + let value = value; + let _globalidx = idx; + format!("({valtype} {value})\n") + &format!("(global.set ${name})") + } + }; + str +} + +fn get_functy_strs(ty: &FunctionTy) -> String { + let paramstr = ty + .params + .iter() + .map(|p| format!("{}", p)) + .collect::>() + .join(" "); + let resultstr = ty + .results + .iter() + .map(|r| format!("{}", r)) + .collect::>() + .join(" "); + format!("(param {paramstr}) (result {resultstr})") +} diff --git a/package.json b/package.json index 01cd633a..5ae85034 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,13 @@ "typescript": "^5.0.0" }, "dependencies": { - "@webassemblyjs/ast": "^1.11.6", - "@webassemblyjs/wasm-edit": "^1.11.6", - "@webassemblyjs/wasm-parser": "^1.11.6", "acorn": "^8.11.2", "command-line-args": "^5.2.1", "playwright": "^1.39.0" }, "scripts": { "build": "tsc && node ./build.js", - "build-rust": "cd crates/ && cargo build --release", + "build-rust": "cd crates/ && cargo build", "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-rust && npm run build-wasabi && npm run build", "test": "npm run build-rust && npm run build && node ./dist/tests/run-tests.cjs", diff --git a/src/benchmark.cts b/src/benchmark.cts index 7f00302b..63509872 100644 --- a/src/benchmark.cts +++ b/src/benchmark.cts @@ -18,30 +18,32 @@ export default class Benchmark { private record: Record private constructor() { } - async save(benchmarkPath: string, options = { trace: false, rustBackend: false }) { + async save(benchmarkPath: string, options) { 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) => { const binPath = path.join(benchmarkPath, `bin_${i}`) await fs.mkdir(binPath) - if (options.trace === true) { - await fs.writeFile(path.join(binPath, 'trace.r3'), trace.toString()) - } + await fs.writeFile(path.join(binPath, 'trace.r3'), trace.toString()) const diskSave = path.join(binPath, `temp-trace-${i}.r3`) await fs.writeFile(diskSave, trace.toString()) await fs.writeFile(path.join(binPath, 'index.wasm'), Buffer.from(binary)) - 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, 'index.wasm')}`); - execSync(`wasm-validate ${path.join(binPath, "canned.wasm")}`) - p_measureCodeGen() - } else { + if (options.legacyBackend == true) { 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() + } else { + const p_measureCodeGen = createMeasure('rust-backend', { phase: 'replay-generation', description: `The time it takes for rust backend to generate javascript` }) + if (options.jsBackend) { + execSync(`./crates/target/debug/replay_gen ${diskSave} ${path.join(binPath, 'index.wasm')} ${path.join(binPath, 'replay.js')}`); + } else { + execSync(`./crates/target/debug/replay_gen ${diskSave} ${path.join(binPath, 'index.wasm')} ${path.join(binPath, 'replay.wasm')}`); + execSync(`wasm-tools validate -f all ${path.join(binPath, "replay.wasm")}`) + } + p_measureCodeGen() } await fs.rm(diskSave) })) diff --git a/src/cli/options.cts b/src/cli/options.cts index 839ebee0..147cac35 100644 --- a/src/cli/options.cts +++ b/src/cli/options.cts @@ -9,7 +9,7 @@ export type Options = { file: string, extended: boolean, noRecord: boolean, - rustBackend: boolean + legacyBackend: boolean } export default function getOptions() { @@ -22,15 +22,12 @@ export default function getOptions() { { name: 'file', alias: 'f', type: String }, { name: 'extended', alias: 'e', type: Boolean }, { name: 'no-record', alias: 'n', type: Boolean }, - { name: 'rustBackend', alias: 'r', type: Boolean } + { name: 'legacyBackend', alias: 'l', 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 c77ede20..f7494e43 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, rustBackend: options.rustBackend }) + Benchmark.fromAnalysisResult(results).save(options.benchmarkPath, options) } \ No newline at end of file diff --git a/src/tracer.cts b/src/tracer.cts index db45a0fa..5ca2c25b 100644 --- a/src/tracer.cts +++ b/src/tracer.cts @@ -363,7 +363,6 @@ export default class Analysis implements AnalysisI { const addr = target.addr + memarg.offset const memName = this.getName(Wasabi.module.info.memories[target.memIdx]) let byteLength = this.getByteLength(op) - const res = this.mem_content_equals(target.memIdx, addr, byteLength) if (this.options.extended === true) { let data = [] for (let i = 0; i < byteLength; i++) { @@ -371,13 +370,14 @@ export default class Analysis implements AnalysisI { } this.trace.push(`LE;${0};${memName};${addr};${data.join(',')}`) } - res.forEach((r, i) => { - if (r !== true) { + if (!this.mem_content_equals(target.memIdx, addr, byteLength)) { + for (let i = 0; i < byteLength; i++) { + let r = new Uint8Array(this.Wasabi.module.memories[0].buffer)[addr + i] new Uint8Array(this.shadowMemories[0])[addr + i] = new Uint8Array(this.Wasabi.module.memories[0].buffer)[addr + i] new Uint8Array(this.Wasabi.module.memories[0].buffer)[addr + i] = r as number this.trace.push(`L;${0};${memName};${addr + i};${[r as number]}`) } - }) + } }, global: (location, op, idx, value) => { @@ -446,17 +446,14 @@ export default class Analysis implements AnalysisI { } } - private mem_content_equals(memIdx: number, addr: number, numBytes: number): (number | boolean)[] { + private mem_content_equals(memIdx: number, addr: number, numBytes: number): boolean { let result = [] for (let i = 0; i < numBytes; i++) { const data = new Uint8Array(this.Wasabi.module.memories[0].buffer)[addr + i] - if (new Uint8Array(this.shadowMemories[memIdx])[addr + i] !== data) { - result.push(data) - } else { - result.push(true) - } + if (new Uint8Array(this.shadowMemories[memIdx])[addr + i] !== data) + return false } - return result + return true } private tableGetEvent(tableidx: number, idx: number) { diff --git a/src/wasm-generator.cts b/src/wasm-generator.cts deleted file mode 100644 index 5f195f0e..00000000 --- a/src/wasm-generator.cts +++ /dev/null @@ -1,13 +0,0 @@ -import { ReplayToWriteStream } from "./replay-generator.cjs"; -import { decode } from "@webassemblyjs/wasm-parser"; -import { traverseWithParents, addNode } from "@webassemblyjs/ast"; -import { add } from "@webassemblyjs/wasm-edit" -import fs from "fs/promises"; - -export const generateWasm: ReplayToWriteStream = async (stream, code) => { - const binary = await fs.readFile('gol/bin_0/index.wasm') - const ast = decode(binary) - const newBinary = add(binary, [ - ast.start(0) - ]) -} \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..c20a6aa3 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*/** \ No newline at end of file diff --git a/tests/run-tests.cts b/tests/run-tests.cts index 73a53bba..0bc795c2 100644 --- a/tests/run-tests.cts +++ b/tests/run-tests.cts @@ -44,7 +44,9 @@ async function runNodeTest(name: string, options): Promise { const instrumentedPath = path.join(testPath, "instrumented.wasm"); const tracePath = path.join(testPath, "trace.r3"); const callGraphPath = path.join(testPath, "call-graph.txt"); - const replayPath = path.join(testPath, "replay.js"); + const replayJsPath = path.join(testPath, "replay.js"); + const replayWasmPath = path.join(testPath, "replay.wasm"); + const mergedWasmPath = path.join(testPath, "merged.wasm"); const replayTracePath = path.join(testPath, "replay-trace.r3"); const replayCallGraphPath = path.join(testPath, "replay-call-graph.txt"); await cleanUp(testPath) @@ -99,15 +101,18 @@ async function runNodeTest(name: string, options): Promise { } let replayCode try { - if (options.rustBackend === true) { - const diskSave = path.join(testPath, `temp-trace-0.r3`) - await fs.writeFile(diskSave, traceString) - execSync(`./crates/target/release/replay_gen ${diskSave} ${wasmPath}`); - execSync(`wasm-validate ${path.join(testPath, "canned.wasm")}`) - return { testPath, success: true } - } else { + if (options.legacyBackend === true) { replayCode = await new Generator().generateReplay(trace) - await generateJavascript(fss.createWriteStream(replayPath), replayCode) + await generateJavascript(fss.createWriteStream(replayJsPath), replayCode) + } else { + if (options.jsBackend == true) { + execSync(`./crates/target/debug/replay_gen ${tracePath} ${wasmPath} ${replayJsPath}`); + } else { + execSync(`./crates/target/debug/replay_gen ${tracePath} ${wasmPath} ${replayWasmPath}`); + // we validate and early return as for single wasm accuracy test doesn't make sense + execSync(`wasm-validate ${replayWasmPath}`) + return { testPath, success: true } + } } await delay(0) // WTF why do I need this WHAT THE FUCK @@ -118,7 +123,7 @@ async function runNodeTest(name: string, options): Promise { // 4. Execute replay and generate trace and compare let replayTracer = new Tracer(eval(js + `\nWasabi`), { extended }) try { - const replayBinary = await import(replayPath) + const replayBinary = await import(replayJsPath) const wasm = await replayBinary.instantiate(wasmBinary) replayTracer.init() replayBinary.replay(wasm) @@ -185,28 +190,24 @@ async function runOnlineTests(names: string[], options) { } // ignore specific tests let filter = [ - 'visual6502remix', // takes so long and is not automated yet - 'heatmap', // takes so long - 'image-convolute', // out of memory - 'kittygame', // too slow for rust backend + 'ogv', // TODO: additional ER at end of original trace + 'heatmap', // works fine, but too long so we skip it + 'uarm', // doesn't work for js because string is too long + 'image-convolute', // asm2wasm - f64-to-int is too large ] 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, options) + await cleanUp(testPath); + const report = await testWebPage(testPath, options) stopSpinner(spinner) cleanUpPerformance() await writeReport(name, report) } } -async function runOnlineTest(testPath: string, options) { - await cleanUp(testPath) - return testWebPage(testPath, options) -} - async function runOfflineTests(names: string[], options) { if (names.length > 0) { console.log('=================') @@ -215,12 +216,17 @@ async function runOfflineTests(names: string[], options) { } // ignore specific tests let filter = [ - 'sqllite' + 'sqllite', ] names = names.filter((n) => !filter.includes(n)) for (let name of names) { const spinner = startSpinner(name) - const report = await runOfflineTest(name, options) + 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, options) + server.close() stopSpinner(spinner) await writeReport(name, report) } @@ -258,15 +264,6 @@ function startServer(websitePath: 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, options) - server.close() - return report -} async function testWebPage(testPath: string, options): Promise { const testJsPath = path.join(testPath, 'test.js') @@ -286,8 +283,8 @@ async function testWebPage(testPath: string, options): Promise { } // process.stdout.write(` -e not available`) const benchmark = Benchmark.fromAnalysisResult(analysisResult) - await benchmark.save(benchmarkPath, { trace: true, rustBackend: options.rustBackend }) - if (options.rustBackend === true) { + await benchmark.save(benchmarkPath, options) + if (options.jsBackend != true) { return { testPath, success: true } } let subBenchmarkNames = await getDirectoryNames(benchmarkPath) @@ -338,15 +335,12 @@ async function testWebPage(testPath: string, options): Promise { (async function run() { const optionDefinitions = [ - { name: 'extended', alias: 'e', type: Boolean }, + { name: 'jsBackend', alias: 'j', type: Boolean }, { name: 'category', type: String, multiple: true, defaultOption: true }, { name: 'testcases', alias: 't', type: String, multiple: true }, - { name: 'rustBackend', alias: 'r', type: Boolean } + { name: 'legacyBackend', alias: 'l', type: Boolean } ] const options = commandLineArgs(optionDefinitions) - 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) {