Skip to main content

wasmtime_guest_pprof/
lib.rs

1//! Drive wasmtime's [`GuestProfiler`] for you and emit gzip'd pprof
2//! directly. Spares callers from the epoch-tick / take-and-restore
3//! plumbing and the JSON-intermediate step.
4//!
5//! # Quick start
6//!
7//! See `runners/wasmtime-runner/src/main.rs` in this repo for a worked
8//! example; the key types are [`ProfileSession`], [`ProfilerHost`], and
9//! [`TakeProfileSession`].
10
11#![forbid(unsafe_code)]
12#![warn(missing_docs)]
13
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::thread::{self, JoinHandle};
17use std::time::Duration;
18
19use anyhow::{Context as _, Result, anyhow};
20use firefox_to_pprof::{Builder, FirefoxProfile, FuncTableResolver, SampleWeighting};
21use wasmtime::{Engine, GuestProfiler, Module, Store, UpdateDeadline};
22
23/// Owned wrapper around wasmtime's [`GuestProfiler`].
24///
25/// Wrapped in [`Option`] internally so the deadline callback can `take()`
26/// the profiler out, call `sample()` with the store as `AsContext`, and
27/// put it back — wasmtime's signature requires holding both `&mut
28/// GuestProfiler` and a context simultaneously, which the borrow checker
29/// won't grant on a plain `&mut data().profiler` field.
30pub struct ProfileSession {
31    inner: Option<GuestProfiler>,
32}
33
34impl ProfileSession {
35    /// Construct a new session. See [`GuestProfiler::new`] for the
36    /// parameter contract.
37    pub fn new(
38        engine: &Engine,
39        name: &str,
40        interval: Duration,
41        modules: impl IntoIterator<Item = (String, Module)>,
42    ) -> Result<Self> {
43        let prof = GuestProfiler::new(engine, name, interval, modules)?;
44        Ok(Self { inner: Some(prof) })
45    }
46
47    /// Borrow the underlying profiler, panicking if it was taken and not
48    /// returned (a bug in the deadline callback).
49    pub fn get_mut(&mut self) -> Option<&mut GuestProfiler> {
50        self.inner.as_mut()
51    }
52
53    /// Hand the profiler over to the deadline callback. The callback must
54    /// put it back via [`put_back`](Self::put_back).
55    pub fn take(&mut self) -> Option<GuestProfiler> {
56        self.inner.take()
57    }
58
59    /// Restore the profiler after a `take()`.
60    pub fn put_back(&mut self, profiler: GuestProfiler) {
61        self.inner = Some(profiler);
62    }
63
64    /// Consume the session, serialise to Firefox JSON in-memory, and
65    /// convert to gzip'd pprof.
66    pub fn into_pprof(self) -> Result<Vec<u8>> {
67        let mut json = Vec::new();
68        self.into_json(&mut json)?;
69        json_to_pprof(&json)
70    }
71
72    /// Consume the session and write Firefox JSON.
73    pub fn into_json(self, w: &mut Vec<u8>) -> Result<()> {
74        let profiler = self
75            .inner
76            .ok_or_else(|| anyhow!("profiler was taken but never returned"))?;
77        profiler.finish(w)?;
78        Ok(())
79    }
80}
81
82/// Convert Firefox Profiler JSON bytes into gzip'd pprof.
83///
84/// Same conventions wasmtime's `GuestProfiler` uses — funcTable holds
85/// pre-resolved symbols, per-sample `timeDeltas` weigh each sample.
86pub fn json_to_pprof(json: &[u8]) -> Result<Vec<u8>> {
87    let profile: FirefoxProfile =
88        serde_json::from_slice(json).context("parsing Firefox Profiler JSON")?;
89    Builder::new(
90        &profile,
91        FuncTableResolver,
92        SampleWeighting::PerSampleTimeDeltas {
93            default_interval_ns: (profile.meta.interval.max(1.0) * 1_000_000.0) as i64,
94        },
95    )
96    .encode()
97}
98
99/// Implemented by your `Store<T>` data so the crate can find the
100/// [`ProfileSession`] in the deadline callback.
101pub trait ProfilerHost: Sized {
102    /// Return a mutable reference to the embedded profile session.
103    fn profiler(&mut self) -> &mut ProfileSession;
104}
105
106/// Methods auto-applied to any type that implements [`ProfilerHost`].
107pub trait ProfilerHostExt: ProfilerHost {
108    /// Install the deadline callback on `store`. After this, every epoch
109    /// tick produces one sample.
110    fn install(store: &mut Store<Self>)
111    where
112        Self: 'static,
113    {
114        store.set_epoch_deadline(1);
115        store.epoch_deadline_callback(|mut ctx| {
116            if let Some(mut prof) = ctx.data_mut().profiler().take() {
117                prof.sample(&ctx, Duration::ZERO);
118                ctx.data_mut().profiler().put_back(prof);
119            }
120            Ok(UpdateDeadline::Continue(1))
121        });
122    }
123
124    /// Spin up a background thread that calls `engine.increment_epoch()`
125    /// every `interval`, driving the deadline callback. Returns an RAII
126    /// guard; drop it (or let it go out of scope) to stop sampling.
127    fn start_ticker(engine: &Engine, interval: Duration) -> EpochTicker {
128        EpochTicker::start(engine, interval)
129    }
130
131    /// Consume the store, extract the profile session, and emit gzip'd
132    /// pprof bytes.
133    fn finish_pprof(store: Store<Self>) -> Result<Vec<u8>>
134    where
135        Self: TakeProfileSession,
136    {
137        let session = Self::take_session(store);
138        session.into_pprof()
139    }
140}
141
142impl<T: ProfilerHost> ProfilerHostExt for T {}
143
144/// Companion trait for stores whose data owns the session — needed to
145/// extract the session out of a consumed [`Store`].
146pub trait TakeProfileSession: ProfilerHost {
147    /// Consume `store`, returning the embedded session.
148    fn take_session(store: Store<Self>) -> ProfileSession;
149}
150
151/// Background thread that bumps wasmtime's epoch counter on a fixed
152/// cadence so the deadline callback fires. RAII — drop to stop.
153pub struct EpochTicker {
154    stop: Arc<AtomicBool>,
155    handle: Option<JoinHandle<()>>,
156}
157
158impl EpochTicker {
159    /// Start a ticker on `engine` with the given `interval`. Most callers
160    /// reach this via [`ProfilerHostExt::start_ticker`].
161    pub fn start(engine: &Engine, interval: Duration) -> Self {
162        let stop = Arc::new(AtomicBool::new(false));
163        let stop_flag = stop.clone();
164        let engine = engine.clone();
165        let handle = thread::spawn(move || {
166            while !stop_flag.load(Ordering::Relaxed) {
167                thread::sleep(interval);
168                engine.increment_epoch();
169            }
170        });
171        Self {
172            stop,
173            handle: Some(handle),
174        }
175    }
176}
177
178impl Drop for EpochTicker {
179    fn drop(&mut self) {
180        self.stop.store(true, Ordering::Relaxed);
181        if let Some(h) = self.handle.take() {
182            let _ = h.join();
183        }
184    }
185}