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}