diff --git a/.dockerignore b/.dockerignore index 51424900e8..2d5303a3be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,6 +36,7 @@ arbitrator/tools/wasmer/target/ arbitrator/tools/wasm-tools/ arbitrator/tools/pricers/ arbitrator/tools/module_roots/ +arbitrator/tools/stylus_benchmark arbitrator/langs/rust/target/ arbitrator/langs/bf/target/ diff --git a/.github/workflows/arbitrator-ci.yml b/.github/workflows/arbitrator-ci.yml index 47646017ac..dd58a30571 100644 --- a/.github/workflows/arbitrator-ci.yml +++ b/.github/workflows/arbitrator-ci.yml @@ -171,6 +171,9 @@ jobs: - name: Rustfmt - langs/rust run: cargo fmt --all --manifest-path arbitrator/langs/rust/Cargo.toml -- --check + - name: Rustfmt - tools/stylus_benchmark + run: cargo fmt --all --manifest-path arbitrator/tools/stylus_benchmark/Cargo.toml -- --check + - name: Make proofs from test cases run: make -j test-gen-proofs diff --git a/arbitrator/Cargo.toml b/arbitrator/Cargo.toml index eaafb6e439..3c5228daf2 100644 --- a/arbitrator/Cargo.toml +++ b/arbitrator/Cargo.toml @@ -12,6 +12,7 @@ members = [ exclude = [ "stylus/tests/", "tools/wasmer/", + "tools/stylus_benchmark", ] resolver = "2" diff --git a/arbitrator/arbutil/src/benchmark.rs b/arbitrator/arbutil/src/benchmark.rs new file mode 100644 index 0000000000..580d0191a0 --- /dev/null +++ b/arbitrator/arbutil/src/benchmark.rs @@ -0,0 +1,14 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE + +use crate::evm::api::Ink; +use std::time::{Duration, Instant}; + +// Benchmark is used to track the performance of blocks of code in stylus +#[derive(Clone, Copy, Debug, Default)] +pub struct Benchmark { + pub timer: Option, + pub elapsed_total: Duration, + pub ink_start: Option, + pub ink_total: Ink, +} diff --git a/arbitrator/arbutil/src/lib.rs b/arbitrator/arbutil/src/lib.rs index 9c48a9fefc..e17e8d9448 100644 --- a/arbitrator/arbutil/src/lib.rs +++ b/arbitrator/arbutil/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +pub mod benchmark; /// cbindgen:ignore pub mod color; pub mod crypto; diff --git a/arbitrator/jit/src/lib.rs b/arbitrator/jit/src/lib.rs new file mode 100644 index 0000000000..d0ad76bd03 --- /dev/null +++ b/arbitrator/jit/src/lib.rs @@ -0,0 +1,51 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use std::path::PathBuf; +use structopt::StructOpt; + +mod arbcompress; +mod caller_env; +pub mod machine; +mod prepare; +pub mod program; +mod socket; +pub mod stylus_backend; +mod test; +mod wasip1_stub; +mod wavmio; + +#[derive(StructOpt)] +#[structopt(name = "jit-prover")] +pub struct Opts { + #[structopt(short, long)] + binary: PathBuf, + #[structopt(long, default_value = "0")] + inbox_position: u64, + #[structopt(long, default_value = "0")] + delayed_inbox_position: u64, + #[structopt(long, default_value = "0")] + position_within_message: u64, + #[structopt(long)] + last_block_hash: Option, + #[structopt(long)] + last_send_root: Option, + #[structopt(long)] + inbox: Vec, + #[structopt(long)] + delayed_inbox: Vec, + #[structopt(long)] + preimages: Option, + #[structopt(long)] + cranelift: bool, + #[structopt(long)] + forks: bool, + #[structopt(long)] + pub debug: bool, + #[structopt(long)] + pub require_success: bool, + // JSON inputs supercede any of the command-line inputs which could + // be specified in the JSON file. + #[structopt(long)] + json_inputs: Option, +} diff --git a/arbitrator/jit/src/main.rs b/arbitrator/jit/src/main.rs index 6e44500215..e19fabc250 100644 --- a/arbitrator/jit/src/main.rs +++ b/arbitrator/jit/src/main.rs @@ -1,58 +1,13 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -use crate::machine::{Escape, WasmEnv}; use arbutil::{color, Color}; use eyre::Result; -use std::path::PathBuf; +use jit::machine; +use jit::machine::{Escape, WasmEnv}; +use jit::Opts; use structopt::StructOpt; -mod arbcompress; -mod caller_env; -mod machine; -mod prepare; -mod program; -mod socket; -mod stylus_backend; -mod test; -mod wasip1_stub; -mod wavmio; - -#[derive(StructOpt)] -#[structopt(name = "jit-prover")] -pub struct Opts { - #[structopt(short, long)] - binary: PathBuf, - #[structopt(long, default_value = "0")] - inbox_position: u64, - #[structopt(long, default_value = "0")] - delayed_inbox_position: u64, - #[structopt(long, default_value = "0")] - position_within_message: u64, - #[structopt(long)] - last_block_hash: Option, - #[structopt(long)] - last_send_root: Option, - #[structopt(long)] - inbox: Vec, - #[structopt(long)] - delayed_inbox: Vec, - #[structopt(long)] - preimages: Option, - #[structopt(long)] - cranelift: bool, - #[structopt(long)] - forks: bool, - #[structopt(long)] - debug: bool, - #[structopt(long)] - require_success: bool, - // JSON inputs supercede any of the command-line inputs which could - // be specified in the JSON file. - #[structopt(long)] - json_inputs: Option, -} - fn main() -> Result<()> { let opts = Opts::from_args(); let env = match WasmEnv::cli(&opts) { diff --git a/arbitrator/jit/src/prepare.rs b/arbitrator/jit/src/prepare.rs index e7a7ba0f4d..62dd063b75 100644 --- a/arbitrator/jit/src/prepare.rs +++ b/arbitrator/jit/src/prepare.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -use crate::WasmEnv; +use crate::machine::WasmEnv; use arbutil::{Bytes32, PreimageType}; use eyre::Ok; use prover::parse_input::FileData; diff --git a/arbitrator/jit/src/program.rs b/arbitrator/jit/src/program.rs index f10a059748..d80b3771c6 100644 --- a/arbitrator/jit/src/program.rs +++ b/arbitrator/jit/src/program.rs @@ -4,8 +4,8 @@ #![allow(clippy::too_many_arguments)] use crate::caller_env::JitEnv; -use crate::machine::{Escape, MaybeEscape, WasmEnvMut}; -use crate::stylus_backend::exec_wasm; +use crate::machine::{Escape, MaybeEscape, WasmEnv, WasmEnvMut}; +use crate::stylus_backend::{exec_wasm, MessageFromCothread}; use arbutil::evm::api::Gas; use arbutil::Bytes32; use arbutil::{evm::EvmData, format::DebugBytes, heapify}; @@ -16,6 +16,7 @@ use prover::{ machine::Module, programs::{config::PricingParams, prelude::*}, }; +use std::sync::Arc; const DEFAULT_STYLUS_ARBOS_VERSION: u64 = 31; @@ -130,10 +131,6 @@ pub fn new_program( let evm_data: EvmData = unsafe { *Box::from_raw(evm_data_handler as *mut EvmData) }; let config: JitConfig = unsafe { *Box::from_raw(stylus_config_handler as *mut JitConfig) }; - // buy ink - let pricing = config.stylus.pricing; - let ink = pricing.gas_to_ink(Gas(gas)); - let Some(module) = exec.module_asms.get(&compiled_hash).cloned() else { return Err(Escape::Failure(format!( "module hash {:?} not found in {:?}", @@ -142,6 +139,21 @@ pub fn new_program( ))); }; + exec_program(exec, module, calldata, config, evm_data, gas) +} + +pub fn exec_program( + exec: &mut WasmEnv, + module: Arc<[u8]>, + calldata: Vec, + config: JitConfig, + evm_data: EvmData, + gas: u64, +) -> Result { + // buy ink + let pricing = config.stylus.pricing; + let ink = pricing.gas_to_ink(Gas(gas)); + let cothread = exec_wasm( module, calldata, @@ -162,7 +174,10 @@ pub fn new_program( /// returns request_id for the first request from the program pub fn start_program(mut env: WasmEnvMut, module: u32) -> Result { let (_, exec) = env.jit_env(); + start_program_with_wasm_env(exec, module) +} +pub fn start_program_with_wasm_env(exec: &mut WasmEnv, module: u32) -> Result { if exec.threads.len() as u32 != module || module == 0 { return Escape::hostio(format!( "got request for thread {module} but len is {}", @@ -179,13 +194,18 @@ pub fn start_program(mut env: WasmEnvMut, module: u32) -> Result { /// request_id MUST be last request id returned from start_program or send_response pub fn get_request(mut env: WasmEnvMut, id: u32, len_ptr: GuestPtr) -> Result { let (mut mem, exec) = env.jit_env(); + let msg = get_last_msg(exec, id)?; + mem.write_u32(len_ptr, msg.req_data.len() as u32); + Ok(msg.req_type) +} + +pub fn get_last_msg(exec: &mut WasmEnv, id: u32) -> Result { let thread = exec.threads.last_mut().unwrap(); let msg = thread.last_message()?; if msg.1 != id { return Escape::hostio("get_request id doesn't match"); }; - mem.write_u32(len_ptr, msg.0.req_data.len() as u32); - Ok(msg.0.req_type) + Ok(msg.0) } // gets data associated with last request. @@ -193,12 +213,8 @@ pub fn get_request(mut env: WasmEnvMut, id: u32, len_ptr: GuestPtr) -> Result MaybeEscape { let (mut mem, exec) = env.jit_env(); - let thread = exec.threads.last_mut().unwrap(); - let msg = thread.last_message()?; - if msg.1 != id { - return Escape::hostio("get_request id doesn't match"); - }; - mem.write_slice(data_ptr, &msg.0.req_data); + let msg = get_last_msg(exec, id)?; + mem.write_slice(data_ptr, &msg.req_data); Ok(()) } @@ -217,11 +233,21 @@ pub fn set_response( let result = mem.read_slice(result_ptr, result_len as usize); let raw_data = mem.read_slice(raw_data_ptr, raw_data_len as usize); + set_response_with_wasm_env(exec, id, gas, result, raw_data) +} + +pub fn set_response_with_wasm_env( + exec: &mut WasmEnv, + id: u32, + gas: u64, + result: Vec, + raw_data: Vec, +) -> MaybeEscape { let thread = exec.threads.last_mut().unwrap(); thread.set_response(id, result, raw_data, Gas(gas)) } -/// sends previos response +/// sends previous response /// MUST be called right after set_response to the same id /// returns request_id for the next request pub fn send_response(mut env: WasmEnvMut, req_id: u32) -> Result { @@ -239,7 +265,10 @@ pub fn send_response(mut env: WasmEnvMut, req_id: u32) -> Result { /// removes the last created program pub fn pop(mut env: WasmEnvMut) -> MaybeEscape { let (_, exec) = env.jit_env(); + pop_with_wasm_env(exec) +} +pub fn pop_with_wasm_env(exec: &mut WasmEnv) -> MaybeEscape { match exec.threads.pop() { None => Err(Escape::Child(eyre!("no child"))), Some(mut thread) => thread.wait_done(), @@ -247,8 +276,8 @@ pub fn pop(mut env: WasmEnvMut) -> MaybeEscape { } pub struct JitConfig { - stylus: StylusConfig, - compile: CompileConfig, + pub stylus: StylusConfig, + pub compile: CompileConfig, } /// Creates a `StylusConfig` from its component parts. diff --git a/arbitrator/jit/src/stylus_backend.rs b/arbitrator/jit/src/stylus_backend.rs index 0d8c477c6c..d250780dd9 100644 --- a/arbitrator/jit/src/stylus_backend.rs +++ b/arbitrator/jit/src/stylus_backend.rs @@ -4,6 +4,7 @@ #![allow(clippy::too_many_arguments)] use crate::machine::{Escape, MaybeEscape}; +use arbutil::benchmark::Benchmark; use arbutil::evm::api::{Gas, Ink, VecReader}; use arbutil::evm::{ api::{EvmApiMethod, EVM_API_METHOD_REQ_OFFSET}, @@ -35,6 +36,7 @@ struct MessageToCothread { pub struct MessageFromCothread { pub req_type: u32, pub req_data: Vec, + pub benchmark: Benchmark, } struct CothreadRequestor { @@ -51,6 +53,7 @@ impl RequestHandler for CothreadRequestor { let msg = MessageFromCothread { req_type: req_type as u32 + EVM_API_METHOD_REQ_OFFSET, req_data: req_data.as_ref().to_vec(), + benchmark: Benchmark::default(), }; if let Err(error) = self.tx.send(msg) { @@ -169,6 +172,7 @@ pub fn exec_wasm( let msg = MessageFromCothread { req_data: output, req_type: out_kind as u32, + benchmark: instance.env().benchmark, }; instance .env_mut() diff --git a/arbitrator/stylus/src/env.rs b/arbitrator/stylus/src/env.rs index a153fb5bf1..a2c8189029 100644 --- a/arbitrator/stylus/src/env.rs +++ b/arbitrator/stylus/src/env.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, evm::{ api::{DataReader, EvmApi, Ink}, EvmData, @@ -48,6 +49,8 @@ pub struct WasmEnv> { pub compile: CompileConfig, /// The runtime config pub config: Option, + // Used to benchmark execution blocks of code + pub benchmark: Benchmark, // Using the unused generic parameter D in a PhantomData field _data_reader_marker: PhantomData, } @@ -68,6 +71,7 @@ impl> WasmEnv { outs: vec![], memory: None, meter: None, + benchmark: Benchmark::default(), _data_reader_marker: PhantomData, } } diff --git a/arbitrator/stylus/src/host.rs b/arbitrator/stylus/src/host.rs index c72cafc316..67497302a1 100644 --- a/arbitrator/stylus/src/host.rs +++ b/arbitrator/stylus/src/host.rs @@ -5,6 +5,7 @@ use crate::env::{Escape, HostioInfo, MaybeEscape, WasmEnv, WasmEnvMut}; use arbutil::{ + benchmark::Benchmark, evm::{ api::{DataReader, EvmApi, Gas, Ink}, EvmData, @@ -46,6 +47,10 @@ where &self.evm_data } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.env.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { &mut self.evm_data.return_data_len } @@ -464,3 +469,13 @@ pub(crate) fn console_tee, T: Into + Copy>( } pub(crate) fn null_host>(_: WasmEnvMut) {} + +pub(crate) fn start_benchmark>( + mut env: WasmEnvMut, +) -> MaybeEscape { + hostio!(env, start_benchmark()) +} + +pub(crate) fn end_benchmark>(mut env: WasmEnvMut) -> MaybeEscape { + hostio!(env, end_benchmark()) +} diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index 0fbdb342f3..a31df1034c 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -212,6 +212,8 @@ impl> NativeInstance { imports.define("console", "tee_f32", func!(host::console_tee::)); imports.define("console", "tee_f64", func!(host::console_tee::)); imports.define("debug", "null_host", func!(host::null_host)); + imports.define("debug", "start_benchmark", func!(host::start_benchmark)); + imports.define("debug", "end_benchmark", func!(host::end_benchmark)); } let instance = Instance::new(&mut store, &module, &imports)?; let exports = &instance.exports; @@ -429,6 +431,8 @@ pub fn module(wasm: &[u8], compile: CompileConfig, target: Target) -> Result) { + let _ = match str::from_utf8(req_data) { + Ok(v) => v, + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }; + + match req_type { + 0 => return, + 1 => panic!("ErrExecutionReverted user revert"), + 2 => panic!("ErrExecutionReverted user failure"), + 3 => panic!("ErrOutOfGas user out of ink"), + 4 => panic!("ErrDepth user out of stack"), + _ => panic!("ErrExecutionReverted user unknown"), + } +} + +fn run(compiled_module: Vec) -> (Duration, Ink) { + let calldata = Vec::from([0u8; 32]); + let evm_data = EvmData::default(); + let config = JitConfig { + stylus: StylusConfig { + version: 2, + max_depth: 10000, + pricing: PricingParams { ink_price: 1 }, + }, + compile: CompileConfig::version(2, true), + }; + + let exec = &mut WasmEnv::default(); + + let module = jit::program::exec_program( + exec, + compiled_module.into(), + calldata, + config, + evm_data, + u64::MAX, + ) + .unwrap(); + + let req_id = jit::program::start_program_with_wasm_env(exec, module).unwrap(); + let msg = jit::program::get_last_msg(exec, req_id).unwrap(); + if msg.req_type < EVM_API_METHOD_REQ_OFFSET { + let _ = jit::program::pop_with_wasm_env(exec); + + let req_data = msg.req_data[8..].to_vec(); + check_result(msg.req_type, &req_data); + } else { + panic!("unsupported request type {:?}", msg.req_type); + } + + (msg.benchmark.elapsed_total, msg.benchmark.ink_total) +} + +pub fn benchmark(wat: Vec) -> eyre::Result<()> { + let wasm = wasmer::wat2wasm(&wat)?; + + let compiled_module = native::compile(&wasm, 2, true, Target::default())?; + + let mut durations: Vec = Vec::new(); + let mut ink_spent = Ink(0); + for i in 0..NUMBER_OF_BENCHMARK_RUNS { + print!("Run {:?}, ", i); + let (duration_run, ink_spent_run) = run(compiled_module.clone()); + durations.push(duration_run); + ink_spent = ink_spent_run; + println!( + "duration: {:?}, ink_spent: {:?}", + duration_run, ink_spent_run + ); + } + + // discard top and bottom runs + durations.sort(); + let l = NUMBER_OF_TOP_AND_BOTTOM_RUNS_TO_DISCARD as usize; + let r = NUMBER_OF_BENCHMARK_RUNS as usize - NUMBER_OF_TOP_AND_BOTTOM_RUNS_TO_DISCARD as usize; + durations = durations[l..r].to_vec(); + + let avg_duration = durations.iter().sum::() / (r - l) as u32; + let avg_ink_spent_per_micro_second = ink_spent.0 / avg_duration.as_micros() as u64; + println!("After discarding top and bottom runs: "); + println!( + "avg_duration: {:?}, avg_ink_spent_per_micro_second: {:?}", + avg_duration, avg_ink_spent_per_micro_second + ); + + Ok(()) +} diff --git a/arbitrator/tools/stylus_benchmark/src/main.rs b/arbitrator/tools/stylus_benchmark/src/main.rs new file mode 100644 index 0000000000..4b8971ecab --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/src/main.rs @@ -0,0 +1,44 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +mod benchmark; +mod scenario; + +use clap::Parser; +use scenario::Scenario; +use std::path::PathBuf; +use strum::IntoEnumIterator; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long)] + output_wat_dir_path: Option, + + #[arg(short, long)] + scenario: Option, +} + +fn handle_scenario(scenario: Scenario, output_wat_dir_path: Option) -> eyre::Result<()> { + println!("Benchmarking {}", scenario); + let wat = scenario::generate_wat(scenario, output_wat_dir_path); + benchmark::benchmark(wat) +} + +fn main() -> eyre::Result<()> { + let args = Args::parse(); + + match args.scenario { + Some(scenario) => handle_scenario(scenario, args.output_wat_dir_path), + None => { + println!("No scenario specified, benchmarking all scenarios\n"); + for scenario in Scenario::iter() { + let benchmark_result = handle_scenario(scenario, args.output_wat_dir_path.clone()); + if let Err(err) = benchmark_result { + return Err(err); + } + } + Ok(()) + } + } +} diff --git a/arbitrator/tools/stylus_benchmark/src/scenario.rs b/arbitrator/tools/stylus_benchmark/src/scenario.rs new file mode 100644 index 0000000000..348678ed69 --- /dev/null +++ b/arbitrator/tools/stylus_benchmark/src/scenario.rs @@ -0,0 +1,128 @@ +// Copyright 2021-2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use strum_macros::{Display, EnumIter, EnumString}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, EnumString, Display, EnumIter)] +pub enum Scenario { + #[strum(serialize = "add_i32")] + AddI32, + #[strum(serialize = "xor_i32")] + XorI32, +} + +// Programs to be benchmarked have a loop in which several similar operations are executed. +// The number of operations per loop is chosen to be large enough so the overhead related to the loop is negligible, +// but not too large to avoid a big program size. +// Keeping a small program size is important to better use CPU cache, trying to keep the code in the cache. + +fn write_wat_beginning(wat: &mut Vec) { + wat.write_all(b"(module\n").unwrap(); + wat.write_all(b" (import \"debug\" \"start_benchmark\" (func $start_benchmark))\n") + .unwrap(); + wat.write_all(b" (import \"debug\" \"end_benchmark\" (func $end_benchmark))\n") + .unwrap(); + wat.write_all(b" (memory (export \"memory\") 0 0)\n") + .unwrap(); + wat.write_all(b" (global $ops_counter (mut i32) (i32.const 0))\n") + .unwrap(); + wat.write_all(b" (func (export \"user_entrypoint\") (param i32) (result i32)\n") + .unwrap(); + + wat.write_all(b" call $start_benchmark\n").unwrap(); + + wat.write_all(b" (loop $loop\n").unwrap(); +} + +fn write_wat_end( + wat: &mut Vec, + number_of_loop_iterations: usize, + number_of_ops_per_loop_iteration: usize, +) { + let number_of_ops = number_of_loop_iterations * number_of_ops_per_loop_iteration; + + // update ops_counter + wat.write_all(b" global.get $ops_counter\n") + .unwrap(); + wat.write_all( + format!( + " i32.const {}\n", + number_of_ops_per_loop_iteration + ) + .as_bytes(), + ) + .unwrap(); + wat.write_all(b" i32.add\n").unwrap(); + wat.write_all(b" global.set $ops_counter\n") + .unwrap(); + + // check if we need to continue looping + wat.write_all(b" global.get $ops_counter\n") + .unwrap(); + wat.write_all(format!(" i32.const {}\n", number_of_ops).as_bytes()) + .unwrap(); + wat.write_all(b" i32.lt_s\n").unwrap(); + wat.write_all(b" br_if $loop)\n").unwrap(); + + wat.write_all(b" call $end_benchmark\n").unwrap(); + + wat.write_all(b" i32.const 0)\n").unwrap(); + wat.write_all(b")").unwrap(); +} + +fn wat(write_wat_ops: fn(&mut Vec, usize)) -> Vec { + let number_of_loop_iterations = 200_000; + let number_of_ops_per_loop_iteration = 2000; + + let mut wat = Vec::new(); + + write_wat_beginning(&mut wat); + + write_wat_ops(&mut wat, number_of_ops_per_loop_iteration); + + write_wat_end( + &mut wat, + number_of_loop_iterations, + number_of_ops_per_loop_iteration, + ); + + wat.to_vec() +} + +fn write_add_i32_wat_ops(wat: &mut Vec, number_of_ops_per_loop_iteration: usize) { + wat.write_all(b" i32.const 0\n").unwrap(); + for _ in 0..number_of_ops_per_loop_iteration { + wat.write_all(b" i32.const 1\n").unwrap(); + wat.write_all(b" i32.add\n").unwrap(); + } + wat.write_all(b" drop\n").unwrap(); +} + +fn write_xor_i32_wat_ops(wat: &mut Vec, number_of_ops_per_loop_iteration: usize) { + wat.write_all(b" i32.const 1231\n").unwrap(); + for _ in 0..number_of_ops_per_loop_iteration { + wat.write_all(b" i32.const 12312313\n").unwrap(); + wat.write_all(b" i32.xor\n").unwrap(); + } + wat.write_all(b" drop\n").unwrap(); +} + +pub fn generate_wat(scenario: Scenario, output_wat_dir_path: Option) -> Vec { + let wat = match scenario { + Scenario::AddI32 => wat(write_add_i32_wat_ops), + Scenario::XorI32 => wat(write_xor_i32_wat_ops), + }; + + // print wat to file if needed + if let Some(output_wat_dir_path) = output_wat_dir_path { + let mut output_wat_path = output_wat_dir_path; + output_wat_path.push(format!("{}.wat", scenario)); + let mut file = File::create(output_wat_path).unwrap(); + file.write_all(&wat).unwrap(); + } + + wat +} diff --git a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs index 2f410849fc..25163e25bc 100644 --- a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs +++ b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, crypto, evm::{ self, @@ -21,6 +22,7 @@ use prover::{ }; use ruint2::Uint; use std::fmt::Display; +use std::time::Instant; macro_rules! be { ($int:expr) => { @@ -68,6 +70,7 @@ pub trait UserHost: GasMeteredMachine { fn evm_api(&mut self) -> &mut Self::A; fn evm_data(&self) -> &EvmData; + fn benchmark(&mut self) -> &mut Benchmark; fn evm_return_data_len(&mut self) -> &mut u32; fn read_slice(&self, ptr: GuestPtr, len: u32) -> Result, Self::MemoryErr>; @@ -962,4 +965,38 @@ pub trait UserHost: GasMeteredMachine { self.say(value.into()); Ok(value) } + + // Initializes benchmark data related to a code block. + // A code block is defined by the instructions between start_benchmark and end_benchmark calls. + // If start_benchmark is called multiple times without end_benchmark being called, + // then only the last start_benchmark before end_benchmark will be used. + // It is possible to have multiple code blocks benchmarked in the same program. + fn start_benchmark(&mut self) -> Result<(), Self::Err> { + let ink_curr = self.ink_ready()?; + + let benchmark = self.benchmark(); + benchmark.timer = Some(Instant::now()); + benchmark.ink_start = Some(ink_curr); + + Ok(()) + } + + // Updates cumulative benchmark data related to a code block. + // If end_benchmark is called without a corresponding start_benchmark nothing will happen. + fn end_benchmark(&mut self) -> Result<(), Self::Err> { + let ink_curr = self.ink_ready()?; + + let benchmark = self.benchmark(); + if let Some(timer) = benchmark.timer { + benchmark.elapsed_total = benchmark.elapsed_total.saturating_add(timer.elapsed()); + + let code_block_ink = benchmark.ink_start.unwrap().saturating_sub(ink_curr); + benchmark.ink_total = benchmark.ink_total.saturating_add(code_block_ink); + + benchmark.timer = None; + benchmark.ink_start = None; + }; + + Ok(()) + } } diff --git a/arbitrator/wasm-libraries/user-host/src/program.rs b/arbitrator/wasm-libraries/user-host/src/program.rs index 7b3782b2e5..a2973ce56f 100644 --- a/arbitrator/wasm-libraries/user-host/src/program.rs +++ b/arbitrator/wasm-libraries/user-host/src/program.rs @@ -2,6 +2,7 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use arbutil::{ + benchmark::Benchmark, evm::{ api::{EvmApiMethod, Gas, Ink, VecReader, EVM_API_METHOD_REQ_OFFSET}, req::{EvmApiRequestor, RequestHandler}, @@ -75,6 +76,8 @@ pub(crate) struct Program { pub evm_api: EvmApiRequestor, /// EVM Context info. pub evm_data: EvmData, + // Used to benchmark execution blocks of code + pub benchmark: Benchmark, /// WAVM module index. pub module: u32, /// Call configuration. @@ -167,6 +170,7 @@ impl Program { outs: vec![], evm_api: EvmApiRequestor::new(UserHostRequester::default()), evm_data, + benchmark: Benchmark::default(), module, config, early_exit: None, @@ -237,6 +241,10 @@ impl UserHost for Program { &self.evm_data } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { &mut self.evm_data.return_data_len } diff --git a/arbitrator/wasm-libraries/user-test/src/program.rs b/arbitrator/wasm-libraries/user-test/src/program.rs index 299fca08c3..99252a38f0 100644 --- a/arbitrator/wasm-libraries/user-test/src/program.rs +++ b/arbitrator/wasm-libraries/user-test/src/program.rs @@ -3,6 +3,7 @@ use crate::{ARGS, EVER_PAGES, EVM_DATA, KEYS, LOGS, OPEN_PAGES, OUTS}; use arbutil::{ + benchmark::Benchmark, evm::{ api::{EvmApi, Gas, Ink, VecReader}, user::UserOutcomeKind, @@ -28,6 +29,7 @@ impl From for eyre::ErrReport { /// Mock type representing a `user_host::Program` pub struct Program { evm_api: MockEvmApi, + benchmark: Benchmark, } #[allow(clippy::unit_arg)] @@ -52,6 +54,10 @@ impl UserHost for Program { &EVM_DATA } + fn benchmark(&mut self) -> &mut Benchmark { + &mut self.benchmark + } + fn evm_return_data_len(&mut self) -> &mut u32 { unimplemented!() } @@ -91,6 +97,7 @@ impl Program { pub fn current() -> Self { Self { evm_api: MockEvmApi, + benchmark: Benchmark::default(), } }