1 //! Pass timing.
2 //!
3 //! This modules provides facilities for timing the execution of individual compilation passes.
4 
5 use alloc::boxed::Box;
6 use core::any::Any;
7 use core::fmt;
8 
9 // Each pass that can be timed is predefined with the `define_passes!` macro. Each pass has a
10 // snake_case name and a plain text description used when printing out the timing report.
11 //
12 // This macro defines:
13 //
14 // - A C-style enum containing all the pass names and a `None` variant.
15 // - A usize constant with the number of defined passes.
16 // - A const array of pass descriptions.
17 // - A public function per pass used to start the timing of that pass.
18 macro_rules! define_passes {
19     ($($pass:ident: $desc:expr,)+) => {
20         /// A single profiled pass.
21         #[expect(non_camel_case_types, reason = "macro-generated code")]
22         #[derive(Clone, Copy, Debug, PartialEq, Eq)]
23         pub enum Pass {
24             $(#[doc=$desc] $pass,)+
25             /// No active pass.
26             None,
27         }
28 
29         /// The amount of profiled passes.
30         pub const NUM_PASSES: usize = Pass::None as usize;
31 
32         const DESCRIPTIONS: [&str; NUM_PASSES] = [ $($desc),+ ];
33 
34         $(
35             #[doc=$desc]
36             #[must_use]
37             pub fn $pass() -> Box<dyn Any> {
38                 start_pass(Pass::$pass)
39             }
40         )+
41     }
42 }
43 
44 // Pass definitions.
45 define_passes! {
46     // All these are used in other crates but defined here so they appear in the unified
47     // `PassTimes` output.
48     process_file: "Processing test file",
49     parse_text: "Parsing textual Cranelift IR",
50     wasm_translate_module: "Translate WASM module",
51     wasm_translate_function: "Translate WASM function",
52 
53     verifier: "Verify Cranelift IR",
54 
55     compile: "Compilation passes",
56     try_incremental_cache: "Try loading from incremental cache",
57     store_incremental_cache: "Store in incremental cache",
58     flowgraph: "Control flow graph",
59     domtree: "Dominator tree",
60     loop_analysis: "Loop analysis",
61     preopt: "Pre-legalization rewriting",
62     egraph: "Egraph based optimizations",
63     gvn: "Global value numbering",
64     licm: "Loop invariant code motion",
65     unreachable_code: "Remove unreachable blocks",
66     remove_constant_phis: "Remove constant phi-nodes",
67 
68     vcode_lower: "VCode lowering",
69     vcode_emit: "VCode emission",
70     vcode_emit_finish: "VCode emission finalization",
71 
72     regalloc: "Register allocation",
73     regalloc_checker: "Register allocation symbolic verification",
74     layout_renumber: "Layout full renumbering",
75 
76     canonicalize_nans: "Canonicalization of NaNs",
77 }
78 
79 impl Pass {
80     fn idx(self) -> usize {
81         self as usize
82     }
83 
84     /// Description of the pass.
85     pub fn description(self) -> &'static str {
86         match DESCRIPTIONS.get(self.idx()) {
87             Some(s) => s,
88             None => "<no pass>",
89         }
90     }
91 }
92 
93 impl fmt::Display for Pass {
94     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95         f.write_str(self.description())
96     }
97 }
98 
99 /// A profiler.
100 pub trait Profiler {
101     /// Start a profiling pass.
102     ///
103     /// Will return a token which when dropped indicates the end of the pass.
104     ///
105     /// Multiple passes can be active at the same time, but they must be started and stopped in a
106     /// LIFO fashion.
107     fn start_pass(&self, pass: Pass) -> Box<dyn Any>;
108 }
109 
110 /// The default profiler. You can get the results using [`take_current`].
111 pub struct DefaultProfiler;
112 
113 #[cfg(not(feature = "timing"))]
114 pub(crate) use disabled::*;
115 #[cfg(feature = "timing")]
116 pub use enabled::*;
117 
118 #[cfg(feature = "timing")]
119 mod enabled {
120     use super::{DESCRIPTIONS, DefaultProfiler, NUM_PASSES, Pass, Profiler};
121     use alloc::boxed::Box;
122     use core::any::Any;
123     use core::cell::{Cell, RefCell};
124     use core::fmt;
125     use core::mem;
126     use core::time::Duration;
127     use std::time::Instant;
128 
129     // Information about passes in a single thread.
130     thread_local! {
131         static PROFILER: RefCell<Box<dyn Profiler>> = RefCell::new(Box::new(DefaultProfiler));
132     }
133 
134     /// Set the profiler for the current thread.
135     ///
136     /// Returns the old profiler.
137     pub fn set_thread_profiler(new_profiler: Box<dyn Profiler>) -> Box<dyn Profiler> {
138         PROFILER.with(|profiler| core::mem::replace(&mut *profiler.borrow_mut(), new_profiler))
139     }
140 
141     /// Start timing `pass` as a child of the currently running pass, if any.
142     ///
143     /// This function is called by the publicly exposed pass functions.
144     pub fn start_pass(pass: Pass) -> Box<dyn Any> {
145         PROFILER.with(|profiler| profiler.borrow().start_pass(pass))
146     }
147 
148     /// Accumulated timing information for a single pass.
149     #[derive(Default, Copy, Clone)]
150     struct PassTime {
151         /// Total time spent running this pass including children.
152         total: Duration,
153 
154         /// Time spent running in child passes.
155         child: Duration,
156     }
157 
158     /// Accumulated timing for all passes.
159     pub struct PassTimes {
160         pass: [PassTime; NUM_PASSES],
161     }
162 
163     impl PassTimes {
164         /// Add `other` to the timings of this `PassTimes`.
165         pub fn add(&mut self, other: &Self) {
166             for (a, b) in self.pass.iter_mut().zip(&other.pass[..]) {
167                 a.total += b.total;
168                 a.child += b.child;
169             }
170         }
171 
172         /// Returns the total amount of time taken by all the passes measured.
173         pub fn total(&self) -> Duration {
174             self.pass.iter().map(|p| p.total - p.child).sum()
175         }
176     }
177 
178     impl Default for PassTimes {
179         fn default() -> Self {
180             Self {
181                 pass: [Default::default(); NUM_PASSES],
182             }
183         }
184     }
185 
186     impl fmt::Display for PassTimes {
187         fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
188             writeln!(f, "======== ========  ==================================")?;
189             writeln!(f, "   Total     Self  Pass")?;
190             writeln!(f, "-------- --------  ----------------------------------")?;
191             for (time, desc) in self.pass.iter().zip(&DESCRIPTIONS[..]) {
192                 // Omit passes that haven't run.
193                 if time.total == Duration::default() {
194                     continue;
195                 }
196 
197                 // Write a duration as secs.millis, trailing space.
198                 fn fmtdur(mut dur: Duration, f: &mut fmt::Formatter) -> fmt::Result {
199                     // Round to nearest ms by adding 500us.
200                     dur += Duration::new(0, 500_000);
201                     let ms = dur.subsec_millis();
202                     write!(f, "{:4}.{:03} ", dur.as_secs(), ms)
203                 }
204 
205                 fmtdur(time.total, f)?;
206                 if let Some(s) = time.total.checked_sub(time.child) {
207                     fmtdur(s, f)?;
208                 }
209                 writeln!(f, " {desc}")?;
210             }
211             writeln!(f, "======== ========  ==================================")
212         }
213     }
214 
215     // Information about passes in a single thread.
216     thread_local! {
217         static PASS_TIME: RefCell<PassTimes> = RefCell::new(Default::default());
218     }
219 
220     /// Take the current accumulated pass timings and reset the timings for the current thread.
221     ///
222     /// Only applies when [`DefaultProfiler`] is used.
223     pub fn take_current() -> PassTimes {
224         PASS_TIME.with(|rc| mem::take(&mut *rc.borrow_mut()))
225     }
226 
227     // Information about passes in a single thread.
228     thread_local! {
229         static CURRENT_PASS: Cell<Pass> = const { Cell::new(Pass::None) };
230     }
231 
232     impl Profiler for DefaultProfiler {
233         fn start_pass(&self, pass: Pass) -> Box<dyn Any> {
234             let prev = CURRENT_PASS.with(|p| p.replace(pass));
235             log::debug!("timing: Starting {pass}, (during {prev})");
236             Box::new(DefaultTimingToken {
237                 start: Instant::now(),
238                 pass,
239                 prev,
240             })
241         }
242     }
243 
244     /// A timing token is responsible for timing the currently running pass. Timing starts when it
245     /// is created and ends when it is dropped.
246     ///
247     /// Multiple passes can be active at the same time, but they must be started and stopped in a
248     /// LIFO fashion.
249     struct DefaultTimingToken {
250         /// Start time for this pass.
251         start: Instant,
252 
253         // Pass being timed by this token.
254         pass: Pass,
255 
256         // The previously active pass which will be restored when this token is dropped.
257         prev: Pass,
258     }
259 
260     /// Dropping a timing token indicated the end of the pass.
261     impl Drop for DefaultTimingToken {
262         fn drop(&mut self) {
263             let duration = self.start.elapsed();
264             log::debug!("timing: Ending {}: {}ms", self.pass, duration.as_millis());
265             let old_cur = CURRENT_PASS.with(|p| p.replace(self.prev));
266             debug_assert_eq!(self.pass, old_cur, "Timing tokens dropped out of order");
267             PASS_TIME.with(|rc| {
268                 let mut table = rc.borrow_mut();
269                 table.pass[self.pass.idx()].total += duration;
270                 if let Some(parent) = table.pass.get_mut(self.prev.idx()) {
271                     parent.child += duration;
272                 }
273             })
274         }
275     }
276 }
277 
278 #[cfg(not(feature = "timing"))]
279 mod disabled {
280     use super::{DefaultProfiler, Pass, Profiler};
281     use alloc::boxed::Box;
282     use core::any::Any;
283 
284     impl Profiler for DefaultProfiler {
285         fn start_pass(&self, _pass: Pass) -> Box<dyn Any> {
286             Box::new(())
287         }
288     }
289 
290     pub(crate) fn start_pass(_pass: Pass) -> Box<dyn Any> {
291         Box::new(())
292     }
293 }
294 
295 #[cfg(test)]
296 mod tests {
297     use super::*;
298     use alloc::string::ToString;
299 
300     #[test]
301     fn display() {
302         assert_eq!(Pass::None.to_string(), "<no pass>");
303         assert_eq!(Pass::regalloc.to_string(), "Register allocation");
304     }
305 }
306