winliner/counters.rs
1//! Reading the correct-versus-incorrect counters from an optimized instance.
2
3use anyhow::{anyhow, ensure, Result};
4
5/// Feedback counters for how often speculative inlining guesses were correct or
6/// incorrect.
7///
8/// After speculatively inlining a callee at a `call_indirect` site, you may
9/// want to know whether you speculated correctly in practice or not. Is the
10/// training set reflective of your real world workloads? Do your past recorded
11/// profiles match current behavior?
12///
13/// This type counts how often each `call_indirect`'s target was correctly or
14/// incorrectly guessed.
15///
16/// Construction of a `FeedbackCounters` relies on the
17/// [`emit_feedback_counters`][crate::Optimizer::emit_feedback_counters] option
18/// being enabled when you generated the optimized Wasm. If they were not
19/// enabled, then you'll get an empty set of counters.
20///
21/// ## Serializing and Deserializing `FeedbackCounters`
22///
23/// When the `serde` cargo feature is enabled, `FeedbackCounters` implements
24/// `serde::Serialize` and `serde::Deserialize`:
25///
26/// ```
27/// # fn foo() -> anyhow::Result<()> {
28/// #![cfg(feature = "serde")]
29///
30/// use winliner::FeedbackCounters;
31///
32/// // Read counters in from disk.
33/// let file = std::fs::File::open("path/to/my/counters.json")?;
34/// let my_counters: FeedbackCounters = serde_json::from_reader(file)?;
35///
36/// // Write counters out to disk.
37/// let file = std::fs::File::create("path/to/new/counters.json")?;
38/// serde_json::to_writer(file, &my_counters)?;
39/// # Ok(()) }
40/// ```
41#[derive(Default)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct FeedbackCounters {
44 counters: Vec<FeedbackCounter>,
45 total_correct: u64,
46 total_incorrect: u64,
47}
48
49/// How often a single speculative inlining call site was guessed correctly or
50/// incorrectly.
51///
52/// See [`FeedbackCounters`][crate::FeedbackCounters] for more details.
53#[derive(Clone, Copy)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct FeedbackCounter {
56 correct: u64,
57 incorrect: u64,
58}
59
60impl FeedbackCounters {
61 /// Extract counters from an optimized Wasm program.
62 ///
63 /// The program must have been optimized by Winliner with the
64 /// [`emit_feedback_counters`][crate::Optimizer::emit_feedback_counters]
65 /// option enabled.
66 ///
67 /// To avoid a public dependency on any particular version of Wasmtime (or
68 /// any other Wasm runtime for that matter) this method takes a callback
69 /// function to read a global (by name) from a Wasm instance instead of
70 /// taking the Wasm instance as a parameter directly. It is up to callers to
71 /// implement this callback function for their Wasm runtime. The callback
72 /// function must be able to read `i64`-typed Wasm globals.
73 ///
74 /// # Example
75 ///
76 /// ```
77 /// # fn foo() -> wasmtime::Result<()> {
78 /// use wasmtime::{Instance, Module, Store, Val};
79 /// use winliner::FeedbackCounters;
80 ///
81 /// // Instantiate your optimized Wasm module.
82 /// let mut store = Store::<()>::default();
83 /// let module = Module::from_file(store.engine(), "path/to/optimized.wasm")?;
84 /// let instance = Instance::new(&mut store, &module, &[])?;
85 ///
86 /// // Run the Wasm instance, call its exports, etc...
87 /// # let run = |_, _| -> wasmtime::Result<()> { Ok(()) };
88 /// run(&mut store, instance)?;
89 ///
90 /// // Extract the counters from the instance.
91 /// let counters = FeedbackCounters::from_instance(|name| {
92 /// match instance.get_global(&mut store, name)?.get(&mut store) {
93 /// Val::I64(x) => Some(x as u64),
94 /// _ => None,
95 /// }
96 /// })?;
97 /// # Ok(())
98 /// # }
99 /// ```
100 pub fn from_instance(mut read_global: impl FnMut(&str) -> Option<u64>) -> Result<Self> {
101 let mut counters = vec![];
102 let mut total_correct = 0_u64;
103 let mut total_incorrect = 0_u64;
104
105 for i in 0.. {
106 let correct = match read_global(&format!("__winliner_counter_{i}_correct")) {
107 Some(x) => x,
108 None => break,
109 };
110 total_correct = total_correct.saturating_add(correct);
111
112 let incorrect =
113 read_global(&format!("__winliner_counter_{i}_incorrect")).ok_or_else(|| {
114 anyhow!("Failed to read `__winliner_counter_{i}_incorrect` global")
115 })?;
116 total_incorrect = total_incorrect.saturating_add(incorrect);
117
118 counters.push(FeedbackCounter { correct, incorrect });
119 }
120
121 Ok(FeedbackCounters {
122 counters,
123 total_correct,
124 total_incorrect,
125 })
126 }
127
128 /// Merge another set of counters into this one.
129 ///
130 /// The `other` counters are merged into `self`.
131 ///
132 /// # Example
133 ///
134 /// ```
135 /// # fn foo() -> anyhow::Result<()> {
136 /// use wasmtime::{Engine, Module};
137 /// use winliner::FeedbackCounters;
138 ///
139 /// // Load the optimized Wasm module.
140 /// let engine = Engine::default();
141 /// let module = Module::from_file(&engine, "path/to/optimized.wasm")?;
142 ///
143 /// // Run the Wasm a couple times.
144 /// # let run_and_get_counters = |_| -> anyhow::Result<FeedbackCounters> { unimplemented!() };
145 /// let mut counters1: FeedbackCounters = run_and_get_counters(&module)?;
146 /// let counters2: FeedbackCounters = run_and_get_counters(&module)?;
147 ///
148 /// // Finally, combine the two sets of counters into a single set.
149 /// counters1.merge(&counters2);
150 /// # Ok(()) }
151 /// ```
152 pub fn merge(&mut self, other: &Self) -> Result<()> {
153 ensure!(
154 self.counters.len() == other.counters.len(),
155 "incompatible counters: generated from different Wasm modules"
156 );
157
158 for (me, them) in self.counters.iter_mut().zip(&other.counters) {
159 me.correct = me.correct.saturating_add(them.correct);
160 me.incorrect += me.incorrect.saturating_add(them.incorrect);
161 }
162
163 self.total_correct = self.total_correct.saturating_add(other.total_correct);
164 self.total_incorrect = self.total_incorrect.saturating_add(other.total_incorrect);
165
166 Ok(())
167 }
168
169 /// Get each counter in this set.
170 ///
171 /// You can use this to check for whether any speculative inlining has too
172 /// high of an incorrect guess rate.
173 pub fn counters(&self) -> &[FeedbackCounter] {
174 &self.counters
175 }
176
177 /// Get the total number of calls represented by this set of counters.
178 pub fn total(&self) -> u64 {
179 self.total_correct.saturating_add(self.total_incorrect)
180 }
181
182 /// Get the total number of times we correctly guessed the callee in this
183 /// set of counters.
184 pub fn total_correct(&self) -> u64 {
185 self.total_correct
186 }
187
188 /// Get the total number of times we incorrectly guessed the callee in this
189 /// set of counters.
190 pub fn total_incorrect(&self) -> u64 {
191 self.total_incorrect
192 }
193
194 /// Get the total ratio of correct guesses in this set of counters.
195 ///
196 /// Returns `None` when `self.total() == 0`.
197 pub fn total_correct_ratio(&self) -> Option<f64> {
198 if self.total() > 0 {
199 Some(self.total_correct as f64 / self.total() as f64)
200 } else {
201 None
202 }
203 }
204
205 /// Get the total ratio of incorrect guesses in this set of counters.
206 ///
207 /// Returns `None` when `self.total() == 0`.
208 pub fn total_incorrect_ratio(&self) -> Option<f64> {
209 if self.total() > 0 {
210 Some(self.total_incorrect as f64 / self.total() as f64)
211 } else {
212 None
213 }
214 }
215}
216
217impl FeedbackCounter {
218 /// The number of times we guessed correctly for this speculative inlining.
219 pub fn correct(&self) -> u64 {
220 self.correct
221 }
222
223 /// The number of times we guessed incorrectly for this speculative
224 /// inlining.
225 pub fn incorrect(&self) -> u64 {
226 self.incorrect
227 }
228
229 /// The total number of calls, correct or incorrect, to this counter's call
230 /// site.
231 pub fn total(&self) -> u64 {
232 self.correct.saturating_add(self.incorrect)
233 }
234
235 /// The ratio of correct guesses.
236 ///
237 /// Returns `None` when `self.total() == 0`.
238 pub fn correct_ratio(&self) -> Option<f64> {
239 if self.total() > 0 {
240 Some(self.correct as f64 / self.total() as f64)
241 } else {
242 None
243 }
244 }
245
246 /// The ratio of incorrect guesses.
247 ///
248 /// Returns `None` when `self.total() == 0`.
249 pub fn incorrect_ratio(&self) -> Option<f64> {
250 if self.total() > 0 {
251 Some(self.incorrect as f64 / self.total() as f64)
252 } else {
253 None
254 }
255 }
256}