pulley_interpreter/
profile.rs

1//! Low-level support for profiling pulley.
2//!
3//! This is used in conjunction with the `profiler-html.rs` example with Pulley
4//! and the `pulley.rs` ProfilingAgent in Wasmtime.
5
6use anyhow::{anyhow, bail, Context, Result};
7use std::fs::{File, OpenOptions};
8use std::io::{BufWriter, Write};
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
10use std::sync::Arc;
11use std::vec::Vec;
12
13// Header markers for sections in the binary `*.data` file.
14
15/// Section of the `*.data` file which looks like:
16///
17/// ```text
18/// * byte: ID_FUNCTION
19/// * addr: 8-byte little-endian address that this body was located at
20/// * name_len: 4-byte little-endian byte length of `name`
21/// * name: contents of the name of the function
22/// * body_len: 4-byte little-endian byte length of `body`
23/// * body: contents of the body of the function
24/// ```
25const ID_FUNCTION: u8 = 1;
26
27/// Section of the `*.data` file which looks like:
28///
29/// ```text
30/// * byte: ID_SAMPLES
31/// * sample_len: 4-byte little-endian element count of `samples`
32/// * samples: sequence of 8-byte little endian addresses
33/// ```
34const ID_SAMPLES: u8 = 2;
35
36/// Representation of a currently executing program counter of an interpreter.
37///
38/// Stores an `Arc` internally that is safe to clone/read from other threads.
39#[derive(Default, Clone)]
40pub struct ExecutingPc(Arc<ExecutingPcState>);
41
42#[derive(Default)]
43struct ExecutingPcState {
44    current_pc: AtomicUsize,
45    done: AtomicBool,
46}
47
48impl ExecutingPc {
49    pub(crate) fn as_ref(&self) -> ExecutingPcRef<'_> {
50        ExecutingPcRef(&self.0.current_pc)
51    }
52
53    /// Loads the currently executing program counter, if the interpreter is
54    /// running.
55    pub fn get(&self) -> Option<usize> {
56        match self.0.current_pc.load(Relaxed) {
57            0 => None,
58            n => Some(n),
59        }
60    }
61
62    /// Returns whether the interpreter has been destroyed and will no longer
63    /// execute any code.
64    pub fn is_done(&self) -> bool {
65        self.0.done.load(Relaxed)
66    }
67
68    pub(crate) fn set_done(&self) {
69        self.0.done.store(true, Relaxed)
70    }
71}
72
73#[derive(Copy, Clone)]
74#[repr(transparent)]
75pub(crate) struct ExecutingPcRef<'a>(&'a AtomicUsize);
76
77impl ExecutingPcRef<'_> {
78    pub(crate) fn record(&self, pc: usize) {
79        self.0.store(pc, Relaxed);
80    }
81}
82
83/// Utility to record profiling information to a file.
84pub struct Recorder {
85    /// The buffered writer used to write profiling data. Note that this is
86    /// buffered to amortize the cost of writing out information to the
87    /// filesystem to help avoid profiling overhead.
88    file: BufWriter<File>,
89}
90
91impl Recorder {
92    /// Creates a new recorder which will write to the specified filename.
93    pub fn new(filename: &str) -> Result<Recorder> {
94        Ok(Recorder {
95            file: BufWriter::new(
96                OpenOptions::new()
97                    .write(true)
98                    .create_new(true)
99                    .open(filename)
100                    .with_context(|| format!("failed to open `{filename}` for writing"))?,
101            ),
102        })
103    }
104
105    /// Adds a new function that may be sampled in the future.
106    ///
107    /// This must be given `code` where it resides and will be executed in the
108    /// host address space.
109    pub fn add_function(&mut self, name: &str, code: &[u8]) -> Result<()> {
110        self.file.write_all(&[ID_FUNCTION])?;
111        self.file
112            .write_all(&u64::try_from(code.as_ptr() as usize)?.to_le_bytes())?;
113        self.file
114            .write_all(&u32::try_from(name.len())?.to_le_bytes())?;
115        self.file.write_all(name.as_bytes())?;
116        self.file
117            .write_all(&u32::try_from(code.len())?.to_le_bytes())?;
118        self.file.write_all(code)?;
119        Ok(())
120    }
121
122    /// Adds a new set of samples to this recorded.
123    pub fn add_samples(&mut self, samples: &mut Samples) -> Result<()> {
124        self.file.write_all(&[ID_SAMPLES])?;
125
126        samples.finalize();
127        self.file.write_all(&samples.data)?;
128        samples.reset();
129        Ok(())
130    }
131
132    /// Flushes out all pending data to the filesystem.
133    pub fn flush(&mut self) -> Result<()> {
134        self.file.flush()?;
135        Ok(())
136    }
137}
138
139/// A set of samples of program counters that have been collected over time.
140pub struct Samples {
141    data: Vec<u8>,
142    samples: u32,
143}
144
145impl Samples {
146    /// Adds a new program counter to this sample.
147    pub fn append(&mut self, sample: usize) {
148        self.data.extend_from_slice(&(sample as u64).to_le_bytes());
149        self.samples += 1;
150    }
151
152    /// Returns the number of samples that have been collected.
153    pub fn num_samples(&self) -> u32 {
154        self.samples
155    }
156
157    fn finalize(&mut self) {
158        self.data[..4].copy_from_slice(&self.samples.to_le_bytes());
159    }
160
161    fn reset(&mut self) {
162        self.data.truncate(0);
163        self.data.extend_from_slice(&[0; 4]);
164        self.samples = 0;
165    }
166}
167
168impl Default for Samples {
169    fn default() -> Samples {
170        let mut samples = Samples {
171            data: Vec::new(),
172            samples: 0,
173        };
174        samples.reset();
175        samples
176    }
177}
178
179/// Sections that can be parsed from a `*.data` file.
180///
181/// This is the reverse of [`Recorder`] above.
182pub enum Event<'a> {
183    /// A named function was loaded at the specified address with the specified
184    /// contents.
185    Function(u64, &'a str, &'a [u8]),
186    /// A set of samples were taken.
187    Samples(&'a [SamplePc]),
188}
189
190/// A small wrapper around `u64` to reduce its alignment to 1.
191#[repr(packed)]
192pub struct SamplePc(pub u64);
193
194/// Decodes a `*.data` file presented in its entirety as `bytes` into a sequence
195/// of `Event`s.
196pub fn decode(mut bytes: &[u8]) -> impl Iterator<Item = Result<Event<'_>>> + use<'_> {
197    std::iter::from_fn(move || {
198        if bytes.is_empty() {
199            None
200        } else {
201            Some(decode_one(&mut bytes))
202        }
203    })
204}
205
206fn decode_one<'a>(bytes: &mut &'a [u8]) -> Result<Event<'a>> {
207    match bytes.split_first().unwrap() {
208        (&ID_FUNCTION, rest) => {
209            let (addr, rest) = rest
210                .split_first_chunk()
211                .ok_or_else(|| anyhow!("invalid addr"))?;
212            let addr = u64::from_le_bytes(*addr);
213
214            let (name_len, rest) = rest
215                .split_first_chunk()
216                .ok_or_else(|| anyhow!("invalid name byte len"))?;
217            let name_len = u32::from_le_bytes(*name_len);
218            let (name, rest) = rest
219                .split_at_checked(name_len as usize)
220                .ok_or_else(|| anyhow!("invalid name contents"))?;
221            let name = std::str::from_utf8(name)?;
222
223            let (body_len, rest) = rest
224                .split_first_chunk()
225                .ok_or_else(|| anyhow!("invalid body byte len"))?;
226            let body_len = u32::from_le_bytes(*body_len);
227            let (body, rest) = rest
228                .split_at_checked(body_len as usize)
229                .ok_or_else(|| anyhow!("invalid body contents"))?;
230
231            *bytes = rest;
232            Ok(Event::Function(addr, name, body))
233        }
234
235        (&ID_SAMPLES, rest) => {
236            let (samples, rest) = rest
237                .split_first_chunk()
238                .ok_or_else(|| anyhow!("invalid sample count"))?;
239            let samples = u32::from_le_bytes(*samples);
240            let (samples, rest) = rest
241                .split_at_checked(samples as usize * 8)
242                .ok_or_else(|| anyhow!("invalid sample data"))?;
243            *bytes = rest;
244
245            let (before, mid, after) = unsafe { samples.align_to::<SamplePc>() };
246            if !before.is_empty() || !after.is_empty() {
247                bail!("invalid sample data contents");
248            }
249            Ok(Event::Samples(mid))
250        }
251
252        _ => bail!("unknown ID in profile"),
253    }
254}