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