diff --git a/Cargo.lock b/Cargo.lock index a896d1f280..9bddeda50c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2433,7 +2433,6 @@ dependencies = [ "wasmtime-environ", "wasmtime-jit", "wasmtime-obj", - "wasmtime-profiling", "wasmtime-runtime", "wasmtime-wasi", "wasmtime-wast", @@ -2555,6 +2554,8 @@ dependencies = [ name = "wasmtime-profiling" version = "0.12.0" dependencies = [ + "anyhow", + "cfg-if", "gimli", "goblin", "lazy_static", @@ -2563,6 +2564,8 @@ dependencies = [ "scroll", "serde", "target-lexicon", + "wasmtime-environ", + "wasmtime-runtime", ] [[package]] @@ -2592,7 +2595,6 @@ dependencies = [ "region", "thiserror", "wasmtime-environ", - "wasmtime-profiling", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index cb254357eb..f7f690398b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,11 @@ doc = false [dependencies] # Enable all supported architectures by default. -wasmtime = { path = "crates/api" } +wasmtime = { path = "crates/api", default-features = false } wasmtime-debug = { path = "crates/debug" } wasmtime-environ = { path = "crates/environ" } wasmtime-jit = { path = "crates/jit" } wasmtime-obj = { path = "crates/obj" } -wasmtime-profiling = { path = "crates/profiling" } wasmtime-wast = { path = "crates/wast" } wasmtime-wasi = { path = "crates/wasi" } wasi-common = { path = "crates/wasi-common" } @@ -72,13 +71,14 @@ members = [ ] [features] +default = ["jitdump", "wasmtime/wat"] lightbeam = [ "wasmtime-environ/lightbeam", "wasmtime-jit/lightbeam", "wasmtime-wast/lightbeam", "wasmtime/lightbeam", ] -jitdump = ["wasmtime-profiling/jitdump"] +jitdump = ["wasmtime/jitdump"] test_programs = ["test-programs/test_programs"] [badges] diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index d9be1dd090..746bbdeb5e 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -40,13 +40,16 @@ tempfile = "3.1" maintenance = { status = "actively-developed" } [features] -default = ['wat'] +default = ['wat', 'jitdump'] # Enables experimental support for the lightbeam codegen backend, an alternative # to cranelift. Requires Nightly Rust currently, and this is not enabled by # default. lightbeam = ["wasmtime-jit/lightbeam"] +# Enables support for the `perf` jitdump profiler +jitdump = ["wasmtime-jit/jitdump"] + [[test]] name = "host-segfault" harness = false diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index b6e5cb5ee9..f7eb7b47df 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -26,7 +26,7 @@ pub use crate::func::*; pub use crate::instance::Instance; pub use crate::module::Module; pub use crate::r#ref::{AnyRef, HostInfo, HostRef}; -pub use crate::runtime::{Config, Engine, OptLevel, Store, Strategy}; +pub use crate::runtime::*; pub use crate::trap::Trap; pub use crate::types::*; pub use crate::values::*; diff --git a/crates/api/src/module.rs b/crates/api/src/module.rs index 0338cdbb4b..ea8ae1809d 100644 --- a/crates/api/src/module.rs +++ b/crates/api/src/module.rs @@ -367,7 +367,7 @@ impl Module { &mut store.compiler_mut(), binary, store.engine().config().debug_info, - store.engine().config().profiler.as_ref(), + &*store.engine().config().profiler, )?; Ok(Module { diff --git a/crates/api/src/runtime.rs b/crates/api/src/runtime.rs index 25aa3f6a58..8908483996 100644 --- a/crates/api/src/runtime.rs +++ b/crates/api/src/runtime.rs @@ -3,12 +3,12 @@ use std::cell::RefCell; use std::fmt; use std::path::Path; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use wasmparser::{OperatorValidatorConfig, ValidatingParserConfig}; use wasmtime_environ::settings::{self, Configurable}; use wasmtime_environ::CacheConfig; use wasmtime_jit::{native, CompilationStrategy, Compiler}; -use wasmtime_profiling::{JitDumpAgent, ProfilingAgent, ProfilingStrategy}; +use wasmtime_profiling::{JitDumpAgent, NullProfilerAgent, ProfilingAgent}; // Runtime Environment @@ -26,7 +26,7 @@ pub struct Config { pub(crate) debug_info: bool, pub(crate) strategy: CompilationStrategy, pub(crate) cache_config: CacheConfig, - pub(crate) profiler: Option>>>, + pub(crate) profiler: Arc, } impl Config { @@ -65,7 +65,7 @@ impl Config { flags, strategy: CompilationStrategy::Auto, cache_config: CacheConfig::new_cache_disabled(), - profiler: None, + profiler: Arc::new(NullProfilerAgent), } } @@ -225,11 +225,9 @@ impl Config { /// Profiler creation calls the type's default initializer where the purpose is /// really just to put in place the type used for profiling. pub fn profiler(&mut self, profile: ProfilingStrategy) -> Result<&mut Self> { - match profile { - ProfilingStrategy::JitDumpProfiler => { - self.profiler = { Some(Arc::new(Mutex::new(Box::new(JitDumpAgent::default())))) } - } - _ => self.profiler = { None }, + self.profiler = match profile { + ProfilingStrategy::JitDump => Arc::new(JitDumpAgent::new()?) as Arc, + ProfilingStrategy::None => Arc::new(NullProfilerAgent), }; Ok(self) } @@ -381,6 +379,17 @@ pub enum OptLevel { SpeedAndSize, } +/// Select which profiling technique to support. +#[derive(Debug, Clone, Copy)] +pub enum ProfilingStrategy { + /// No profiler support. + None, + + /// Collect profiling info for "jitdump" file format, used with `perf` on + /// Linux. + JitDump, +} + // Engine /// An `Engine` which is a global context for compilation and management of wasm diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index e489843651..5978269575 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -17,7 +17,12 @@ test = false doctest = false [dependencies] -wasmtime = { path = "../api" } +wasmtime = { path = "../api", default-features = false } wasi-common = { path = "../wasi-common" } wasmtime-wasi = { path = "../wasi" } wat = "1.0" + +[features] +default = ['jitdump'] +lightbeam = ["wasmtime/lightbeam"] +jitdump = ["wasmtime/jitdump"] diff --git a/crates/c-api/include/wasmtime.h b/crates/c-api/include/wasmtime.h index 5334d49bd3..52e39825af 100644 --- a/crates/c-api/include/wasmtime.h +++ b/crates/c-api/include/wasmtime.h @@ -25,6 +25,12 @@ enum wasmtime_opt_level_enum { // OptLevel WASMTIME_OPT_LEVEL_SPEED_AND_SIZE, }; +typedef uint8_t wasmtime_profiling_strategy_t; +enum wasmtime_profiling_strategy_t { // ProfilingStrategy + WASMTIME_PROFILING_STRATEGY_NONE, + WASMTIME_PROFILING_STRATEGY_JITDUMP, +}; + #define WASMTIME_CONFIG_PROP(name, ty) \ WASM_API_EXTERN void wasmtime_config_##name##_set(wasm_config_t*, ty); @@ -37,6 +43,7 @@ WASMTIME_CONFIG_PROP(wasm_multi_value, bool) WASMTIME_CONFIG_PROP(strategy, wasmtime_strategy_t) WASMTIME_CONFIG_PROP(cranelift_debug_verifier, bool) WASMTIME_CONFIG_PROP(cranelift_opt_level, wasmtime_opt_level_t) +WASMTIME_CONFIG_PROP(profiler, wasmtime_profiling_strategy_t) /////////////////////////////////////////////////////////////////////////////// diff --git a/crates/c-api/src/ext.rs b/crates/c-api/src/ext.rs index c61e7ad07d..a81be32b9c 100644 --- a/crates/c-api/src/ext.rs +++ b/crates/c-api/src/ext.rs @@ -3,7 +3,7 @@ use crate::{wasm_byte_vec_t, wasm_config_t, wasm_engine_t}; use std::str; -use wasmtime::{OptLevel, Strategy}; +use wasmtime::{OptLevel, ProfilingStrategy, Strategy}; #[repr(u8)] #[derive(Clone)] @@ -21,6 +21,13 @@ pub enum wasmtime_opt_level_t { WASMTIME_OPT_LEVEL_SPEED_AND_SIZE, } +#[repr(u8)] +#[derive(Clone)] +pub enum wasmtime_profiling_strategy_t { + WASMTIME_PROFILING_STRATEGY_NONE, + WASMTIME_PROFILING_STRATEGY_JITDUMP, +} + #[no_mangle] pub unsafe extern "C" fn wasmtime_config_debug_info_set(c: *mut wasm_config_t, enable: bool) { (*c).config.debug_info(enable); @@ -88,6 +95,18 @@ pub unsafe extern "C" fn wasmtime_config_cranelift_opt_level_set( }); } +#[no_mangle] +pub unsafe extern "C" fn wasmtime_config_profiler_set( + c: *mut wasm_config_t, + strategy: wasmtime_profiling_strategy_t, +) { + use wasmtime_profiling_strategy_t::*; + drop((*c).config.profiler(match strategy { + WASMTIME_PROFILING_STRATEGY_NONE => ProfilingStrategy::None, + WASMTIME_PROFILING_STRATEGY_JITDUMP => ProfilingStrategy::JitDump, + })); +} + #[no_mangle] pub unsafe extern "C" fn wasmtime_wat2wasm( _engine: *mut wasm_engine_t, diff --git a/crates/jit/Cargo.toml b/crates/jit/Cargo.toml index f90142c8cc..5f8e53819e 100644 --- a/crates/jit/Cargo.toml +++ b/crates/jit/Cargo.toml @@ -34,6 +34,7 @@ winapi = { version = "0.3.7", features = ["winnt", "impl-default"] } [features] lightbeam = ["wasmtime-environ/lightbeam"] +jitdump = ["wasmtime-profiling/jitdump"] [badges] maintenance = { status = "actively-developed" } diff --git a/crates/jit/src/code_memory.rs b/crates/jit/src/code_memory.rs index 968b1cb62e..a31695527b 100644 --- a/crates/jit/src/code_memory.rs +++ b/crates/jit/src/code_memory.rs @@ -5,7 +5,6 @@ use region; use std::mem::ManuallyDrop; use std::{cmp, mem}; use wasmtime_environ::{Compilation, CompiledFunction}; -use wasmtime_profiling::ProfilingAgent; use wasmtime_runtime::{Mmap, VMFunctionBody}; struct CodeMemoryEntry { @@ -237,22 +236,4 @@ impl CodeMemory { Ok(()) } - - /// Calls the module_load for a given ProfilerAgent. Includes - /// all memory address and length for the given module. - /// TODO: Properly handle the possibilities of multiple mmapped regions - /// which may, amongst other things, influence being more specific about - /// the module name. - pub fn profiler_module_load( - &mut self, - profiler: &mut Box, - module_name: &str, - dbg_image: Option<&[u8]>, - ) -> () { - for CodeMemoryEntry { mmap: m, table: _t } in &mut self.entries { - if m.len() > 0 { - profiler.module_load(module_name, m.as_ptr(), m.len(), dbg_image); - } - } - } } diff --git a/crates/jit/src/compiler.rs b/crates/jit/src/compiler.rs index 0329320a76..5bf2fd95ff 100644 --- a/crates/jit/src/compiler.rs +++ b/crates/jit/src/compiler.rs @@ -22,7 +22,6 @@ use wasmtime_environ::{ FunctionBodyData, Module, ModuleMemoryOffset, ModuleVmctxInfo, Relocation, Relocations, Traps, Tunables, VMOffsets, }; -use wasmtime_profiling::ProfilingAgent; use wasmtime_runtime::{ InstantiationError, SignatureRegistry, TrapRegistration, TrapRegistry, VMFunctionBody, VMSharedSignatureIndex, VMTrampoline, @@ -246,16 +245,6 @@ impl Compiler { self.code_memory.publish(); } - pub(crate) fn profiler_module_load( - &mut self, - profiler: &mut Box, - module_name: &str, - dbg_image: Option<&[u8]>, - ) -> () { - self.code_memory - .profiler_module_load(profiler, module_name, dbg_image); - } - /// Shared signature registry. pub fn signatures(&self) -> &SignatureRegistry { &self.signatures diff --git a/crates/jit/src/instantiate.rs b/crates/jit/src/instantiate.rs index 9fbef727b7..50380ea97e 100644 --- a/crates/jit/src/instantiate.rs +++ b/crates/jit/src/instantiate.rs @@ -10,7 +10,7 @@ use crate::resolver::Resolver; use std::collections::HashMap; use std::io::Write; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use thiserror::Error; use wasmtime_debug::read_debuginfo; use wasmtime_environ::entity::{BoxedSlice, PrimaryMap}; @@ -64,7 +64,7 @@ impl<'data> RawCompiledModule<'data> { compiler: &mut Compiler, data: &'data [u8], debug_info: bool, - profiler: Option<&Arc>>>, + profiler: &dyn ProfilingAgent, ) -> Result { let environ = ModuleEnvironment::new(compiler.frontend_config(), compiler.tunables()); @@ -105,19 +105,11 @@ impl<'data> RawCompiledModule<'data> { compiler.publish_compiled_code(); // Initialize profiler and load the wasm module - match profiler { - Some(_) => { - let region_name = String::from("wasm_module"); - let mut profiler = profiler.unwrap().lock().unwrap(); - match &compilation.dbg_image { - Some(dbg) => { - compiler.profiler_module_load(&mut profiler, ®ion_name, Some(&dbg)) - } - _ => compiler.profiler_module_load(&mut profiler, ®ion_name, None), - }; - } - _ => (), - }; + profiler.module_load( + &translation.module, + &compilation.finished_functions, + compilation.dbg_image.as_deref(), + ); let dbg_jit_registration = if let Some(img) = compilation.dbg_image { let mut bytes = Vec::new(); @@ -157,7 +149,7 @@ impl CompiledModule { compiler: &mut Compiler, data: &'data [u8], debug_info: bool, - profiler: Option<&Arc>>>, + profiler: &dyn ProfilingAgent, ) -> Result { let raw = RawCompiledModule::<'data>::new(compiler, data, debug_info, profiler)?; @@ -290,7 +282,7 @@ pub unsafe fn instantiate( resolver: &mut dyn Resolver, debug_info: bool, is_bulk_memory: bool, - profiler: Option<&Arc>>>, + profiler: &dyn ProfilingAgent, ) -> Result { let instance = CompiledModule::new(compiler, data, debug_info, profiler)?.instantiate( is_bulk_memory, diff --git a/crates/profiling/Cargo.toml b/crates/profiling/Cargo.toml index 8ca66bd1d9..eca6a0ee03 100644 --- a/crates/profiling/Cargo.toml +++ b/crates/profiling/Cargo.toml @@ -11,17 +11,21 @@ readme = "README.md" edition = "2018" [dependencies] -libc = { version = "0.2.60", default-features = false } -goblin = "0.1.3" -serde = { version = "1.0.99", features = ["derive"] } -scroll = "0.10.1" -gimli = "0.20.0" -object = "0.17.0" -target-lexicon = "0.10.0" +anyhow = "1.0" +cfg-if = "0.1" +gimli = { version = "0.20.0", optional = true } +goblin = { version = "0.1.3", optional = true } lazy_static = "1.4" +libc = { version = "0.2.60", default-features = false } +object = { version = "0.17.0", optional = true } +scroll = { version = "0.10.1", optional = true } +serde = { version = "1.0.99", features = ["derive"] } +target-lexicon = "0.10.0" +wasmtime-environ = { path = "../environ", version = "0.12.0" } +wasmtime-runtime = { path = "../runtime", version = "0.12.0" } [badges] maintenance = { status = "actively-developed" } [features] -jitdump = [] +jitdump = ['goblin', 'object', 'scroll', 'gimli'] diff --git a/crates/profiling/src/jitdump_disabled.rs b/crates/profiling/src/jitdump_disabled.rs new file mode 100644 index 0000000000..5197ddb5b5 --- /dev/null +++ b/crates/profiling/src/jitdump_disabled.rs @@ -0,0 +1,33 @@ +use crate::ProfilingAgent; +use anyhow::{bail, Result}; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::Module; +use wasmtime_runtime::VMFunctionBody; + +/// Interface for driving the creation of jitdump files +#[derive(Debug)] +pub struct JitDumpAgent { + _private: (), +} + +impl JitDumpAgent { + /// Intialize a JitDumpAgent and write out the header + pub fn new() -> Result { + if cfg!(feature = "jitdump") { + bail!("jitdump is not supported on this platform"); + } else { + bail!("jitdump support disabled at compile time"); + } + } +} + +impl ProfilingAgent for JitDumpAgent { + fn module_load( + &self, + _module: &Module, + _functions: &PrimaryMap, + _dbg_image: Option<&[u8]>, + ) { + } +} diff --git a/crates/profiling/src/jitdump.rs b/crates/profiling/src/jitdump_linux.rs similarity index 77% rename from crates/profiling/src/jitdump.rs rename to crates/profiling/src/jitdump_linux.rs index cff4155c83..0b98b77f43 100644 --- a/crates/profiling/src/jitdump.rs +++ b/crates/profiling/src/jitdump_linux.rs @@ -10,22 +10,25 @@ //! Report //! sudo perf report -i perf.jit.data -F+period,srcline //! Note: For descriptive results, the WASM file being executed should contain dwarf debug data -use libc::c_int; -#[cfg(not(target_os = "windows"))] -use libc::{c_void, clock_gettime, mmap, open, sysconf}; + +use crate::ProfilingAgent; +use anyhow::Result; use object::Object; use scroll::{IOwrite, SizeWith, NATIVE}; use serde::{Deserialize, Serialize}; -use std::error::Error; -#[cfg(not(target_os = "windows"))] -use std::ffi::CString; use std::fmt::Debug; -use std::fs::File; +use std::fs::{File, OpenOptions}; +use std::io; use std::io::Write; -#[cfg(not(target_os = "windows"))] -use std::os::unix::io::FromRawFd; +use std::os::unix::prelude::*; +use std::ptr; +use std::sync::Mutex; use std::{borrow, mem, process}; use target_lexicon::Architecture; +use wasmtime_environ::entity::PrimaryMap; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::Module; +use wasmtime_runtime::VMFunctionBody; #[cfg(target_pointer_width = "64")] use goblin::elf64 as elf; @@ -133,71 +136,110 @@ pub struct FileHeader { } /// Interface for driving the creation of jitdump files -#[derive(Debug, Default)] pub struct JitDumpAgent { + // Note that we use a mutex internally to serialize writing out to our + // `jitdump_file` within this process, since multiple threads may be sharing + // this jit agent. + state: Mutex, +} + +struct State { /// File instance for the jit dump file - pub jitdump_file: Option, + jitdump_file: File, + + map_addr: usize, + /// Unique identifier for jitted code - pub code_index: u64, + code_index: u64, + /// Flag for experimenting with dumping code load record /// after each function (true) or after each module. This /// flag is currently set to true. - pub dump_funcs: bool, + dump_funcs: bool, } impl JitDumpAgent { /// Intialize a JitDumpAgent and write out the header - pub fn init(&mut self) -> Result<(), Box> { - #[cfg(target_os = "windows")] - return Err("Target OS not supported."); - #[cfg(not(target_os = "windows"))] - { - let filename = format!("./jit-{}.dump", process::id()); - let mut jitdump_file; - unsafe { - let filename_c = CString::new(filename)?; - let fd = open( - filename_c.as_ptr(), - libc::O_CREAT | libc::O_TRUNC | libc::O_RDWR, - 0666, - ); - let pgsz = sysconf(libc::_SC_PAGESIZE) as usize; - mmap( - 0 as *mut c_void, - pgsz, - libc::PROT_EXEC | libc::PROT_READ, - libc::MAP_PRIVATE, - fd, - 0, - ); - jitdump_file = File::from_raw_fd(fd); - } - JitDumpAgent::write_file_header(&mut jitdump_file)?; - self.jitdump_file = Some(jitdump_file); - self.code_index = 0; - self.dump_funcs = true; - Ok(()) - } - } + pub fn new() -> Result { + let filename = format!("./jit-{}.dump", process::id()); + let jitdump_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&filename)?; - /// Returns timestamp from a single source - #[allow(unused_variables)] - fn get_time_stamp(timestamp: &mut u64) -> c_int { - #[cfg(not(target_os = "windows"))] - { - unsafe { - let mut ts = mem::MaybeUninit::zeroed().assume_init(); - clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); - // TODO: What does it mean for either sec or nsec to be negative? - *timestamp = (ts.tv_sec * 1000000000 + ts.tv_nsec) as u64; + // After we make our `*.dump` file we execute an `mmap` syscall, + // specifically with executable permissions, to map it into our address + // space. This is required so `perf inject` will work later. The `perf + // inject` command will see that an mmap syscall happened, and it'll see + // the filename we mapped, and that'll trigger it to actually read and + // parse the file. + // + // To match what some perf examples are doing we keep this `mmap` alive + // until this agent goes away. + let map_addr = unsafe { + let ptr = libc::mmap( + ptr::null_mut(), + libc::sysconf(libc::_SC_PAGESIZE) as usize, + libc::PROT_EXEC | libc::PROT_READ, + libc::MAP_PRIVATE, + jitdump_file.as_raw_fd(), + 0, + ); + if ptr == libc::MAP_FAILED { + return Err(io::Error::last_os_error().into()); } + ptr as usize + }; + let mut state = State { + jitdump_file, + map_addr, + code_index: 0, + dump_funcs: true, + }; + state.write_file_header()?; + Ok(JitDumpAgent { + state: Mutex::new(state), + }) + } +} + +impl ProfilingAgent for JitDumpAgent { + fn module_load( + &self, + module: &Module, + functions: &PrimaryMap, + dbg_image: Option<&[u8]>, + ) { + self.state + .lock() + .unwrap() + .module_load(module, functions, dbg_image); + } +} + +impl State { + /// Returns timestamp from a single source + fn get_time_stamp(&self) -> u64 { + // We need to use `CLOCK_MONOTONIC` on Linux which is what `Instant` + // conveniently also uses, but `Instant` doesn't allow us to get access + // to nanoseconds as an internal detail, so we calculate the nanoseconds + // ourselves here. + unsafe { + let mut ts = mem::MaybeUninit::zeroed(); + assert_eq!( + libc::clock_gettime(libc::CLOCK_MONOTONIC, ts.as_mut_ptr()), + 0 + ); + let ts = ts.assume_init(); + // TODO: What does it mean for either sec or nsec to be negative? + (ts.tv_sec * 1_000_000_000 + ts.tv_nsec) as u64 } - return 0; } /// Returns the ELF machine architecture. - #[allow(dead_code)] - fn get_e_machine() -> u32 { + fn get_e_machine(&self) -> u32 { match target_lexicon::HOST.architecture { Architecture::X86_64 => elf::header::EM_X86_64 as u32, Architecture::I686 => elf::header::EM_386 as u32, @@ -207,30 +249,23 @@ impl JitDumpAgent { } } - #[allow(dead_code)] - fn write_file_header(file: &mut File) -> Result<(), JitDumpError> { - let mut header: FileHeader = Default::default(); - let mut timestamp: u64 = 0; - JitDumpAgent::get_time_stamp(&mut timestamp); - header.timestamp = timestamp; + fn write_file_header(&mut self) -> Result<()> { + let header = FileHeader { + timestamp: self.get_time_stamp(), + e_machine: self.get_e_machine(), + magic: if cfg!(target_endian = "little") { + 0x4A695444 + } else { + 0x4454694a + }, + version: 1, + size: mem::size_of::() as u32, + pad1: 0, + pid: process::id(), + flags: 0, + }; - let e_machine = JitDumpAgent::get_e_machine(); - if e_machine != elf::header::EM_NONE as u32 { - header.e_machine = e_machine; - } - - if cfg!(target_endian = "little") { - header.magic = 0x4A695444 - } else { - header.magic = 0x4454694a - } - header.version = 1; - header.size = mem::size_of::() as u32; - header.pad1 = 0; - header.pid = process::id(); - header.flags = 0; - - file.iowrite_with(header, NATIVE)?; + self.jitdump_file.iowrite_with(header, NATIVE)?; Ok(()) } @@ -239,38 +274,31 @@ impl JitDumpAgent { record_name: &str, cl_record: CodeLoadRecord, code_buffer: &[u8], - ) -> Result<(), JitDumpError> { - let mut jitdump_file = self.jitdump_file.as_ref().unwrap(); - jitdump_file.iowrite_with(cl_record, NATIVE)?; - jitdump_file.write_all(record_name.as_bytes())?; - jitdump_file.write_all(b"\0")?; - jitdump_file.write_all(code_buffer)?; + ) -> Result<()> { + self.jitdump_file.iowrite_with(cl_record, NATIVE)?; + self.jitdump_file.write_all(record_name.as_bytes())?; + self.jitdump_file.write_all(b"\0")?; + self.jitdump_file.write_all(code_buffer)?; Ok(()) } /// Write DebugInfoRecord to open jit dump file. /// Must be written before the corresponding CodeLoadRecord. - fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> Result<(), JitDumpError> { - self.jitdump_file - .as_ref() - .unwrap() - .iowrite_with(dir_record, NATIVE)?; + fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> Result<()> { + self.jitdump_file.iowrite_with(dir_record, NATIVE)?; Ok(()) } /// Write DebugInfoRecord to open jit dump file. /// Must be written before the corresponding CodeLoadRecord. - fn write_debug_info_entries( - &mut self, - die_entries: Vec, - ) -> Result<(), JitDumpError> { + fn write_debug_info_entries(&mut self, die_entries: Vec) -> Result<()> { for entry in die_entries.iter() { - let mut jitdump_file = self.jitdump_file.as_ref().unwrap(); - jitdump_file.iowrite_with(entry.address, NATIVE)?; - jitdump_file.iowrite_with(entry.line, NATIVE)?; - jitdump_file.iowrite_with(entry.discriminator, NATIVE)?; - jitdump_file.write_all(entry.filename.as_bytes())?; - jitdump_file.write_all(b"\0")?; + self.jitdump_file.iowrite_with(entry.address, NATIVE)?; + self.jitdump_file.iowrite_with(entry.line, NATIVE)?; + self.jitdump_file + .iowrite_with(entry.discriminator, NATIVE)?; + self.jitdump_file.write_all(entry.filename.as_bytes())?; + self.jitdump_file.write_all(b"\0")?; } Ok(()) } @@ -278,25 +306,27 @@ impl JitDumpAgent { /// Sent when a method is compiled and loaded into memory by the VM. pub fn module_load( &mut self, - module_name: &str, - addr: *const u8, - len: usize, + module: &Module, + functions: &PrimaryMap, dbg_image: Option<&[u8]>, ) -> () { let pid = process::id(); let tid = pid; // ThreadId does appear to track underlying thread. Using PID. - if let Some(img) = &dbg_image { - if let Err(err) = self.dump_from_debug_image(img, module_name, addr, len, pid, tid) { - println!( - "Jitdump: module_load failed dumping from debug image: {:?}\n", - err - ); + for (idx, func) in functions.iter() { + let (addr, len) = unsafe { ((**func).as_ptr() as *const u8, (**func).len()) }; + if let Some(img) = &dbg_image { + if let Err(err) = self.dump_from_debug_image(img, "wasm", addr, len, pid, tid) { + println!( + "Jitdump: module_load failed dumping from debug image: {:?}\n", + err + ); + } + } else { + let timestamp = self.get_time_stamp(); + let name = super::debug_name(module, idx); + self.dump_code_load_record(&name, addr, len, timestamp, pid, tid); } - } else { - let mut timestamp: u64 = 0; - JitDumpAgent::get_time_stamp(&mut timestamp); - self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); } } @@ -347,7 +377,7 @@ impl JitDumpAgent { len: usize, pid: u32, tid: u32, - ) -> Result<(), JitDumpError> { + ) -> Result<()> { let file = object::File::parse(&dbg_image).unwrap(); let endian = if file.is_little_endian() { gimli::RunTimeEndian::Little @@ -355,7 +385,7 @@ impl JitDumpAgent { gimli::RunTimeEndian::Big }; - let load_section = |id: gimli::SectionId| -> Result, JitDumpError> { + let load_section = |id: gimli::SectionId| -> Result> { Ok(file .section_data_by_name(id.name()) .unwrap_or(borrow::Cow::Borrowed(&[][..]))) @@ -385,8 +415,7 @@ impl JitDumpAgent { break; } if !self.dump_funcs { - let mut timestamp: u64 = 0; - JitDumpAgent::get_time_stamp(&mut timestamp); + let timestamp = self.get_time_stamp(); self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); } Ok(()) @@ -401,7 +430,7 @@ impl JitDumpAgent { len: usize, pid: u32, tid: u32, - ) -> Result<(), JitDumpError> { + ) -> Result<()> { let mut depth = 0; let mut entries = unit.entries(); while let Some((delta_depth, entry)) = entries.next_dfs()? { @@ -486,9 +515,7 @@ impl JitDumpAgent { self.code_index += 1; self.dump_debug_info(&unit, &dwarf, clr.address, clr.size, None)?; - let mut timestamp: u64 = 0; - JitDumpAgent::get_time_stamp(&mut timestamp); - clr.header.timestamp = timestamp; + clr.header.timestamp = self.get_time_stamp(); unsafe { let code_buffer: &[u8] = @@ -570,9 +597,8 @@ impl JitDumpAgent { address: u64, size: u64, file_suffix: Option<&str>, - ) -> Result<(), JitDumpError> { - let mut timestamp: u64 = 0; - JitDumpAgent::get_time_stamp(&mut timestamp); + ) -> Result<()> { + let timestamp = self.get_time_stamp(); if let Some(program) = unit.line_program.clone() { let mut debug_info_record = DebugInfoRecord { header: RecordHeader { @@ -636,60 +662,17 @@ impl JitDumpAgent { } } -use crate::ProfilingAgent; -impl ProfilingAgent for JitDumpAgent { - fn module_load( - &mut self, - module_name: &str, - addr: *const u8, - len: usize, - dbg_image: Option<&[u8]>, - ) -> () { - if self.jitdump_file.is_none() { - if JitDumpAgent::init(self).ok().is_some() { - JitDumpAgent::module_load(self, module_name, addr, len, dbg_image); - } else { - println!("Jitdump: Failed to initialize JitDumpAgent\n"); - } - } else { - JitDumpAgent::module_load(self, module_name, addr, len, dbg_image); +impl Drop for State { + fn drop(&mut self) { + unsafe { + libc::munmap( + self.map_addr as *mut _, + libc::sysconf(libc::_SC_PAGESIZE) as usize, + ); } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JitDumpError { - GimliError(gimli::Error), - IOError, -} - -impl Error for JitDumpError { - fn description(&self) -> &str { - match *self { - JitDumpError::GimliError(ref err) => err.description(), - JitDumpError::IOError => "An I/O error occurred.", - } - } -} - -impl std::fmt::Display for JitDumpError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { - Debug::fmt(self, f) - } -} - -impl From for JitDumpError { - fn from(err: gimli::Error) -> Self { - JitDumpError::GimliError(err) - } -} - -impl From for JitDumpError { - fn from(_err: std::io::Error) -> Self { - JitDumpError::IOError - } -} - trait Reader: gimli::Reader + Send + Sync {} impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where diff --git a/crates/profiling/src/lib.rs b/crates/profiling/src/lib.rs index 7e6c851a10..d708143140 100644 --- a/crates/profiling/src/lib.rs +++ b/crates/profiling/src/lib.rs @@ -1,40 +1,36 @@ use std::error::Error; use std::fmt; +use wasmtime_environ::entity::{EntityRef, PrimaryMap}; +use wasmtime_environ::wasm::DefinedFuncIndex; +use wasmtime_environ::Module; +use wasmtime_runtime::VMFunctionBody; -#[cfg(feature = "jitdump")] -mod jitdump; - -#[cfg(feature = "jitdump")] -pub use crate::jitdump::JitDumpAgent; - -#[cfg(not(feature = "jitdump"))] -pub type JitDumpAgent = NullProfilerAgent; - -/// Select which profiling technique to use -#[derive(Debug, Clone, Copy)] -pub enum ProfilingStrategy { - /// No profiler support - NullProfiler, - - /// Collect profile for jitdump file format - JitDumpProfiler, +cfg_if::cfg_if! { + if #[cfg(all(feature = "jitdump", target_os = "linux"))] { + #[path = "jitdump_linux.rs"] + mod jitdump; + } else { + #[path = "jitdump_disabled.rs"] + mod jitdump; + } } +pub use crate::jitdump::JitDumpAgent; + /// Common interface for profiling tools. -pub trait ProfilingAgent { +pub trait ProfilingAgent: Send + Sync + 'static { /// Notify the profiler of a new module loaded into memory fn module_load( - &mut self, - module_name: &str, - addr: *const u8, - len: usize, + &self, + module: &Module, + functions: &PrimaryMap, dbg_image: Option<&[u8]>, ) -> (); } /// Default agent for unsupported profiling build. #[derive(Debug, Default, Clone, Copy)] -pub struct NullProfilerAgent {} +pub struct NullProfilerAgent; #[derive(Debug)] struct NullProfilerAgentError; @@ -55,11 +51,19 @@ impl Error for NullProfilerAgentError { impl ProfilingAgent for NullProfilerAgent { fn module_load( - &mut self, - _module_name: &str, - _addr: *const u8, - _len: usize, + &self, + _module: &Module, + _functions: &PrimaryMap, _dbg_image: Option<&[u8]>, ) -> () { } } + +#[allow(dead_code)] +fn debug_name(module: &Module, index: DefinedFuncIndex) -> String { + let index = module.local.func_index(index); + match module.func_names.get(&index) { + Some(s) => s.clone(), + None => format!("wasm::wasm-function[{}]", index.index()), + } +} diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 6a40360d28..516498566a 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -11,7 +11,6 @@ readme = "README.md" edition = "2018" [dependencies] -wasmtime-profiling = { path = "../profiling", version = "0.12.0" } wasmtime-environ = { path = "../environ", version = "0.12.0" } region = "2.0.0" libc = { version = "0.2.60", default-features = false } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 47fae58778..3a84c96bcb 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" anyhow = "1.0" log = { version = "0.4.8", default-features = false } wasi-common = { path = "../wasi-common", version = "0.12.0" } -wasmtime = { path = "../api", version = "0.12.0" } +wasmtime = { path = "../api", version = "0.12.0", default-features = false } wasmtime-runtime = { path = "../runtime", version = "0.12.0" } wig = { path = "../wasi-common/wig", version = "0.12.0" } diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml index 4425b2f75e..917ed3c989 100644 --- a/crates/wast/Cargo.toml +++ b/crates/wast/Cargo.toml @@ -12,11 +12,11 @@ edition = "2018" [dependencies] anyhow = "1.0.19" -wasmtime = { path = "../api", version = "0.12.0" } +wasmtime = { path = "../api", version = "0.12.0", default-features = false } wast = "11.0.0" [badges] maintenance = { status = "actively-developed" } [features] -lightbeam = [] +lightbeam = ["wasmtime/lightbeam"] diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index ab60ce0bd7..9db228cf27 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ - [Running `hello-world.wasm`](./tutorial-run-hello-world.md) - [Examples](./examples.md) - [Markdown parser](./examples-markdown.md) + - [Profiling WebAssembly](./examples-profiling.md) - [Using WebAssembly from your lanugage](./lang.md) - [Python](./lang-python.md) - [.NET](./lang-dotnet.md) diff --git a/docs/assets/perf-annotate-fib.png b/docs/assets/perf-annotate-fib.png new file mode 100755 index 0000000000..be296a7674 Binary files /dev/null and b/docs/assets/perf-annotate-fib.png differ diff --git a/docs/assets/perf-report-fib.png b/docs/assets/perf-report-fib.png new file mode 100755 index 0000000000..2589b77cf8 Binary files /dev/null and b/docs/assets/perf-report-fib.png differ diff --git a/docs/examples-profiling.md b/docs/examples-profiling.md new file mode 100644 index 0000000000..36666dae57 --- /dev/null +++ b/docs/examples-profiling.md @@ -0,0 +1,152 @@ +# Profiling WebAssembly + +One of WebAssembly's major goals is to be quite close to native code in terms of +performance, so typically when executing wasm you'll be quite interested in how +well your wasm module is performing! From time to time you might want to dive a +bit deeper into the performance of your wasm, and this is where profiling comes +into the picture. + +Profiling support in Wasmtime is still under development, but if you're using a +supported profiler this example is targeted at helping you get some more +information about the performance of your wasm. + +## Using `perf` on Linux + +One profiler supported by Wasmtime is the [`perf` +profiler](https://perf.wiki.kernel.org/index.php/Main_Page) for Linux. This is +an extremely powerful profiler with lots of documentation on the web, but for +the rest of this section we'll assume you're running on Linux and already have +`perf` installed. + +Profiling support with `perf` uses the "jitdump" support in the `perf` CLI. This +requires runtime support from Wasmtime itself, so you will need to manually +change a few things to enable profiling support in your application. First +you'll want to make sure that Wasmtime is compiled with the `jitdump` Cargo +feature (which is enabled by default). Otherwise enabling runtime support +depends on how you're using Wasmtime: + +* **Rust API** - you'll want to call the [`Config::profiler`] method with + `ProfilingStrategy::JitDump` to enable profiling of your wasm modules. + +* **C API** - you'll want to call the `wasmtime_config_profiler_set` API with a + `WASMTIME_PROFILING_STRATEGY_JITDUMP` value. + +* **Command Line** - you'll want to pass the `--jitdump` flag on the command + line. + +Once jitdump support is enabled, you'll use `perf record` like usual to record +your application's performance. You'll need to also be sure to pass the +`--clockid mono` or `-k mono` flag to `perf record`. + +For example if you're using the CLI, you'll execute: + +```sh +$ perf record -k mono wasmtime --jitdump foo.wasm +``` + +This will create a `perf.data` file as per usual, but it will *also* create a +`jit-XXXX.dump` file. This extra `*.dump` file is the jitdump file which is +specified by `perf` and Wasmtime generates at runtime. + +The next thing you need to do is to merge the `*.dump` file into the +`perf.data` file, which you can do with the `perf inject` command: + +```sh +$ perf inject --jit --input perf.data --output perf.jit.data +``` + +This will read `perf.data`, automatically pick up the `*.dump` file that's +correct, and then create `perf.jit.data` which merges all the JIT information +together. This should also create a lot of `jitted-XXXX-N.so` files in the +current directory which are ELF images for all the JIT functions that were +created by Wasmtime. + +After that you can explore the `perf.jit.data` profile as you usually would, +for example with: + +```sh +$ perf report --input perf.jit.data +``` + +You should be able to annotate wasm functions and see their raw assembly. You +should also see entries for wasm functions show up as one function and the +name of each function matches the debug name section in the wasm file. + +Note that support for jitdump is still relatively new in Wasmtime, so if you +have any problems, please don't hesitate to [file an issue]! + +[file an issue]: https://github.com/bytecodealliance/wasmtime/issues/new + +### `perf` and DWARF information + +If the jitdump profile doesn't give you enough information by default, you can +also enable dwarf debug information to be generated for JIT code which should +give the `perf` profiler more information about what's being profiled. This can +include information like more desriptive function names, filenames, and line +numbers. + +Enabling dwarf debug information for JIT code depends on how you're using +Wasmtime: + +* **Rust API** - you'll want to call the [`Config::debug_info`] method. + +* **C API** - you'll want to call the `wasmtime_config_debug_info_set` API. + +* **Command Line** - you'll want to pass the `-g` flag on the command line. + +You shouldn't need to do anything else to get this information into `perf`. The +perf collection data should automatically pick up all this dwarf debug +information. + +### `perf` example + +Let's run through a quick example with `perf` to get the feel for things. First +let's take a look at some wasm: + +```rust +fn main() { + let n = 42; + println!("fib({}) = {}", n, fib(n)); +} + +fn fib(n: u32) -> u32 { + if n <= 2 { + 1 + } else { + fib(n - 1) + fib(n - 2) + } +} +``` + +To collect perf information for this wasm module we'll execute: + +```sh +$ rustc --target wasm32-wasi fib.rs -O +$ perf record -k mono wasmtime --jitdump fib.wasm +fib(42) = 267914296 +[ perf record: Woken up 1 times to write data ] +[ perf record: Captured and wrote 0.147 MB perf.data (3435 samples) ] +$ perf inject --jit --input perf.data --output perf.jit.data +``` + +And we should have all out information now! We can execute `perf report` for +example to see that 99% of our runtime (as expected) is spent in our `fib` +function. Note that the symbol has been demangled to `fib::fib` which is what +the Rust symbol is: + +``` +$ perf report --input perf.jit-data +``` + +![perf report output](assets/perf-report-fib.png) + +Alternatively we could also use `perf annotate` to take a look at the +disassembly of the `fib` function, seeing what the JIT generated: + +``` +$ perf annotate --input perf.jit-data +``` + +![perf annotate output](assets/perf-annotate-fib.png) + +[`Config::debug_info`]: https://bytecodealliance.github.io/wasmtime/api/wasmtime/struct.Config.html#method.debug_info diff --git a/src/lib.rs b/src/lib.rs index 89c5f417a5..db0d65b72c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,8 +30,7 @@ mod obj; use anyhow::{bail, Result}; use std::path::PathBuf; use structopt::StructOpt; -use wasmtime::{Config, Strategy}; -use wasmtime_profiling::ProfilingStrategy; +use wasmtime::{Config, ProfilingStrategy, Strategy}; pub use obj::compile_to_obj; @@ -46,8 +45,8 @@ fn pick_compilation_strategy(cranelift: bool, lightbeam: bool) -> Result Result { Ok(match jitdump { - true => ProfilingStrategy::JitDumpProfiler, - false => ProfilingStrategy::NullProfiler, + true => ProfilingStrategy::JitDump, + false => ProfilingStrategy::None, }) } diff --git a/tests/instantiate.rs b/tests/instantiate.rs deleted file mode 100644 index 961b219e8d..0000000000 --- a/tests/instantiate.rs +++ /dev/null @@ -1,40 +0,0 @@ -use more_asserts::assert_gt; -use std::path::PathBuf; -use wasmtime_environ::settings; -use wasmtime_environ::settings::Configurable; -use wasmtime_environ::CacheConfig; -use wasmtime_jit::{instantiate, native, CompilationStrategy, Compiler, NullResolver}; - -const PATH_MODULE_RS2WASM_ADD_FUNC: &str = r"tests/wasm/rs2wasm-add-func.wat"; - -/// Simple test reading a wasm-file and translating to binary representation. -#[test] -fn test_environ_translate() { - let path = PathBuf::from(PATH_MODULE_RS2WASM_ADD_FUNC); - let data = wat::parse_file(path).expect("expecting valid wat-file"); - assert_gt!(data.len(), 0); - - let mut flag_builder = settings::builder(); - flag_builder.enable("enable_verifier").unwrap(); - - let isa_builder = native::builder(); - let isa = isa_builder.finish(settings::Flags::new(flag_builder)); - - let mut resolver = NullResolver {}; - let cache_config = CacheConfig::new_cache_disabled(); - let mut compiler = Compiler::new(isa, CompilationStrategy::Auto, cache_config); - unsafe { - let instance = instantiate( - &mut compiler, - &data, - &mut resolver, - // Bulk memory. - false, - // Debug info. - false, - // Profiler. - None, - ); - assert!(instance.is_ok()); - } -} diff --git a/tests/misc_testsuite/rs2wasm-add-func.wast b/tests/misc_testsuite/rs2wasm-add-func.wast new file mode 100644 index 0000000000..21ff7f5d7a --- /dev/null +++ b/tests/misc_testsuite/rs2wasm-add-func.wast @@ -0,0 +1,20 @@ +(module + (type (;0;) (func)) + (type (;1;) (func (param i32 i32) (result i32))) + (func $add (type 1) (param i32 i32) (result i32) + get_local 1 + get_local 0 + i32.add) + (func $start (type 0)) + (table (;0;) 1 1 anyfunc) + (memory (;0;) 17) + (global (;0;) i32 (i32.const 1049114)) + (global (;1;) i32 (i32.const 1049114)) + (export "memory" (memory 0)) + (export "__indirect_function_table" (table 0)) + (export "__heap_base" (global 0)) + (export "__data_end" (global 1)) + (export "add" (func $add)) + (export "start" (func $start)) + (data (i32.const 1048576) "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00") + (data (i32.const 1049092) "invalid malloc request"))