1 use crate::prelude::*;
2 use crate::runtime::vm::Backtrace;
3 use crate::{instantiate::CompiledModule, AsContext, Module};
4 #[allow(unused_imports)]
5 use anyhow::bail;
6 use anyhow::Result;
7 use fxprof_processed_profile::{
8     debugid::DebugId, CategoryHandle, Frame, FrameFlags, FrameInfo, LibraryInfo, Profile,
9     ReferenceTimestamp, Symbol, SymbolTable, Timestamp,
10 };
11 use std::ops::Range;
12 use std::sync::Arc;
13 use std::time::{Duration, Instant};
14 use wasmtime_environ::demangle_function_name_or_index;
15 
16 // TODO: collect more data
17 // - Provide additional hooks for recording host-guest transitions, to be
18 //   invoked from a Store::call_hook
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 /// # Accuracy
37 ///
38 /// The data collection granularity is limited by the mechanism you use to
39 /// interrupt guest execution and collect a profiling sample.
40 ///
41 /// If you use epoch interruption, then samples will only be collected at
42 /// function entry points and loop headers. This introduces some bias to the
43 /// results. In addition, samples will only be taken at times when WebAssembly
44 /// functions are running, not during host-calls.
45 ///
46 /// It is technically possible to use fuel interruption instead. That
47 /// introduces worse bias since samples occur after a certain number of
48 /// WebAssembly instructions, which can take different amounts of time.
49 ///
50 /// You may instead be able to use platform-specific methods, such as
51 /// `setitimer(ITIMER_VIRTUAL, ...)` on POSIX-compliant systems, to sample on
52 /// a more accurate interval. The only current requirement is that the guest
53 /// you wish to profile must be on the same stack where you call `sample`,
54 /// and executing within the same thread. However, the `GuestProfiler::sample`
55 /// method is not currently async-signal-safe, so doing this correctly is not
56 /// easy.
57 ///
58 /// # Security
59 ///
60 /// Profiles produced using this profiler do not include any configuration
61 /// details from the host, such as virtual memory addresses, or from any
62 /// WebAssembly modules that you haven't specifically allowed. So for
63 /// example, these profiles should be safe to share with untrusted users
64 /// who have provided untrusted code that you are running in a multi-tenancy
65 /// environment.
66 ///
67 /// However, the profile does include byte offsets into the text section of
68 /// the compiled module, revealing some information about the size of the code
69 /// generated for each module. For user-provided modules, the user could get
70 /// the same information by compiling the module for themself using a similar
71 /// version of Wasmtime on the same target architecture, but for any module
72 /// where they don't already have the WebAssembly module binary available this
73 /// could theoretically lead to an undesirable information disclosure. So you
74 /// should only include user-provided modules in profiles.
75 #[derive(Debug)]
76 pub struct GuestProfiler {
77     profile: Profile,
78     modules: Vec<(Range<usize>, fxprof_processed_profile::LibraryHandle)>,
79     process: fxprof_processed_profile::ProcessHandle,
80     thread: fxprof_processed_profile::ThreadHandle,
81     start: Instant,
82 }
83 
84 impl GuestProfiler {
85     /// Begin profiling a new guest. When this function is called, the current
86     /// wall-clock time is recorded as the start time for the guest.
87     ///
88     /// The `module_name` parameter is recorded in the profile to help identify
89     /// where the profile came from.
90     ///
91     /// The `interval` parameter should match the rate at which you intend
92     /// to call `sample`. However, this is used as a hint and not required to
93     /// exactly match the real sample rate.
94     ///
95     /// Only modules which are present in the `modules` vector will appear in
96     /// stack traces in this profile. Any stack frames which were executing
97     /// host code or functions from other modules will be omitted. See the
98     /// "Security" section of the [`GuestProfiler`] documentation for guidance
99     /// on what modules should not be included in this list.
100     pub fn new(module_name: &str, interval: Duration, modules: Vec<(String, Module)>) -> Self {
101         let zero = ReferenceTimestamp::from_millis_since_unix_epoch(0.0);
102         let mut profile = Profile::new(module_name, zero, interval.into());
103 
104         let mut modules: Vec<_> = modules
105             .into_iter()
106             .filter_map(|(name, module)| {
107                 let compiled = module.compiled_module();
108                 let text = compiled.text().as_ptr_range();
109                 let address_range = text.start as usize..text.end as usize;
110                 module_symbols(name, compiled).map(|lib| (address_range, profile.add_lib(lib)))
111             })
112             .collect();
113 
114         modules.sort_unstable_by_key(|(range, _)| range.start);
115 
116         profile.set_reference_timestamp(std::time::SystemTime::now().into());
117         let process = profile.add_process(module_name, 0, Timestamp::from_nanos_since_reference(0));
118         let thread = profile.add_thread(process, 0, Timestamp::from_nanos_since_reference(0), true);
119         let start = Instant::now();
120         Self {
121             profile,
122             modules,
123             process,
124             thread,
125             start,
126         }
127     }
128 
129     /// Add a sample to the profile. This function collects a backtrace from
130     /// any stack frames for allowed modules on the current stack. It should
131     /// typically be called from a callback registered using
132     /// [`Store::epoch_deadline_callback()`](crate::Store::epoch_deadline_callback).
133     ///
134     /// The `delta` parameter is the amount of CPU time that was used by this
135     /// guest since the previous sample. It is allowed to pass `Duration::ZERO`
136     /// here if recording CPU usage information is not needed.
137     pub fn sample(&mut self, store: impl AsContext, delta: Duration) {
138         let now = Timestamp::from_nanos_since_reference(
139             self.start.elapsed().as_nanos().try_into().unwrap(),
140         );
141 
142         let backtrace = Backtrace::new(store.as_context().0.vmruntime_limits());
143         let frames = backtrace
144             .frames()
145             // Samply needs to see the oldest frame first, but we list the newest
146             // first, so iterate in reverse.
147             .rev()
148             .filter_map(|frame| {
149                 // Find the first module whose start address includes this PC.
150                 let module_idx = self
151                     .modules
152                     .partition_point(|(range, _)| range.start > frame.pc());
153                 if let Some((range, lib)) = self.modules.get(module_idx) {
154                     if range.contains(&frame.pc()) {
155                         return Some(FrameInfo {
156                             frame: Frame::RelativeAddressFromReturnAddress(
157                                 *lib,
158                                 u32::try_from(frame.pc() - range.start).unwrap(),
159                             ),
160                             category_pair: CategoryHandle::OTHER.into(),
161                             flags: FrameFlags::empty(),
162                         });
163                     }
164                 }
165                 None
166             });
167 
168         self.profile
169             .add_sample(self.thread, now, frames, delta.into(), 1);
170     }
171 
172     /// When the guest finishes running, call this function to write the
173     /// profile to the given `output`. The output is a JSON-formatted object in
174     /// the [Firefox "processed profile format"][fmt]. Files in this format may
175     /// be visualized at <https://profiler.firefox.com/>.
176     ///
177     /// [fmt]: https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
178     pub fn finish(mut self, output: impl std::io::Write) -> Result<()> {
179         let now = Timestamp::from_nanos_since_reference(
180             self.start.elapsed().as_nanos().try_into().unwrap(),
181         );
182         self.profile.set_thread_end_time(self.thread, now);
183         self.profile.set_process_end_time(self.process, now);
184 
185         serde_json::to_writer(output, &self.profile)?;
186         Ok(())
187     }
188 }
189 
190 fn module_symbols(name: String, compiled: &CompiledModule) -> Option<LibraryInfo> {
191     let symbols = Vec::from_iter(compiled.finished_functions().map(|(defined_idx, _)| {
192         let loc = compiled.func_loc(defined_idx);
193         let func_idx = compiled.module().func_index(defined_idx);
194         let mut name = String::new();
195         demangle_function_name_or_index(
196             &mut name,
197             compiled.func_name(func_idx),
198             defined_idx.as_u32() as usize,
199         )
200         .unwrap();
201         Symbol {
202             address: loc.start,
203             size: Some(loc.length),
204             name,
205         }
206     }));
207     if symbols.is_empty() {
208         return None;
209     }
210 
211     Some(LibraryInfo {
212         name,
213         debug_name: String::new(),
214         path: String::new(),
215         debug_path: String::new(),
216         debug_id: DebugId::nil(),
217         code_id: None,
218         arch: None,
219         symbol_table: Some(Arc::new(SymbolTable::new(symbols))),
220     })
221 }
222