Skip to main content

wasmtime/runtime/
profiling.rs

1#[cfg(feature = "component-model")]
2use crate::component::Component;
3use crate::runtime::vm::Backtrace;
4use crate::{AsContext, CallHook, Module};
5use crate::{Engine, prelude::*};
6use core::cmp::Ordering;
7use fxprof_processed_profile::debugid::DebugId;
8use fxprof_processed_profile::{
9    CategoryHandle, Frame, FrameFlags, FrameInfo, LibraryInfo, MarkerLocations, MarkerTiming,
10    Profile, ReferenceTimestamp, StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, Symbol,
11    SymbolTable, Timestamp,
12};
13use std::ops::Range;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use wasmtime_environ::{FuncKey, demangle_function_name_or_index};
17
18// TODO: collect more data
19// - On non-Windows, measure thread-local CPU usage between events with
20//   rustix::time::clock_gettime(ClockId::ThreadCPUTime)
21// - Report which wasm module, and maybe instance, each frame came from
22
23/// Collects basic profiling data for a single WebAssembly guest.
24///
25/// This profiler can't provide measurements that are as accurate or detailed
26/// as a platform-specific profiler, such as `perf` on Linux. On the other
27/// hand, this profiler works on every platform that Wasmtime supports. Also,
28/// as an embedder you can use this profiler selectively on individual guest
29/// instances rather than profiling the entire process.
30///
31/// To use this, you'll need to arrange to call [`GuestProfiler::sample`] at
32/// regular intervals while the guest is on the stack. The most straightforward
33/// way to do that is to call it from a callback registered with
34/// [`Store::epoch_deadline_callback()`](crate::Store::epoch_deadline_callback).
35///
36/// Starting a profile with [`GuestProfiler::new`] prepares the profiled
37/// modules each time, which for modules with many functions can take
38/// milliseconds. To profile the same modules many times, for example once per
39/// instance, prepare them once with [`GuestProfilerModules`] and start each
40/// profile with [`GuestProfiler::new_with_modules`].
41///
42/// # Accuracy
43///
44/// The data collection granularity is limited by the mechanism you use to
45/// interrupt guest execution and collect a profiling sample.
46///
47/// If you use epoch interruption, then samples will only be collected at
48/// function entry points and loop headers. This introduces some bias to the
49/// results. In addition, samples will only be taken at times when WebAssembly
50/// functions are running, not during host-calls.
51///
52/// It is technically possible to use fuel interruption instead. That
53/// introduces worse bias since samples occur after a certain number of
54/// WebAssembly instructions, which can take different amounts of time.
55///
56/// You may instead be able to use platform-specific methods, such as
57/// `setitimer(ITIMER_VIRTUAL, ...)` on POSIX-compliant systems, to sample on
58/// a more accurate interval. The only current requirement is that the guest
59/// you wish to profile must be on the same stack where you call `sample`,
60/// and executing within the same thread. However, the `GuestProfiler::sample`
61/// method is not currently async-signal-safe, so doing this correctly is not
62/// easy.
63///
64/// # Security
65///
66/// Profiles produced using this profiler do not include any configuration
67/// details from the host, such as virtual memory addresses, or from any
68/// WebAssembly modules that you haven't specifically allowed. So for
69/// example, these profiles should be safe to share with untrusted users
70/// who have provided untrusted code that you are running in a multi-tenancy
71/// environment.
72///
73/// However, the profile does include byte offsets into the text section of
74/// the compiled module, revealing some information about the size of the code
75/// generated for each module. For user-provided modules, the user could get
76/// the same information by compiling the module for themself using a similar
77/// version of Wasmtime on the same target architecture, but for any module
78/// where they don't already have the WebAssembly module binary available this
79/// could theoretically lead to an undesirable information disclosure. So you
80/// should only include user-provided modules in profiles.
81#[derive(Debug)]
82pub struct GuestProfiler {
83    profile: Profile,
84    modules: GuestProfilerModules,
85    /// Library handle in `profile` for each entry of `modules`, by index.
86    libs: Vec<fxprof_processed_profile::LibraryHandle>,
87    process: fxprof_processed_profile::ProcessHandle,
88    thread: fxprof_processed_profile::ThreadHandle,
89    start: Instant,
90    marker: CallMarker,
91}
92
93/// The set of modules a [`GuestProfiler`] attributes samples to, prepared
94/// once and shared by any number of profilers.
95///
96/// Preparing a module builds its symbol table, which demangles the name of
97/// every function it defines; for modules with many functions this is the
98/// bulk of the cost of starting a profile. [`GuestProfiler::new`] does this
99/// work every time it is called. Embedders that profile the same modules
100/// repeatedly, for example once per request or once per instance, can instead
101/// build a `GuestProfilerModules` once and pass it to
102/// [`GuestProfiler::new_with_modules`] for each profile, which then does no
103/// per-module work beyond registering the prepared symbol tables.
104///
105/// Cloning is cheap, since the prepared modules are reference-counted, and a
106/// `GuestProfilerModules` can be shared between threads. Profiles started
107/// from the same set are independent of one another.
108///
109/// # Example
110///
111/// ```
112/// # use std::time::Duration;
113/// # use wasmtime::*;
114/// # fn main() -> Result<()> {
115/// let engine = Engine::default();
116/// let module = Module::new(&engine, r#"(module (func (export "run")))"#)?;
117///
118/// // Prepare the modules once...
119/// let modules = GuestProfilerModules::new(&engine, [("main.wasm".to_string(), module.clone())])?;
120///
121/// // ...then start a profile for each guest that runs them.
122/// for _ in 0..3 {
123///     let profiler = GuestProfiler::new_with_modules("main", Duration::from_millis(10), &modules);
124///     let mut store = Store::new(&engine, profiler);
125///     // Arrange to call `GuestProfiler::sample` while the guest runs, for
126///     // example from `Store::epoch_deadline_callback`.
127///     let instance = Instance::new(&mut store, &module, &[])?;
128///     instance
129///         .get_typed_func::<(), ()>(&mut store, "run")?
130///         .call(&mut store, ())?;
131///     let mut profile = Vec::new();
132///     store.into_data().finish(&mut profile)?;
133/// }
134/// # Ok(())
135/// # }
136/// ```
137#[derive(Clone, Debug)]
138pub struct GuestProfilerModules {
139    /// Sorted by `text_range.start`, for lookup by program counter.
140    modules: Arc<[PreparedModule]>,
141}
142
143#[derive(Debug)]
144struct PreparedModule {
145    name: String,
146    module: Module,
147    text_range: Range<usize>,
148    symbol_table: Arc<SymbolTable>,
149}
150
151impl GuestProfilerModules {
152    /// Prepares `modules` for profiling.
153    ///
154    /// Each entry pairs a module with the name it is given in profiles. Only
155    /// modules in `modules` appear in stack traces of profiles that use this
156    /// set; stack frames executing host code or functions from other modules
157    /// are omitted. See the "Security" section of the [`GuestProfiler`]
158    /// documentation for guidance on which modules to include. Modules that
159    /// define no functions are skipped, since no sample can land in them.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if `engine` has guest debugging enabled, which
164    /// profiling does not support.
165    ///
166    /// # Panics
167    ///
168    /// Panics if any module in `modules` was not created with `engine`.
169    pub fn new(
170        engine: &Engine,
171        modules: impl IntoIterator<Item = (String, Module)>,
172    ) -> Result<Self> {
173        // Check that guest debugging is not enabled. The
174        // instrumentation would make profiling results unreliable,
175        // but more fundamentally, it means that code is cloned per
176        // instantiation (for breakpoint patching) so the logic below
177        // is incorrect.
178        if engine.tunables().debug_guest {
179            crate::bail!("Profiling cannot be performed when guest-debugging is enabled.");
180        }
181
182        let mut modules: Vec<_> = modules
183            .into_iter()
184            .filter_map(|(name, module)| {
185                assert!(Engine::same(module.engine(), engine));
186                let compiled = module.compiled_module();
187                let text_range = {
188                    // Assumption: within text, the code for a given module is packed linearly and
189                    // is non-overlapping; if this is violated, it should be safe but might result
190                    // in incorrect profiling results.
191                    let start = compiled.finished_function_ranges().next()?.1.start;
192                    let end = compiled.finished_function_ranges().last()?.1.end;
193
194                    let start = (module.engine_code().text_range().start + start).raw();
195                    let end = (module.engine_code().text_range().start + end).raw();
196                    start..end
197                };
198                let symbol_table = module_symbol_table(&module)?;
199                Some(PreparedModule {
200                    name,
201                    module,
202                    text_range,
203                    symbol_table,
204                })
205            })
206            .collect();
207
208        modules.sort_unstable_by_key(|m| m.text_range.start);
209        Ok(Self {
210            modules: modules.into(),
211        })
212    }
213
214    /// Prepares the static modules of `component`, plus `extra_modules`, for
215    /// profiling.
216    ///
217    /// See [`GuestProfilerModules::new`] for additional information.
218    #[cfg(feature = "component-model")]
219    pub fn new_component(
220        engine: &Engine,
221        component: &Component,
222        extra_modules: impl IntoIterator<Item = (String, Module)>,
223    ) -> Result<Self> {
224        let modules = component
225            .static_modules()
226            .map(|m| (m.name().unwrap_or("<unknown>").to_string(), m.clone()))
227            .chain(extra_modules);
228        Self::new(engine, modules)
229    }
230}
231
232impl GuestProfiler {
233    /// Begin profiling a new guest. When this function is called, the current
234    /// wall-clock time is recorded as the start time for the guest.
235    ///
236    /// The `module_name` parameter is recorded in the profile to help identify
237    /// where the profile came from.
238    ///
239    /// The `interval` parameter should match the rate at which you intend
240    /// to call `sample`. However, this is used as a hint and not required to
241    /// exactly match the real sample rate.
242    ///
243    /// Only modules which are present in the `modules` vector will appear in
244    /// stack traces in this profile. Any stack frames which were executing
245    /// host code or functions from other modules will be omitted. See the
246    /// "Security" section of the [`GuestProfiler`] documentation for guidance
247    /// on what modules should not be included in this list.
248    ///
249    /// This prepares `modules` afresh for every profile, which for modules
250    /// with many functions is most of this function's cost. To profile the
251    /// same modules repeatedly, prepare them once with
252    /// [`GuestProfilerModules::new`] and use
253    /// [`GuestProfiler::new_with_modules`] instead.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if `engine` has guest debugging enabled, which
258    /// profiling does not support.
259    ///
260    /// # Panics
261    ///
262    /// Panics if any module in `modules` was not created with `engine`.
263    pub fn new(
264        engine: &Engine,
265        module_name: &str,
266        interval: Duration,
267        modules: impl IntoIterator<Item = (String, Module)>,
268    ) -> Result<Self> {
269        let modules = GuestProfilerModules::new(engine, modules)?;
270        Ok(Self::new_with_modules(module_name, interval, &modules))
271    }
272
273    /// Begin profiling a new guest using a set of modules prepared with
274    /// [`GuestProfilerModules`]. When this function is called, the current
275    /// wall-clock time is recorded as the start time for the guest.
276    ///
277    /// This behaves like [`GuestProfiler::new`], except that the per-module
278    /// preparation has already been done, so starting a profile costs the
279    /// same however many functions the modules define. Only modules in
280    /// `modules` will appear in stack traces in this profile.
281    ///
282    /// See [`GuestProfiler::new`] for the meaning of `module_name` and
283    /// `interval`.
284    pub fn new_with_modules(
285        module_name: &str,
286        interval: Duration,
287        modules: &GuestProfilerModules,
288    ) -> Self {
289        let zero = ReferenceTimestamp::from_millis_since_unix_epoch(0.0);
290        let mut profile = Profile::new(module_name, zero, interval.into());
291
292        let libs = modules
293            .modules
294            .iter()
295            .map(|m| {
296                // The profile keys libraries by value, so the symbol table is
297                // attached after the library is added rather than carried in
298                // the `LibraryInfo`, which would hash every function name.
299                let lib = profile.add_lib(LibraryInfo {
300                    name: m.name.clone(),
301                    debug_name: String::new(),
302                    path: String::new(),
303                    debug_path: String::new(),
304                    debug_id: DebugId::nil(),
305                    code_id: None,
306                    arch: None,
307                    symbol_table: None,
308                });
309                profile.set_lib_symbol_table(lib, m.symbol_table.clone());
310                lib
311            })
312            .collect();
313
314        profile.set_reference_timestamp(std::time::SystemTime::now().into());
315        let process = profile.add_process(module_name, 0, Timestamp::from_nanos_since_reference(0));
316        let thread = profile.add_thread(process, 0, Timestamp::from_nanos_since_reference(0), true);
317        let start = Instant::now();
318        let marker = CallMarker::new(&mut profile);
319        Self {
320            profile,
321            modules: modules.clone(),
322            libs,
323            process,
324            thread,
325            start,
326            marker,
327        }
328    }
329
330    /// Create a new profiler for the provided component
331    ///
332    /// See [`GuestProfiler::new`] for additional information; this function
333    /// works identically except that it takes a component and sets up
334    /// instrumentation to track calls in each of its constituent modules.
335    #[cfg(feature = "component-model")]
336    pub fn new_component(
337        engine: &Engine,
338        component_name: &str,
339        interval: Duration,
340        component: Component,
341        extra_modules: impl IntoIterator<Item = (String, Module)>,
342    ) -> Result<Self> {
343        let modules = GuestProfilerModules::new_component(engine, &component, extra_modules)?;
344        Ok(Self::new_with_modules(component_name, interval, &modules))
345    }
346
347    /// Add a sample to the profile. This function collects a backtrace from
348    /// any stack frames for allowed modules on the current stack. It should
349    /// typically be called from a callback registered using
350    /// [`Store::epoch_deadline_callback()`](crate::Store::epoch_deadline_callback).
351    ///
352    /// The `delta` parameter is the amount of CPU time that was used by this
353    /// guest since the previous sample. It is allowed to pass `Duration::ZERO`
354    /// here if recording CPU usage information is not needed.
355    pub fn sample(&mut self, store: impl AsContext, delta: Duration) {
356        let now = Timestamp::from_nanos_since_reference(
357            self.start.elapsed().as_nanos().try_into().unwrap(),
358        );
359        let backtrace = Backtrace::new(store.as_context().0);
360        let frames = lookup_frames(&self.modules.modules, &self.libs, &backtrace);
361        let stack = self
362            .profile
363            .intern_stack_frames(self.thread, frames.into_iter());
364        self.profile
365            .add_sample(self.thread, now, stack, delta.into(), 1);
366    }
367
368    /// Add a marker for transitions between guest and host to the profile.
369    /// This function should typically be called from a callback registered
370    /// using [`Store::call_hook()`](crate::Store::call_hook), and the `kind`
371    /// parameter should be the value of the same type passed into that hook.
372    pub fn call_hook(&mut self, store: impl AsContext, kind: CallHook) {
373        let now = Timestamp::from_nanos_since_reference(
374            self.start.elapsed().as_nanos().try_into().unwrap(),
375        );
376        match kind {
377            CallHook::CallingWasm | CallHook::ReturningFromWasm => {}
378            CallHook::CallingHost => {
379                let backtrace = Backtrace::new(store.as_context().0);
380                let frames = lookup_frames(&self.modules.modules, &self.libs, &backtrace);
381                let marker = self.profile.add_marker(
382                    self.thread,
383                    MarkerTiming::IntervalStart(now),
384                    self.marker,
385                );
386                let stack = self
387                    .profile
388                    .intern_stack_frames(self.thread, frames.into_iter());
389                self.profile.set_marker_stack(self.thread, marker, stack);
390            }
391            CallHook::ReturningFromHost => {
392                self.profile
393                    .add_marker(self.thread, MarkerTiming::IntervalEnd(now), self.marker);
394            }
395        }
396    }
397
398    /// When the guest finishes running, call this function to write the
399    /// profile to the given `output`. The output is a JSON-formatted object in
400    /// the [Firefox "processed profile format"][fmt]. Files in this format may
401    /// be visualized at <https://profiler.firefox.com/>.
402    ///
403    /// [fmt]: https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
404    pub fn finish(mut self, output: impl std::io::Write) -> Result<()> {
405        let now = Timestamp::from_nanos_since_reference(
406            self.start.elapsed().as_nanos().try_into().unwrap(),
407        );
408        self.profile.set_thread_end_time(self.thread, now);
409        self.profile.set_process_end_time(self.process, now);
410
411        serde_json::to_writer(output, &self.profile)?;
412        Ok(())
413    }
414}
415
416fn module_symbol_table(module: &Module) -> Option<Arc<SymbolTable>> {
417    let compiled = module.compiled_module();
418    let symbols = Vec::from_iter(
419        module
420            .env_module()
421            .defined_func_indices()
422            .map(|defined_idx| {
423                let key =
424                    FuncKey::DefinedWasmFunction(module.env_module().module_index, defined_idx);
425                let loc = compiled.func_loc(key);
426                let func_idx = compiled.module().func_index(defined_idx);
427                let mut name = String::new();
428                demangle_function_name_or_index(
429                    &mut name,
430                    compiled.func_name(func_idx),
431                    defined_idx.as_u32() as usize,
432                )
433                .unwrap();
434                Symbol {
435                    address: loc.start,
436                    size: Some(loc.length),
437                    name,
438                }
439            }),
440    );
441    if symbols.is_empty() {
442        return None;
443    }
444
445    Some(Arc::new(SymbolTable::new(symbols)))
446}
447
448fn lookup_frames<'a>(
449    modules: &'a [PreparedModule],
450    libs: &'a [fxprof_processed_profile::LibraryHandle],
451    backtrace: &'a Backtrace,
452) -> impl Iterator<Item = FrameInfo> + 'a {
453    backtrace
454        .frames()
455        // Samply needs to see the oldest frame first, but we list the newest
456        // first, so iterate in reverse.
457        .rev()
458        .filter_map(|frame| {
459            let idx = modules
460                .binary_search_by(|probe| {
461                    if probe.text_range.contains(&frame.pc()) {
462                        Ordering::Equal
463                    } else {
464                        probe.text_range.start.cmp(&frame.pc())
465                    }
466                })
467                .ok()?;
468            let module = modules.get(idx)?;
469
470            // We need to point to the modules full text (not just its functions) as
471            // the offset for the final phase; these can be different for component
472            // model modules.
473            let module_text_start = module.module.text().as_ptr_range().start as usize;
474            return Some(FrameInfo {
475                frame: Frame::RelativeAddressFromReturnAddress(
476                    libs[idx],
477                    u32::try_from(frame.pc() - module_text_start).unwrap(),
478                ),
479                category_pair: CategoryHandle::OTHER.into(),
480                flags: FrameFlags::empty(),
481            });
482        })
483}
484
485#[derive(Debug, Clone, Copy)]
486struct CallMarker {
487    name: StringHandle,
488}
489
490impl CallMarker {
491    fn new(profile: &mut Profile) -> Self {
492        let name = profile.intern_string(Self::UNIQUE_MARKER_TYPE_NAME);
493        Self { name }
494    }
495}
496
497impl StaticSchemaMarker for CallMarker {
498    const UNIQUE_MARKER_TYPE_NAME: &'static str = "hostcall";
499    const FIELDS: &'static [StaticSchemaMarkerField] = &[];
500    const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART
501        .union(MarkerLocations::MARKER_TABLE.union(MarkerLocations::TIMELINE_OVERVIEW));
502
503    fn name(&self, _profile: &mut Profile) -> StringHandle {
504        self.name
505    }
506    fn category(&self, _profile: &mut Profile) -> CategoryHandle {
507        CategoryHandle::OTHER
508    }
509    fn string_field_value(&self, _field_index: u32) -> StringHandle {
510        unreachable!("no fields")
511    }
512    fn number_field_value(&self, _field_index: u32) -> f64 {
513        unreachable!("no fields")
514    }
515}