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}