1 #[cfg(feature = "component-model")]
2 use crate::component::Component;
3 use crate::instantiate::CompiledModule;
4 use crate::prelude::*;
5 use crate::runtime::vm::Backtrace;
6 use crate::{AsContext, CallHook, Module};
7 use core::cmp::Ordering;
8 use fxprof_processed_profile::debugid::DebugId;
9 use fxprof_processed_profile::{
10     CategoryHandle, Frame, FrameFlags, FrameInfo, LibraryInfo, MarkerLocations, MarkerTiming,
11     Profile, ReferenceTimestamp, StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, Symbol,
12     SymbolTable, Timestamp,
13 };
14 use std::ops::Range;
15 use std::sync::Arc;
16 use std::time::{Duration, Instant};
17 use wasmtime_environ::demangle_function_name_or_index;
18 
19 // TODO: collect more data
20 // - On non-Windows, measure thread-local CPU usage between events with
21 //   rustix::time::clock_gettime(ClockId::ThreadCPUTime)
22 // - Report which wasm module, and maybe instance, each frame came from
23 
24 /// Collects basic profiling data for a single WebAssembly guest.
25 ///
26 /// This profiler can't provide measurements that are as accurate or detailed
27 /// as a platform-specific profiler, such as `perf` on Linux. On the other
28 /// hand, this profiler works on every platform that Wasmtime supports. Also,
29 /// as an embedder you can use this profiler selectively on individual guest
30 /// instances rather than profiling the entire process.
31 ///
32 /// To use this, you'll need to arrange to call [`GuestProfiler::sample`] at
33 /// regular intervals while the guest is on the stack. The most straightforward
34 /// way to do that is to call it from a callback registered with
35 /// [`Store::epoch_deadline_callback()`](crate::Store::epoch_deadline_callback).
36 ///
37 /// # Accuracy
38 ///
39 /// The data collection granularity is limited by the mechanism you use to
40 /// interrupt guest execution and collect a profiling sample.
41 ///
42 /// If you use epoch interruption, then samples will only be collected at
43 /// function entry points and loop headers. This introduces some bias to the
44 /// results. In addition, samples will only be taken at times when WebAssembly
45 /// functions are running, not during host-calls.
46 ///
47 /// It is technically possible to use fuel interruption instead. That
48 /// introduces worse bias since samples occur after a certain number of
49 /// WebAssembly instructions, which can take different amounts of time.
50 ///
51 /// You may instead be able to use platform-specific methods, such as
52 /// `setitimer(ITIMER_VIRTUAL, ...)` on POSIX-compliant systems, to sample on
53 /// a more accurate interval. The only current requirement is that the guest
54 /// you wish to profile must be on the same stack where you call `sample`,
55 /// and executing within the same thread. However, the `GuestProfiler::sample`
56 /// method is not currently async-signal-safe, so doing this correctly is not
57 /// easy.
58 ///
59 /// # Security
60 ///
61 /// Profiles produced using this profiler do not include any configuration
62 /// details from the host, such as virtual memory addresses, or from any
63 /// WebAssembly modules that you haven't specifically allowed. So for
64 /// example, these profiles should be safe to share with untrusted users
65 /// who have provided untrusted code that you are running in a multi-tenancy
66 /// environment.
67 ///
68 /// However, the profile does include byte offsets into the text section of
69 /// the compiled module, revealing some information about the size of the code
70 /// generated for each module. For user-provided modules, the user could get
71 /// the same information by compiling the module for themself using a similar
72 /// version of Wasmtime on the same target architecture, but for any module
73 /// where they don't already have the WebAssembly module binary available this
74 /// could theoretically lead to an undesirable information disclosure. So you
75 /// should only include user-provided modules in profiles.
76 #[derive(Debug)]
77 pub struct GuestProfiler {
78     profile: Profile,
79     modules: Modules,
80     process: fxprof_processed_profile::ProcessHandle,
81     thread: fxprof_processed_profile::ThreadHandle,
82     start: Instant,
83     marker: CallMarker,
84 }
85 
86 #[derive(Debug)]
87 struct ProfiledModule {
88     module: Module,
89     fxprof_libhandle: fxprof_processed_profile::LibraryHandle,
90     text_range: Range<usize>,
91 }
92 
93 type Modules = Vec<ProfiledModule>;
94 
95 impl GuestProfiler {
96     /// Begin profiling a new guest. When this function is called, the current
97     /// wall-clock time is recorded as the start time for the guest.
98     ///
99     /// The `module_name` parameter is recorded in the profile to help identify
100     /// where the profile came from.
101     ///
102     /// The `interval` parameter should match the rate at which you intend
103     /// to call `sample`. However, this is used as a hint and not required to
104     /// exactly match the real sample rate.
105     ///
106     /// Only modules which are present in the `modules` vector will appear in
107     /// stack traces in this profile. Any stack frames which were executing
108     /// host code or functions from other modules will be omitted. See the
109     /// "Security" section of the [`GuestProfiler`] documentation for guidance
110     /// on what modules should not be included in this list.
111     pub fn new(
112         module_name: &str,
113         interval: Duration,
114         modules: impl IntoIterator<Item = (String, Module)>,
115     ) -> Self {
116         let zero = ReferenceTimestamp::from_millis_since_unix_epoch(0.0);
117         let mut profile = Profile::new(module_name, zero, interval.into());
118 
119         // Past this point, we just need to think about modules as we pull out
120         // the disparate module information from components.
121         let mut modules: Vec<_> = modules
122             .into_iter()
123             .filter_map(|(name, module)| {
124                 let compiled = module.compiled_module();
125                 let text_range = {
126                     // Assumption: within text, the code for a given module is packed linearly and
127                     // is non-overlapping; if this is violated, it should be safe but might result
128                     // in incorrect profiling results.
129                     let start =
130                         compiled.finished_functions().next()?.1.as_ptr_range().start as usize;
131                     let end = compiled.finished_functions().last()?.1.as_ptr_range().end as usize;
132                     start..end
133                 };
134 
135                 module_symbols(name, compiled).map(|lib| {
136                     let libhandle = profile.add_lib(lib);
137                     ProfiledModule {
138                         module,
139                         fxprof_libhandle: libhandle,
140                         text_range,
141                     }
142                 })
143             })
144             .collect();
145 
146         modules.sort_unstable_by_key(|m| m.text_range.start);
147 
148         profile.set_reference_timestamp(std::time::SystemTime::now().into());
149         let process = profile.add_process(module_name, 0, Timestamp::from_nanos_since_reference(0));
150         let thread = profile.add_thread(process, 0, Timestamp::from_nanos_since_reference(0), true);
151         let start = Instant::now();
152         let marker = CallMarker::new(&mut profile);
153         Self {
154             profile,
155             modules,
156             process,
157             thread,
158             start,
159             marker,
160         }
161     }
162 
163     /// Create a new profiler for the provided component
164     ///
165     /// See [`GuestProfiler::new`] for additional information; this function
166     /// works identically except that it takes a component and sets up
167     /// instrumentation to track calls in each of its constituent modules.
168     #[cfg(feature = "component-model")]
169     pub fn new_component(
170         component_name: &str,
171         interval: Duration,
172         component: Component,
173         extra_modules: impl IntoIterator<Item = (String, Module)>,
174     ) -> Self {
175         let modules = component
176             .static_modules()
177             .map(|m| (m.name().unwrap_or("<unknown>").to_string(), m.clone()))
178             .chain(extra_modules);
179         Self::new(component_name, interval, modules)
180     }
181 
182     /// Add a sample to the profile. This function collects a backtrace from
183     /// any stack frames for allowed modules on the current stack. It should
184     /// typically be called from a callback registered using
185     /// [`Store::epoch_deadline_callback()`](crate::Store::epoch_deadline_callback).
186     ///
187     /// The `delta` parameter is the amount of CPU time that was used by this
188     /// guest since the previous sample. It is allowed to pass `Duration::ZERO`
189     /// here if recording CPU usage information is not needed.
190     pub fn sample(&mut self, store: impl AsContext, delta: Duration) {
191         let now = Timestamp::from_nanos_since_reference(
192             self.start.elapsed().as_nanos().try_into().unwrap(),
193         );
194         let backtrace = Backtrace::new(store.as_context().0);
195         let frames = lookup_frames(&self.modules, &backtrace);
196         let stack = self
197             .profile
198             .intern_stack_frames(self.thread, frames.into_iter());
199         self.profile
200             .add_sample(self.thread, now, stack, delta.into(), 1);
201     }
202 
203     /// Add a marker for transitions between guest and host to the profile.
204     /// This function should typically be called from a callback registered
205     /// using [`Store::call_hook()`](crate::Store::call_hook), and the `kind`
206     /// parameter should be the value of the same type passed into that hook.
207     pub fn call_hook(&mut self, store: impl AsContext, kind: CallHook) {
208         let now = Timestamp::from_nanos_since_reference(
209             self.start.elapsed().as_nanos().try_into().unwrap(),
210         );
211         match kind {
212             CallHook::CallingWasm | CallHook::ReturningFromWasm => {}
213             CallHook::CallingHost => {
214                 let backtrace = Backtrace::new(store.as_context().0);
215                 let frames = lookup_frames(&self.modules, &backtrace);
216                 let marker = self.profile.add_marker(
217                     self.thread,
218                     MarkerTiming::IntervalStart(now),
219                     self.marker,
220                 );
221                 let stack = self
222                     .profile
223                     .intern_stack_frames(self.thread, frames.into_iter());
224                 self.profile.set_marker_stack(self.thread, marker, stack);
225             }
226             CallHook::ReturningFromHost => {
227                 self.profile
228                     .add_marker(self.thread, MarkerTiming::IntervalEnd(now), self.marker);
229             }
230         }
231     }
232 
233     /// When the guest finishes running, call this function to write the
234     /// profile to the given `output`. The output is a JSON-formatted object in
235     /// the [Firefox "processed profile format"][fmt]. Files in this format may
236     /// be visualized at <https://profiler.firefox.com/>.
237     ///
238     /// [fmt]: https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
239     pub fn finish(mut self, output: impl std::io::Write) -> Result<()> {
240         let now = Timestamp::from_nanos_since_reference(
241             self.start.elapsed().as_nanos().try_into().unwrap(),
242         );
243         self.profile.set_thread_end_time(self.thread, now);
244         self.profile.set_process_end_time(self.process, now);
245 
246         serde_json::to_writer(output, &self.profile)?;
247         Ok(())
248     }
249 }
250 
251 fn module_symbols(name: String, compiled: &CompiledModule) -> Option<LibraryInfo> {
252     let symbols = Vec::from_iter(compiled.finished_functions().map(|(defined_idx, _)| {
253         let loc = compiled.func_loc(defined_idx);
254         let func_idx = compiled.module().func_index(defined_idx);
255         let mut name = String::new();
256         demangle_function_name_or_index(
257             &mut name,
258             compiled.func_name(func_idx),
259             defined_idx.as_u32() as usize,
260         )
261         .unwrap();
262         Symbol {
263             address: loc.start,
264             size: Some(loc.length),
265             name,
266         }
267     }));
268     if symbols.is_empty() {
269         return None;
270     }
271 
272     Some(LibraryInfo {
273         name,
274         debug_name: String::new(),
275         path: String::new(),
276         debug_path: String::new(),
277         debug_id: DebugId::nil(),
278         code_id: None,
279         arch: None,
280         symbol_table: Some(Arc::new(SymbolTable::new(symbols))),
281     })
282 }
283 
284 fn lookup_frames<'a>(
285     modules: &'a Modules,
286     backtrace: &'a Backtrace,
287 ) -> impl Iterator<Item = FrameInfo> + 'a {
288     backtrace
289         .frames()
290         // Samply needs to see the oldest frame first, but we list the newest
291         // first, so iterate in reverse.
292         .rev()
293         .filter_map(|frame| {
294             let idx = modules
295                 .binary_search_by(|probe| {
296                     if probe.text_range.contains(&frame.pc()) {
297                         Ordering::Equal
298                     } else {
299                         probe.text_range.start.cmp(&frame.pc())
300                     }
301                 })
302                 .ok()?;
303             let module = modules.get(idx)?;
304 
305             // We need to point to the modules full text (not just its functions) as
306             // the offset for the final phase; these can be different for component
307             // model modules.
308             let module_text_start = module.module.text().as_ptr_range().start as usize;
309             return Some(FrameInfo {
310                 frame: Frame::RelativeAddressFromReturnAddress(
311                     module.fxprof_libhandle,
312                     u32::try_from(frame.pc() - module_text_start).unwrap(),
313                 ),
314                 category_pair: CategoryHandle::OTHER.into(),
315                 flags: FrameFlags::empty(),
316             });
317         })
318 }
319 
320 #[derive(Debug, Clone, Copy)]
321 struct CallMarker {
322     name: StringHandle,
323 }
324 
325 impl CallMarker {
326     fn new(profile: &mut Profile) -> Self {
327         let name = profile.intern_string(Self::UNIQUE_MARKER_TYPE_NAME);
328         Self { name }
329     }
330 }
331 
332 impl StaticSchemaMarker for CallMarker {
333     const UNIQUE_MARKER_TYPE_NAME: &'static str = "hostcall";
334     const FIELDS: &'static [StaticSchemaMarkerField] = &[];
335     const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART
336         .union(MarkerLocations::MARKER_TABLE.union(MarkerLocations::TIMELINE_OVERVIEW));
337 
338     fn name(&self, _profile: &mut Profile) -> StringHandle {
339         self.name
340     }
341     fn category(&self, _profile: &mut Profile) -> CategoryHandle {
342         CategoryHandle::OTHER
343     }
344     fn string_field_value(&self, _field_index: u32) -> StringHandle {
345         unreachable!("no fields")
346     }
347     fn number_field_value(&self, _field_index: u32) -> f64 {
348         unreachable!("no fields")
349     }
350 }
351