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}