From 690dd8d9e1c010de132cacc10da30da34a483ddc Mon Sep 17 00:00:00 2001 From: Jimmy Bourassa Date: Mon, 24 Apr 2023 17:43:27 -0400 Subject: [PATCH] Implement Wasm profiling w/ wasmprof --- Cargo.lock | 251 ++++++++++++++++++++++++++++++------- Cargo.toml | 8 +- src/engine.rs | 57 +++++++-- src/function_run_result.rs | 7 ++ src/main.rs | 54 +++++++- tests/integration_tests.rs | 46 +++++++ 6 files changed, 363 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b4232c2..2327f944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,21 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "assert_fs" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "async-trait" version = "0.1.61" @@ -374,6 +389,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpp_demangle" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee34052ee3d93d6d8f3e6f81d85c47921f6653a19a7b70e939e3e602d893a674" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.5" @@ -764,6 +788,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fd-lock" version = "3.0.8" @@ -826,6 +859,7 @@ version = "3.4.0" dependencies = [ "anyhow", "assert_cmd", + "assert_fs", "clap", "colored", "deterministic-wasi-ctx", @@ -835,6 +869,7 @@ dependencies = [ "serde", "serde_json", "wasi-common", + "wasmprof", "wasmtime", "wasmtime-wasi", ] @@ -880,6 +915,30 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -983,6 +1042,23 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -994,6 +1070,15 @@ dependencies = [ "serde", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-extras" version = "0.17.1" @@ -1099,9 +1184,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linear_memory_function" @@ -1198,6 +1283,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "static_assertions", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1420,6 +1517,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -1427,7 +1533,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -1511,9 +1617,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" @@ -1672,6 +1778,16 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "spin_sleep" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cafa7900db085f4354dbc7025e25d7a839a14360ea13b5fc4fd717f2d3b23134" +dependencies = [ + "once_cell", + "winapi", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1687,6 +1803,12 @@ dependencies = [ "serde_with", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -1737,6 +1859,19 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix 0.37.7", + "windows-sys 0.45.0", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -1772,6 +1907,16 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.21" @@ -1955,9 +2100,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-cap-std-sync" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8306bc71532b8a78f31e35d88e43506dfc5fc26bf30c7f0673cbf6beeb28bb6a" +checksum = "612510e6c7b6681f7d29ce70ef26e18349c26acd39b7d89f1727d90b7f58b20e" dependencies = [ "anyhow", "async-trait", @@ -1979,9 +2124,9 @@ dependencies = [ [[package]] name = "wasi-common" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c4bd23d8aeec5da68339e121dccf21e2b2f7b9028c39413bfd2fe94eb7a535" +checksum = "008136464e438c5049a614b6ea1bae9f6c4d354ce9ee2b4d9a1ac6e73f31aafc" dependencies = [ "anyhow", "bitflags", @@ -2070,11 +2215,25 @@ dependencies = [ "url", ] +[[package]] +name = "wasmprof" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d5409ff7cd7102fa17cd3108b0374bc203f979471b4db15d6b89c83350ae24" +dependencies = [ + "cpp_demangle 0.4.2", + "libc", + "nix", + "rustc-demangle", + "spin_sleep", + "wasmtime", +] + [[package]] name = "wasmtime" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65e578b6d35f3e808b21b00c652b4c3ded90249f642d504a67700d7a02cac1c" +checksum = "f907fdead3153cb9bfb7a93bbd5b62629472dc06dee83605358c64c52ed3dda9" dependencies = [ "anyhow", "async-trait", @@ -2104,18 +2263,18 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdaba4716347d5936234f17d1c75a3a92f21edaefc96dbdc64b36ef53504c1e1" +checksum = "d3b9daa7c14cd4fa3edbf69de994408d5f4b7b0959ac13fa69d465f6597f810d" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-cache" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673200e1afd89735b9e641ec63218e9b7edf2860257db1968507c0538511d612" +checksum = "c86437fa68626fe896e5afc69234bb2b5894949083586535f200385adfd71213" dependencies = [ "anyhow", "base64", @@ -2133,9 +2292,9 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e83eb45f3bab16800cb9da977b04cd427f3e2b1e6668b6c2dcbc8baec8a6b6d" +checksum = "267096ed7cc93b4ab15d3daa4f195e04dbb7e71c7e5c6457ae7d52e9dd9c3607" dependencies = [ "anyhow", "proc-macro2", @@ -2148,15 +2307,15 @@ dependencies = [ [[package]] name = "wasmtime-component-util" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9c89418c99fed44b9081e09ec4a9c5a3843ad663c4b0beceb16cac7a70c31d" +checksum = "74e02ca7a4a3c69d72b88f26f0192e333958df6892415ac9ab84dcc42c9000c2" [[package]] name = "wasmtime-cranelift" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4490e68a86a8515071c19bd54a679b5c239c43badd24c18c764a63117ba119" +checksum = "b1cefde0cce8cb700b1b21b6298a3837dba46521affd7b8c38a9ee2c869eee04" dependencies = [ "anyhow", "cranelift-codegen", @@ -2176,9 +2335,9 @@ dependencies = [ [[package]] name = "wasmtime-cranelift-shared" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73493000ea57cc755b4ab48df9194740c00ea6dcd2714b660b7859a451c1b925" +checksum = "cd041e382ef5aea1b9fc78442394f1a4f6d676ce457e7076ca4cb3f397882f8b" dependencies = [ "anyhow", "cranelift-codegen", @@ -2191,9 +2350,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2a2c8dcf2c4bacaa5bd29fbbc744769804377747940c6d5fe12b15bdfafe2c" +checksum = "a990198cee4197423045235bf89d3359e69bd2ea031005f4c2d901125955c949" dependencies = [ "anyhow", "cranelift-entity", @@ -2210,9 +2369,9 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bcc711e6ccb9314b4d90112912060539ce98ac43b4d4408680609414de004f" +checksum = "7ab182d5ab6273a133ab88db94d8ca86dc3e57e43d70baaa4d98f94ddbd7d10a" dependencies = [ "cc", "cfg-if", @@ -2223,15 +2382,15 @@ dependencies = [ [[package]] name = "wasmtime-jit" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b75238696641fb46dcf3cd6aaf09ac4c48040c5e2391d5c5a9883c35b09a627" +checksum = "0de48df552cfca1c9b750002d3e07b45772dd033b0b206d5c0968496abf31244" dependencies = [ "addr2line", "anyhow", "bincode", "cfg-if", - "cpp_demangle", + "cpp_demangle 0.3.5", "gimli", "ittapi", "log", @@ -2248,9 +2407,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47cc7e383300218d338fcbe95f2d7343e125b6b0d284d0d9b7e6acc7dd112a1" +checksum = "6e0554b84c15a27d76281d06838aed94e13a77d7bf604bbbaf548aa20eb93846" dependencies = [ "object", "once_cell", @@ -2259,9 +2418,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c1b25e736692815a53f669e774e230b80ec063f21596f006f8310b9f2dd910" +checksum = "aecae978b13f7f67efb23bd827373ace4578f2137ec110bbf6a4a7cde4121bbd" dependencies = [ "cfg-if", "libc", @@ -2270,9 +2429,9 @@ dependencies = [ [[package]] name = "wasmtime-runtime" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a305b2e4e62dfc67c8d25b2db1c2ac6ba44c7bcf0ccefb7fd9205338bed3f6a" +checksum = "658cf6f325232b6760e202e5255d823da5e348fdea827eff0a2a22319000b441" dependencies = [ "anyhow", "cc", @@ -2307,9 +2466,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee49d3a53fd66187ac35f49f1cb5a28b3a104e066117d7194a0df7faf02658e" +checksum = "4a3b5cb7606625ec229f0e33394a1637b34a58ad438526eba859b5fdb422ac1e" dependencies = [ "anyhow", "libc", @@ -2321,9 +2480,9 @@ dependencies = [ [[package]] name = "wasmtime-wit-bindgen" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796fdb0983ac1b3da4509169f49eea5e902b5641324466dc6f158c6e4ea693f5" +checksum = "983db9cc294d1adaa892a53ff6a0dc6605fc0ab1a4da5d8a2d2d4bde871ff7dd" dependencies = [ "anyhow", "heck", @@ -2362,9 +2521,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883e99f57044e457243de44477104db73e90d892130e11da4cf7d1d9df3333e6" +checksum = "6b16a7462893c46c6d3dd2a1f99925953bdbb921080606e1a4c9344864492fa4" dependencies = [ "anyhow", "async-trait", @@ -2377,9 +2536,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0743743724253da8c775c1668bfdb0e14f47b0666b6b41f997fb21a33e8768df" +checksum = "489499e186ab24c8ac6d89e9934c54ced6f19bd473730e6a74f533bd67ecd905" dependencies = [ "anyhow", "heck", @@ -2392,9 +2551,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "8.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a21994785b4bbc8cf3811e1422feb3c6b613b9da51c8bacf35cc29ca2f356a0" +checksum = "e9142e7fce24a4344c85a43c8b719ef434fc6155223bade553e186cb4183b6cc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a2bb808a..d7537692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ opt-level = "s" opt-level = 3 [dependencies] -wasmtime = "=8.0.0" -wasmtime-wasi = "=8.0.0" -wasi-common = "=8.0.0" +wasmtime = "=8.0.1" +wasmtime-wasi = "=8.0.1" +wasi-common = "=8.0.1" deterministic-wasi-ctx = "=0.1.13" anyhow = "1.0" clap = { version = "4.3", features = ["derive"] } @@ -34,7 +34,9 @@ colored = "2.0" serde = "1.0" rust-embed = "6.7.0" is-terminal = "0.4.7" +wasmprof = "0.2.0" [dev-dependencies] assert_cmd = "2.0" predicates = "3.0" +assert_fs = "1.0.13" diff --git a/src/engine.rs b/src/engine.rs index 666301a0..14614277 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use rust_embed::RustEmbed; use std::{collections::HashSet, io::Cursor, path::PathBuf}; use wasi_common::{I32Exit, WasiCtx}; -use wasmtime::{Config, Engine, Linker, Module, Store}; +use wasmtime::{AsContextMut, Config, Engine, Linker, Module, Store}; use crate::{ function_run_result::{ @@ -12,13 +12,19 @@ use crate::{ logs::LogStream, }; +#[derive(Clone)] +pub struct ProfileOpts { + pub interval: u32, + pub out: PathBuf, +} + #[derive(RustEmbed)] #[folder = "providers/"] struct StandardProviders; fn import_modules( module: &Module, - engine: Engine, + engine: &Engine, linker: &mut Linker, mut store: &mut Store, ) { @@ -28,7 +34,7 @@ fn import_modules( let imported_module_bytes = StandardProviders::get(&format!("{imported_module}.wasm")); if let Some(bytes) = imported_module_bytes { - let imported_module = Module::from_binary(&engine, &bytes.data) + let imported_module = Module::from_binary(engine, &bytes.data) .unwrap_or_else(|_| panic!("Failed to load module {imported_module}")); let imported_module_instance = linker @@ -45,8 +51,17 @@ fn import_modules( }); } -pub fn run(function_path: PathBuf, input: Vec) -> Result { - let engine = Engine::new(Config::new().wasm_multi_memory(true).consume_fuel(true))?; +pub fn run( + function_path: PathBuf, + input: Vec, + profile_opts: Option<&ProfileOpts>, +) -> Result { + let engine = Engine::new( + Config::new() + .wasm_multi_memory(true) + .consume_fuel(true) + .epoch_interruption(true), + )?; let module = Module::from_file(&engine, &function_path) .map_err(|e| anyhow!("Couldn't load the Function {:?}: {}", &function_path, e))?; @@ -57,6 +72,7 @@ pub fn run(function_path: PathBuf, input: Vec) -> Result let memory_usage: u64; let instructions: u64; let mut error_logs: String = String::new(); + let profile_data: Option; { let mut linker = Linker::new(&engine); @@ -67,15 +83,31 @@ pub fn run(function_path: PathBuf, input: Vec) -> Result wasi.set_stderr(Box::new(error_stream.clone())); let mut store = Store::new(&engine, wasi); store.add_fuel(u64::MAX)?; + store.set_epoch_deadline(1); - import_modules(&module, engine, &mut linker, &mut store); + import_modules(&module, &engine, &mut linker, &mut store); linker.module(&mut store, "Function", &module)?; let instance = linker.instantiate(&mut store, &module)?; - let module_result = instance - .get_typed_func::<(), ()>(&mut store, "_start")? - .call(&mut store, ()); + let func = instance + .get_typed_func::<(), ()>(store.as_context_mut(), "_start") + .unwrap(); + + let module_result; + (module_result, profile_data) = if let Some(profile_opts) = profile_opts { + let (result, profile_data) = wasmprof::ProfilerBuilder::new(&mut store) + .frequency(profile_opts.interval) + .weight_unit(wasmprof::WeightUnit::Fuel) + .profile(|store| func.call(store.as_context_mut(), ())); + + ( + result, + Some(profile_data.into_collapsed_stacks().to_string()), + ) + } else { + (func.call(store.as_context_mut(), ()), None) + }; // modules may exit with a specific exit code, an exit code of 0 is considered success but is reported as // a GuestFault by wasmtime, so we need to map it to a success result. Any other exit code is considered @@ -147,6 +179,7 @@ pub fn run(function_path: PathBuf, input: Vec) -> Result instructions, logs.to_string(), output, + profile_data, ); Ok(function_run_result) @@ -167,6 +200,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/js_function.wasm").to_path_buf(), input, + None, ); assert!(function_run_result.is_ok()); @@ -178,6 +212,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/exit_code_function_zero.wasm").to_path_buf(), input, + None, ) .unwrap(); @@ -190,6 +225,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/exit_code_function_one.wasm").to_path_buf(), input, + None, ) .unwrap(); @@ -202,6 +238,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/linear_memory_function.wasm").to_path_buf(), input, + None, ) .unwrap(); @@ -214,6 +251,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/log_truncation_function.wasm").to_path_buf(), input, + None, ) .unwrap(); @@ -228,6 +266,7 @@ mod tests { let function_run_result = run( Path::new("benchmark/build/size_function.wasm").to_path_buf(), input, + None, ) .unwrap(); diff --git a/src/function_run_result.rs b/src/function_run_result.rs index 7929890e..96b3b902 100644 --- a/src/function_run_result.rs +++ b/src/function_run_result.rs @@ -22,6 +22,8 @@ pub struct FunctionRunResult { pub instructions: u64, pub logs: String, pub output: FunctionOutput, + #[serde(skip)] + pub profile: Option, } impl FunctionRunResult { @@ -32,6 +34,7 @@ impl FunctionRunResult { instructions: u64, logs: String, output: FunctionOutput, + profile: Option, ) -> Self { FunctionRunResult { name, @@ -40,6 +43,7 @@ impl FunctionRunResult { instructions, output, logs, + profile, } } @@ -131,6 +135,7 @@ mod tests { output: FunctionOutput::JsonOutput(serde_json::json!({ "test": "test" })), + profile: None, }; let predicate = predicates::str::contains("Instructions: 1.001K") @@ -149,6 +154,7 @@ mod tests { output: FunctionOutput::JsonOutput(serde_json::json!({ "test": "test" })), + profile: None, }; let predicate = predicates::str::contains("Instructions: 1") @@ -167,6 +173,7 @@ mod tests { output: FunctionOutput::JsonOutput(serde_json::json!({ "test": "test" })), + profile: None, }; let predicate = predicates::str::contains("Instructions: 999") diff --git a/src/main.rs b/src/main.rs index b3df2d55..b8722e23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,12 @@ use std::{ use anyhow::{anyhow, Result}; use clap::{CommandFactory, Parser}; -use function_runner::engine::run; +use function_runner::engine::{run, ProfileOpts}; use is_terminal::IsTerminal; +const PROFILE_DEFAULT_INTERVAL: u32 = 500_000; // every 5us + /// Simple Function runner which takes JSON as a convenience. #[derive(Parser, Debug)] #[clap(version)] @@ -25,6 +27,49 @@ struct Opts { /// Log the run result as a JSON object #[clap(short, long)] json: bool, + + /// Enable profiling. This will make your Function run slower. + /// The resulting profile can be used in speedscope (https://www.speedscope.app/) + /// Specifying --profile-* argument will also enable profiling. + #[clap(short, long)] + profile: bool, + + /// Where to save the profile information. Defaults to ./{wasm-filename}.perf. + #[clap(long)] + profile_out: Option, + + #[clap(long)] + /// How many samples per seconds. Defaults to 500_000 (every 5us). + profile_frequency: Option, +} + +impl Opts { + pub fn profile_opts(&self) -> Option { + if !self.profile && self.profile_out.is_none() && self.profile_frequency.is_none() { + return None; + } + + let interval = self.profile_frequency.unwrap_or(PROFILE_DEFAULT_INTERVAL); + let out = self + .profile_out + .clone() + .unwrap_or_else(|| self.default_profile_out()); + + Some(ProfileOpts { interval, out }) + } + + fn default_profile_out(&self) -> PathBuf { + let mut path = PathBuf::new(); + + path.set_file_name( + self.function + .file_name() + .unwrap_or(std::ffi::OsStr::new("function")), + ); + path.set_extension("perf"); + + path + } } fn main() -> Result<()> { @@ -48,7 +93,8 @@ fn main() -> Result<()> { let _ = serde_json::from_slice::(&buffer) .map_err(|e| anyhow!("Invalid input JSON: {}", e))?; - let function_run_result = run(opts.function, buffer)?; + let profile_opts = opts.profile_opts(); + let function_run_result = run(opts.function, buffer, profile_opts.as_ref())?; if opts.json { println!("{}", function_run_result.to_json()); @@ -56,5 +102,9 @@ fn main() -> Result<()> { println!("{function_run_result}"); } + if let Some(profile) = function_run_result.profile.as_ref() { + std::fs::write(profile_opts.unwrap().out, profile)?; + } + Ok(()) } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index b1a53bc4..ab8491c8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2,6 +2,7 @@ mod tests { use assert_cmd::prelude::*; + use assert_fs::prelude::*; use function_runner::function_run_result::FunctionRunResult; use predicates::prelude::*; use predicates::{prelude::predicate, str::contains}; @@ -148,6 +149,37 @@ mod tests { Ok(()) } + #[test] + fn profile_writes_file() -> Result<(), Box> { + let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; + cmd.arg("--profile").assert().success(); + temp.child("runtime_function.perf") + .assert(predicate::path::exists()); + + Ok(()) + } + + #[test] + fn profile_writes_specified_file_name() -> Result<(), Box> { + let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; + cmd.args(["--profile-out", "foo.perf"]).assert().success(); + temp.child("foo.perf").assert(predicate::path::exists()); + + Ok(()) + } + + #[test] + fn profile_frequency_triggers_profiling() -> Result<(), Box> { + let (mut cmd, temp) = profile_base_cmd_in_temp_dir()?; + cmd.args(["--profile-frequency", "80000"]) + .assert() + .success(); + temp.child("runtime_function.perf") + .assert(predicate::path::exists()); + + Ok(()) + } + #[test] fn incorrect_input() -> Result<(), Box> { let mut cmd = Command::cargo_bin("function-runner")?; @@ -164,4 +196,18 @@ mod tests { Ok(()) } + + fn profile_base_cmd_in_temp_dir( + ) -> Result<(Command, assert_fs::TempDir), Box> { + let mut cmd = Command::cargo_bin("function-runner")?; + let cwd = std::env::current_dir()?; + let temp = assert_fs::TempDir::new()?; + + cmd.current_dir(temp.path()) + .arg("--function") + .arg(cwd.join("benchmark/build/runtime_function.wasm")) + .arg(cwd.join("benchmark/build/volume_discount.json")); + + Ok((cmd, temp)) + } }