1 //! Basic support for emitting a `*.data` file which contains samples of pulley
2 //! bytecode.
3 //!
4 //! Pulley is Wasmtime's interpreter and native profilers are not good at
5 //! profiling bytecode interpreters because they show hot bytecode instructions
6 //! but we're instead often interested in the shape of the bytecode itself
7 //! around the hot instruction, for example to identify new macro-instructions
8 //! to add to Pulley. This module serves as a means of collecting data from
9 //! Pulley being executed in-process and serializing it to a file.
10 //!
11 //! The file collected here is populated by a sampling thread in-process. This
12 //! sampling thread only collects the current program counter of any interpreters
13 //! in the process. This does not collect stack traces at all. That means that
14 //! this profiler is only suitable for looking at "self time" and is not
15 //! suitable for getting a broader picture of what's going on (e.g. why
16 //! something was called in the first place).
17 //!
18 //! The general design of this profiler is:
19 //!
20 //! * Support for this all requires a `pulley-profile` feature at compile-time
21 //!   as it's generally a perf hit to the interpreter loop.
22 //! * Each Pulley interpreter updates an `AtomicUsize` before all instructions
23 //!   with the current PC that it's executing.
24 //! * This module spawns a "sampling thread" which will, at some frequency,
25 //!   collect all the PCs of all interpreters in the process.
26 //! * Once enough samples have been collected they're flushed out to a data file
27 //!   on a second thread, the "recording thread".
28 //!
29 //! The hope is that the sampling thread stays as steady as possible in its
30 //! sampling rate while not hitting OOM conditions in the process or anything
31 //! like that. The `*.data` file that's emitted is intended to be processed by
32 //! example code in the `pulley-interpreter` crate or `pulley/examples/*.rs` in
33 //! the Wasmtime repository.
34 
35 use crate::ToWasmtimeResult as _;
36 use crate::prelude::*;
37 use crate::profiling_agent::ProfilingAgent;
38 #[cfg(feature = "runtime")]
39 use crate::vm::Interpreter;
40 use pulley_interpreter::profile::{ExecutingPc, Recorder, Samples};
41 use std::mem;
42 use std::sync::mpsc;
43 use std::sync::{Arc, Condvar, Mutex};
44 use std::thread::{self, JoinHandle};
45 use std::time::{Duration, Instant};
46 
47 /// Implementation of `ProfilingAgent` from the Wasmtime crate.
48 struct PulleyAgent {
49     state: Arc<State>,
50 
51     /// Handle to the thread performing periodic sampling. This is joined on
52     /// `Drop` of this structure so it's not a daemon thread permanently.
53     sampling_thread: Option<JoinHandle<()>>,
54 
55     /// Same as the sampling thread above, but for recording data to the
56     /// filesystem.
57     recording_thread: Option<JoinHandle<()>>,
58 }
59 
60 struct State {
61     /// Protected state about the recorder, or the file being created. This is
62     /// accessed both from the "recording thread" as well as `Engine` threads to
63     /// register new pulley bytecode.
64     recorder: Mutex<Recorder>,
65 
66     /// Protected state about sampling interpreters. This is accessed both from
67     /// the "sampling thread" primarily but is additionally accessed from
68     /// `Engine` threads to register new interpreters coming online.
69     sampling: Mutex<SamplingState>,
70 
71     /// Condition variable which is signaled when sampling should cease and
72     /// exit. This is coupled with `Drop for PulleyAgent`.
73     sampling_done: Condvar,
74 
75     /// The frequency at which samples are collected. Defaults to 1000 but can
76     /// be configured with the `PULLEY_SAMPLING_FREQ` environment variable.
77     sampling_freq: u32,
78 
79     /// Number of samples to buffer before flushing them to a file. Defaults to
80     /// 20000 but can be configured with the `PULLEY_SAMPLING_FLUSH_AMT`
81     /// environment variable.
82     sampling_flush_amt: u32,
83 }
84 
85 /// State protected by a mutex in `State` above related to sampling.
86 #[derive(Default)]
87 struct SamplingState {
88     /// All interpreters known to be executing. This is a list of
89     /// pointers-to-the-current-PC which is updated whenever the interpreter
90     /// executes an instruction.
91     interpreters: Vec<ExecutingPc>,
92 
93     /// Current list of samples that have been collected.
94     samples: Samples,
95 }
96 
new() -> Result<Box<dyn ProfilingAgent>>97 pub fn new() -> Result<Box<dyn ProfilingAgent>> {
98     let pid = std::process::id();
99     let filename = format!("./pulley-{pid}.data");
100     let mut agent = PulleyAgent {
101         state: Arc::new(State {
102             recorder: Mutex::new(Recorder::new(&filename).to_wasmtime_result()?),
103             sampling: Default::default(),
104             sampling_done: Condvar::new(),
105             sampling_freq: std::env::var("PULLEY_SAMPLING_FREQ")
106                 .ok()
107                 .and_then(|s| s.parse::<u32>().ok())
108                 .unwrap_or(1_000),
109             sampling_flush_amt: std::env::var("PULLEY_SAMPLING_FLUSH_AMT")
110                 .ok()
111                 .and_then(|s| s.parse::<u32>().ok())
112                 .unwrap_or(20_000),
113         }),
114         sampling_thread: None,
115         recording_thread: None,
116     };
117 
118     let (tx, rx) = mpsc::channel();
119     let state = agent.state.clone();
120     agent.sampling_thread = Some(thread::spawn(move || sampling_thread(&state, tx)));
121     let state = agent.state.clone();
122     agent.recording_thread = Some(thread::spawn(move || recording_thread(&state, rx)));
123 
124     Ok(Box::new(agent))
125 }
126 
127 impl ProfilingAgent for PulleyAgent {
128     /// New functions are registered with `Recorder` to record the exact
129     /// bytecode so disassembly is available during profile analysis.
130     ///
131     /// Note that this also provides the native address that code is loaded at
132     /// so samples know what code it's within.
register_function(&self, name: &str, code: &[u8])133     fn register_function(&self, name: &str, code: &[u8]) {
134         self.state
135             .recorder
136             .lock()
137             .unwrap()
138             .add_function(name, code)
139             .expect("failed to register pulley function");
140     }
141 
142     /// Registers a new interpreter coming online. Interpreters, with
143     /// `pulley-profile` enabled, store a shadow program counter updated on each
144     /// instruction which we can read from a different thread.
145     #[cfg(feature = "runtime")]
register_interpreter(&self, interpreter: &Interpreter)146     fn register_interpreter(&self, interpreter: &Interpreter) {
147         let pc = interpreter.pulley().executing_pc();
148         self.state
149             .sampling
150             .lock()
151             .unwrap()
152             .interpreters
153             .push(pc.clone());
154     }
155 }
156 
157 /// Execution of the thread responsible for sampling interpreters.
158 ///
159 /// This thread has a few tasks:
160 ///
161 /// * Needs to sample, at `state.sampling_freq`, the state of all known
162 ///   interpreters. Ideally this sampling is as steady as possible.
163 /// * Needs to clean up interpreters which have been destroyed as there's
164 ///   otherwise no hook for doing so.
165 /// * Needs to send batches of samples to the recording thread to get written to
166 ///   the filesystem.
sampling_thread(state: &State, to_record: mpsc::Sender<Samples>)167 fn sampling_thread(state: &State, to_record: mpsc::Sender<Samples>) {
168     // Calculate the `Duration` between each sample which will be in
169     // nanoseconds. This duration is then used to create an `Instant` in time
170     // where we'll be collecting the next sample.
171     let between_ticks = Duration::new(0, 1_000_000_000 / state.sampling_freq);
172     let start = Instant::now();
173     let mut next_sample = start + between_ticks;
174 
175     // Helper closure to send off a batch of samples to the recording thread.
176     // Note that recording is done off-thread to ensure that the filesystem I/O
177     // interferes as little as possible with the sampling rate here.
178     let record = |sampling: &mut SamplingState| {
179         if sampling.samples.num_samples() == 0 {
180             return;
181         }
182         let samples = mem::take(&mut sampling.samples);
183         to_record.send(samples).unwrap();
184     };
185 
186     let mut sampling = state.sampling.lock().unwrap();
187 
188     loop {
189         // Calculate the duration, from this current moment in time, to when the
190         // next sample is supposed to be taken. If the next sampling time is in
191         // the past then this won't sleep but will still check the condvar.
192         let dur = next_sample
193             .checked_duration_since(Instant::now())
194             .unwrap_or(Duration::new(0, 0));
195 
196         // Wait on `state.sampling_done`, but with the timeout we've calculated.
197         // If this times out that means that the next sample can proceed.
198         // Otherwise if this did not time out then it means that sampling should
199         // cease as the profiler is being destroyed.
200         let (guard, result) = state.sampling_done.wait_timeout(sampling, dur).unwrap();
201         sampling = guard;
202         if !result.timed_out() {
203             break;
204         }
205 
206         // Now that we've decided to take a sample increment the next sample
207         // time by our interval. Once we're done sampling below we'll then sleep
208         // again up to this time.
209         next_sample += between_ticks;
210 
211         // Sample the state of all interpreters known. This first starts by
212         // discarding any interpreters that are offline. Samples without a PC
213         // are additionally discarded as it means the interpreter is inactive.
214         //
215         // Once enough samples have been collected they're flushed to the
216         // recording thread.
217         let SamplingState {
218             interpreters,
219             samples,
220         } = &mut *sampling;
221         interpreters.retain(|a| !a.is_done());
222         for interpreter in interpreters.iter() {
223             if let Some(pc) = interpreter.get() {
224                 samples.append(pc);
225             }
226         }
227         if samples.num_samples() > state.sampling_flush_amt {
228             record(&mut sampling);
229         }
230     }
231 
232     // Send any final samples to the recording thread after the loop has exited.
233     record(&mut sampling);
234 }
235 
236 /// Helper thread responsible for writing samples to the filesystem.
237 ///
238 /// This receives samples over `to_record` and then performs the filesystem I/O
239 /// necessary to write them out. This thread completes once `to_record` is
240 /// closed, or when the sampling thread completes. At that time all data in the
241 /// recorder is flushed out as well.
recording_thread(state: &State, to_record: mpsc::Receiver<Samples>)242 fn recording_thread(state: &State, to_record: mpsc::Receiver<Samples>) {
243     for mut samples in to_record {
244         state
245             .recorder
246             .lock()
247             .unwrap()
248             .add_samples(&mut samples)
249             .expect("failed to write samples");
250     }
251 
252     state.recorder.lock().unwrap().flush().unwrap();
253 }
254 
255 impl Drop for PulleyAgent {
drop(&mut self)256     fn drop(&mut self) {
257         // First notify the sampling thread that it's time to shut down and
258         // wait for it to exit.
259         self.state.sampling_done.notify_one();
260         self.sampling_thread.take().unwrap().join().unwrap();
261 
262         // Wait on the recording thread as well which should terminate once
263         // `sampling_thread` has terminated as well.
264         self.recording_thread.take().unwrap().join().unwrap();
265     }
266 }
267