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