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