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     dce: "Dead code elimination",
66     egraph: "Egraph based optimizations",
67     gvn: "Global value numbering",
68     licm: "Loop invariant code motion",
69     unreachable_code: "Remove unreachable blocks",
70     remove_constant_phis: "Remove constant phi-nodes",
71 
72     vcode_lower: "VCode lowering",
73     vcode_emit: "VCode emission",
74     vcode_emit_finish: "VCode emission finalization",
75 
76     regalloc: "Register allocation",
77     regalloc_checker: "Register allocation symbolic verification",
78     layout_renumber: "Layout full renumbering",
79 
80     canonicalize_nans: "Canonicalization of NaNs",
81 }
82 
83 impl Pass {
84     fn idx(self) -> usize {
85         self as usize
86     }
87 
88     /// Description of the pass.
89     pub fn description(self) -> &'static str {
90         match DESCRIPTIONS.get(self.idx()) {
91             Some(s) => s,
92             None => "<no pass>",
93         }
94     }
95 }
96 
97 impl fmt::Display for Pass {
98     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99         f.write_str(self.description())
100     }
101 }
102 
103 /// A profiler.
104 pub trait Profiler {
105     /// Start a profiling pass.
106     ///
107     /// Will return a token which when dropped indicates the end of the pass.
108     ///
109     /// Multiple passes can be active at the same time, but they must be started and stopped in a
110     /// LIFO fashion.
111     fn start_pass(&self, pass: Pass) -> Box<dyn Any>;
112 }
113 
114 // Information about passes in a single thread.
115 thread_local! {
116     static PROFILER: RefCell<Box<dyn Profiler>> = RefCell::new(Box::new(DefaultProfiler));
117 }
118 
119 /// Set the profiler for the current thread.
120 ///
121 /// Returns the old profiler.
122 pub fn set_thread_profiler(new_profiler: Box<dyn Profiler>) -> Box<dyn Profiler> {
123     PROFILER.with(|profiler| std::mem::replace(&mut *profiler.borrow_mut(), new_profiler))
124 }
125 
126 /// Start timing `pass` as a child of the currently running pass, if any.
127 ///
128 /// This function is called by the publicly exposed pass functions.
129 fn start_pass(pass: Pass) -> Box<dyn Any> {
130     PROFILER.with(|profiler| profiler.borrow().start_pass(pass))
131 }
132 
133 /// Accumulated timing information for a single pass.
134 #[derive(Default, Copy, Clone)]
135 struct PassTime {
136     /// Total time spent running this pass including children.
137     total: Duration,
138 
139     /// Time spent running in child passes.
140     child: Duration,
141 }
142 
143 /// Accumulated timing for all passes.
144 pub struct PassTimes {
145     pass: [PassTime; NUM_PASSES],
146 }
147 
148 impl PassTimes {
149     /// Add `other` to the timings of this `PassTimes`.
150     pub fn add(&mut self, other: &Self) {
151         for (a, b) in self.pass.iter_mut().zip(&other.pass[..]) {
152             a.total += b.total;
153             a.child += b.child;
154         }
155     }
156 
157     /// Returns the total amount of time taken by all the passes measured.
158     pub fn total(&self) -> Duration {
159         self.pass.iter().map(|p| p.total - p.child).sum()
160     }
161 }
162 
163 impl Default for PassTimes {
164     fn default() -> Self {
165         Self {
166             pass: [Default::default(); NUM_PASSES],
167         }
168     }
169 }
170 
171 impl fmt::Display for PassTimes {
172     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173         writeln!(f, "======== ========  ==================================")?;
174         writeln!(f, "   Total     Self  Pass")?;
175         writeln!(f, "-------- --------  ----------------------------------")?;
176         for (time, desc) in self.pass.iter().zip(&DESCRIPTIONS[..]) {
177             // Omit passes that haven't run.
178             if time.total == Duration::default() {
179                 continue;
180             }
181 
182             // Write a duration as secs.millis, trailing space.
183             fn fmtdur(mut dur: Duration, f: &mut fmt::Formatter) -> fmt::Result {
184                 // Round to nearest ms by adding 500us.
185                 dur += Duration::new(0, 500_000);
186                 let ms = dur.subsec_millis();
187                 write!(f, "{:4}.{:03} ", dur.as_secs(), ms)
188             }
189 
190             fmtdur(time.total, f)?;
191             if let Some(s) = time.total.checked_sub(time.child) {
192                 fmtdur(s, f)?;
193             }
194             writeln!(f, " {}", desc)?;
195         }
196         writeln!(f, "======== ========  ==================================")
197     }
198 }
199 
200 // Information about passes in a single thread.
201 thread_local! {
202     static PASS_TIME: RefCell<PassTimes> = RefCell::new(Default::default());
203 }
204 
205 /// The default profiler. You can get the results using [`take_current`].
206 pub struct DefaultProfiler;
207 
208 /// Take the current accumulated pass timings and reset the timings for the current thread.
209 ///
210 /// Only applies when [`DefaultProfiler`] is used.
211 pub fn take_current() -> PassTimes {
212     PASS_TIME.with(|rc| mem::take(&mut *rc.borrow_mut()))
213 }
214 
215 #[cfg(feature = "timing")]
216 mod enabled {
217     use super::{DefaultProfiler, Pass, Profiler, PASS_TIME};
218     use std::any::Any;
219     use std::boxed::Box;
220     use std::cell::Cell;
221     use std::time::Instant;
222 
223     // Information about passes in a single thread.
224     thread_local! {
225         static CURRENT_PASS: Cell<Pass> = const { Cell::new(Pass::None) };
226     }
227 
228     impl Profiler for DefaultProfiler {
229         fn start_pass(&self, pass: Pass) -> Box<dyn Any> {
230             let prev = CURRENT_PASS.with(|p| p.replace(pass));
231             log::debug!("timing: Starting {}, (during {})", pass, prev);
232             Box::new(DefaultTimingToken {
233                 start: Instant::now(),
234                 pass,
235                 prev,
236             })
237         }
238     }
239 
240     /// A timing token is responsible for timing the currently running pass. Timing starts when it
241     /// is created and ends when it is dropped.
242     ///
243     /// Multiple passes can be active at the same time, but they must be started and stopped in a
244     /// LIFO fashion.
245     struct DefaultTimingToken {
246         /// Start time for this pass.
247         start: Instant,
248 
249         // Pass being timed by this token.
250         pass: Pass,
251 
252         // The previously active pass which will be restored when this token is dropped.
253         prev: Pass,
254     }
255 
256     /// Dropping a timing token indicated the end of the pass.
257     impl Drop for DefaultTimingToken {
258         fn drop(&mut self) {
259             let duration = self.start.elapsed();
260             log::debug!("timing: Ending {}: {}ms", self.pass, duration.as_millis());
261             let old_cur = CURRENT_PASS.with(|p| p.replace(self.prev));
262             debug_assert_eq!(self.pass, old_cur, "Timing tokens dropped out of order");
263             PASS_TIME.with(|rc| {
264                 let mut table = rc.borrow_mut();
265                 table.pass[self.pass.idx()].total += duration;
266                 if let Some(parent) = table.pass.get_mut(self.prev.idx()) {
267                     parent.child += duration;
268                 }
269             })
270         }
271     }
272 }
273 
274 #[cfg(not(feature = "timing"))]
275 mod disabled {
276     use super::{DefaultProfiler, Pass, Profiler};
277     use std::any::Any;
278     use std::boxed::Box;
279 
280     impl Profiler for DefaultProfiler {
281         fn start_pass(&self, _pass: Pass) -> Box<dyn Any> {
282             Box::new(())
283         }
284     }
285 }
286 
287 #[cfg(test)]
288 mod tests {
289     use super::*;
290     use alloc::string::ToString;
291 
292     #[test]
293     fn display() {
294         assert_eq!(Pass::None.to_string(), "<no pass>");
295         assert_eq!(Pass::regalloc.to_string(), "Register allocation");
296     }
297 }
298