Skip to main content

winliner/
profile.rs

1//! Extracting profiles from instrumented Wasm programs and merging profiles
2//! together.
3
4use std::collections::BTreeMap;
5
6use anyhow::{anyhow, ensure, Context, Result};
7
8/// Observed behavior about one or more Wasm executions.
9///
10/// A `Profile` records observed `call_indirect` behavior about one or more Wasm
11/// executions:
12///
13/// * How many times was each `call_indirect` executed?
14/// * How many times was `table[x]` called from each call site?
15/// * Etc...
16///
17/// ## Constructing a `Profile`
18///
19/// There are two primary ways to get a `Profile`, one for each instrumentation
20/// strategy:
21///
22/// 1. If you instrumented your Wasm using the
23/// [`InstrumentationStrategy::ThreeGlobals`][crate::InstrumentationStrategy::ThreeGlobals]
24/// strategy, you can use the [`Profile::from_three_globals`] constructor.
25///
26/// 2. If you instrumented your Wasm using the
27/// [`InstrumentationStrategy::HostCalls`][crate::InstrumentationStrategy::HostCalls]
28/// strategy, you can implement the `winliner.add_indirect_call` host import
29/// using a [`ProfileBuilder`][crate::ProfileBuilder] and then call
30/// [`ProfileBuilder::build`][crate::ProfileBuilder::build] to extract the
31/// finished profile.
32///
33/// ## Merging `Profile`s
34///
35/// It can be difficult to get representative profiling data from a single Wasm
36/// execution. Luckily, a single `Profile` can represent many different
37/// executions! For each profiling run, record a new `Profile` and then call
38/// [`Profile::merge`] to combine them into a single, aggregate `Profile`.
39///
40/// ## Serializing and Deserializing `Profile`s
41///
42/// When the `serde` cargo feature is enabled, `Profile` implements
43/// `serde::Serialize` and `serde::Deserialize`:
44///
45/// ```
46/// # fn foo() -> anyhow::Result<()> {
47/// #![cfg(feature = "serde")]
48///
49/// use winliner::Profile;
50///
51/// // Read a profile in from disk.
52/// let file = std::fs::File::open("path/to/my/profile.json")?;
53/// let my_profile: Profile = serde_json::from_reader(file)?;
54///
55/// // Write a profile out to disk.
56/// let file = std::fs::File::create("path/to/new/profile.json")?;
57/// serde_json::to_writer(file, &my_profile)?;
58/// # Ok(()) }
59/// ```
60#[derive(Clone, Debug, Default)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct Profile {
63    // Per-call site profiling information.
64    //
65    // Note that a lack of profile data for a particular call site implies that
66    // the associated `call_indirect` was never executed (or at least never
67    // observed to have been executed: our profiling is sometimes imprecise).
68    pub(crate) call_sites: BTreeMap<u32, CallSiteProfile>,
69}
70
71#[derive(Clone, Debug, Default)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub(crate) struct CallSiteProfile {
74    // The total count of indirect calls for this call site.
75    pub(crate) total_call_count: u64,
76    // The observed callees and their associated counts. Note that these counts
77    // don't necessarily add up to `total_call_count` since we can be missing
78    // information due to imprecise instrumentation strategies.
79    pub(crate) callee_to_count: BTreeMap<u32, u64>,
80}
81
82impl Profile {
83    /// Extract a profile from a Wasm program that was instrumented with the
84    /// "three-globals" strategy.
85    ///
86    /// To avoid a public dependency on any particular version of Wasmtime (or
87    /// any other Wasm runtime for that matter) this method takes a callback
88    /// function to read a global (by name) from a Wasm instance instead of
89    /// taking the Wasm instance as a parameter directly. It is up to callers to
90    /// implement this callback function for their Wasm runtime. The callback
91    /// function must be able to read `i32`- and `i64`-typed Wasm globals,
92    /// zero-extending `i32` values as necessary.
93    ///
94    /// # Example
95    ///
96    /// ```
97    /// # fn foo() -> wasmtime::Result<()> {
98    /// use wasmtime::{Instance, Module, Store, Val};
99    /// use winliner::Profile;
100    ///
101    /// // Instantiate your instrumented Wasm module.
102    /// let mut store = Store::<()>::default();
103    /// let module = Module::from_file(store.engine(), "path/to/instrumented.wasm")?;
104    /// let instance = Instance::new(&mut store, &module, &[])?;
105    ///
106    /// // Run the Wasm instance, call its exports, etc... to gather PGO data.
107    /// # let run = |_| -> wasmtime::Result<()> { Ok(()) };
108    /// run(instance)?;
109    ///
110    /// // Extract the profile from the instance.
111    /// let profile = Profile::from_three_globals(|name| {
112    ///     match instance.get_global(&mut store, name)?.get(&mut store) {
113    ///         Val::I32(x) => Some(x as u32 as u64),
114    ///         Val::I64(x) => Some(x as u64),
115    ///         _ => None,
116    ///     }
117    /// })?;
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub fn from_three_globals(mut read_global: impl FnMut(&str) -> Option<u64>) -> Result<Self> {
122        let mut profile = Profile::default();
123
124        for call_site_index in 0.. {
125            let total_call_count =
126                match read_global(&format!("__winliner_call_site_{call_site_index}_total")) {
127                    None => break,
128                    Some(x) => x,
129                };
130
131            let last_callee = read_global(&format!(
132                "__winliner_call_site_{call_site_index}_last_callee"
133            ))
134            .ok_or_else(|| {
135                anyhow!(
136                    "Failed to read `__winliner_call_site_{call_site_index}_last_callee` global"
137                )
138            })?;
139            let last_callee = u32::try_from(last_callee).context("callee is out of bounds")?;
140
141            let last_callee_count = read_global(&format!(
142                "__winliner_call_site_{call_site_index}_last_callee_count"
143            ))
144            .ok_or_else(|| {
145                anyhow!(
146                    "Failed to read `__winliner_call_site_{call_site_index}_last_callee` global"
147                )
148            })?;
149
150            ensure!(
151                total_call_count >= last_callee_count,
152                "Bogus profiling data: call site's total count is less than the last callee's call \
153                 count",
154            );
155
156            let mut callee_to_count = BTreeMap::new();
157            callee_to_count.insert(last_callee, last_callee_count);
158
159            profile.call_sites.insert(
160                call_site_index,
161                CallSiteProfile {
162                    total_call_count,
163                    callee_to_count,
164                },
165            );
166        }
167
168        Ok(profile)
169    }
170
171    /// Merge two profiles together.
172    ///
173    /// The `other` profile is merged into `self`.
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// # fn foo() -> anyhow::Result<()> {
179    /// use wasmtime::{Engine, Module};
180    /// use winliner::Profile;
181    ///
182    /// // Load the instrumented Wasm module.
183    /// let engine = Engine::default();
184    /// let module = Module::from_file(&engine, "path/to/instrumented.wasm")?;
185    ///
186    /// // Record a couple of PGO profiles.
187    /// # let record_one_profile = |_| -> anyhow::Result<Profile> { unimplemented!() };
188    /// let mut profile1 = record_one_profile(&module)?;
189    /// let profile2 = record_one_profile(&module)?;
190    ///
191    /// // Finally, combine the two profiles into a single profile.
192    /// profile1.merge(&profile2);
193    /// # Ok(()) }
194    /// ```
195    pub fn merge(&mut self, other: &Profile) {
196        for (call_site_index, other) in other.call_sites.iter() {
197            let call_site = self.call_sites.entry(*call_site_index).or_default();
198            call_site.total_call_count += other.total_call_count;
199            for (callee, count) in other.callee_to_count.iter() {
200                *call_site.callee_to_count.entry(*callee).or_default() += count;
201            }
202        }
203    }
204}
205
206/// A builder for constructing [`Profile`][crate::Profile]s.
207///
208/// Primarily for use in conjunction with
209/// [`InstrumentationStrategy::HostCalls`][crate::InstrumentationStrategy::HostCalls]
210/// and implementing the `winliner.add_indirect_call` import function for the
211/// instrumented Wasm.
212///
213/// # Example
214///
215/// ```
216/// use winliner::ProfileBuilder;
217///
218/// // Create a new builder.
219/// let mut builder = ProfileBuilder::new();
220///
221/// // Record some observed calls.
222/// let callee = 42;
223/// let call_site = 36;
224/// builder.add_indirect_call(callee, call_site);
225///
226/// // Construct the finished profile from the builder.
227/// let profile = builder.build();
228/// ```
229#[derive(Clone, Default)]
230pub struct ProfileBuilder {
231    profile: Profile,
232}
233
234impl ProfileBuilder {
235    /// Create a new, empty builder.
236    pub fn new() -> Self {
237        Default::default()
238    }
239
240    /// Record the observed target of an indirect call at the given call site.
241    pub fn add_indirect_call(&mut self, callee: u32, call_site: u32) {
242        let call_site = self.profile.call_sites.entry(call_site).or_default();
243        call_site.total_call_count += 1;
244        *call_site.callee_to_count.entry(callee).or_default() += 1;
245    }
246
247    /// Construct the finished profile from this builder.
248    pub fn build(self) -> Profile {
249        self.profile
250    }
251}