diff options
Diffstat (limited to 'crates/ra_prof/src/hprof.rs')
-rw-r--r-- | crates/ra_prof/src/hprof.rs | 391 |
1 files changed, 391 insertions, 0 deletions
diff --git a/crates/ra_prof/src/hprof.rs b/crates/ra_prof/src/hprof.rs new file mode 100644 index 000000000..79268513d --- /dev/null +++ b/crates/ra_prof/src/hprof.rs | |||
@@ -0,0 +1,391 @@ | |||
1 | //! Simple hierarchical profiler | ||
2 | use std::{ | ||
3 | cell::RefCell, | ||
4 | collections::{BTreeMap, HashSet}, | ||
5 | io::{stderr, Write}, | ||
6 | sync::{ | ||
7 | atomic::{AtomicBool, Ordering}, | ||
8 | RwLock, | ||
9 | }, | ||
10 | time::{Duration, Instant}, | ||
11 | }; | ||
12 | |||
13 | use once_cell::sync::Lazy; | ||
14 | |||
15 | /// Filtering syntax | ||
16 | /// env RA_PROFILE=* // dump everything | ||
17 | /// env RA_PROFILE=foo|bar|baz // enabled only selected entries | ||
18 | /// env RA_PROFILE=*@3>10 // dump everything, up to depth 3, if it takes more than 10 ms | ||
19 | pub fn init() { | ||
20 | let spec = std::env::var("RA_PROFILE").unwrap_or_default(); | ||
21 | init_from(&spec); | ||
22 | } | ||
23 | |||
24 | pub fn init_from(spec: &str) { | ||
25 | let filter = if spec.is_empty() { Filter::disabled() } else { Filter::from_spec(spec) }; | ||
26 | set_filter(filter); | ||
27 | } | ||
28 | |||
29 | pub type Label = &'static str; | ||
30 | |||
31 | /// This function starts a profiling scope in the current execution stack with a given description. | ||
32 | /// It returns a Profile structure and measure elapsed time between this method invocation and Profile structure drop. | ||
33 | /// It supports nested profiling scopes in case when this function invoked multiple times at the execution stack. In this case the profiling information will be nested at the output. | ||
34 | /// Profiling information is being printed in the stderr. | ||
35 | /// | ||
36 | /// # Example | ||
37 | /// ``` | ||
38 | /// use ra_prof::{profile, set_filter, Filter}; | ||
39 | /// | ||
40 | /// let f = Filter::from_spec("profile1|profile2@2"); | ||
41 | /// set_filter(f); | ||
42 | /// profiling_function1(); | ||
43 | /// | ||
44 | /// fn profiling_function1() { | ||
45 | /// let _p = profile("profile1"); | ||
46 | /// profiling_function2(); | ||
47 | /// } | ||
48 | /// | ||
49 | /// fn profiling_function2() { | ||
50 | /// let _p = profile("profile2"); | ||
51 | /// } | ||
52 | /// ``` | ||
53 | /// This will print in the stderr the following: | ||
54 | /// ```text | ||
55 | /// 0ms - profile | ||
56 | /// 0ms - profile2 | ||
57 | /// ``` | ||
58 | pub fn profile(label: Label) -> Profiler { | ||
59 | assert!(!label.is_empty()); | ||
60 | if !PROFILING_ENABLED.load(Ordering::Relaxed) { | ||
61 | return Profiler { label: None, detail: None }; | ||
62 | } | ||
63 | |||
64 | PROFILE_STACK.with(|stack| { | ||
65 | let mut stack = stack.borrow_mut(); | ||
66 | if stack.starts.is_empty() { | ||
67 | if let Ok(f) = FILTER.try_read() { | ||
68 | if f.version > stack.filter_data.version { | ||
69 | stack.filter_data = f.clone(); | ||
70 | } | ||
71 | }; | ||
72 | } | ||
73 | if stack.starts.len() > stack.filter_data.depth { | ||
74 | return Profiler { label: None, detail: None }; | ||
75 | } | ||
76 | let allowed = &stack.filter_data.allowed; | ||
77 | if stack.starts.is_empty() && !allowed.is_empty() && !allowed.contains(label) { | ||
78 | return Profiler { label: None, detail: None }; | ||
79 | } | ||
80 | |||
81 | stack.starts.push(Instant::now()); | ||
82 | Profiler { label: Some(label), detail: None } | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | pub struct Profiler { | ||
87 | label: Option<Label>, | ||
88 | detail: Option<String>, | ||
89 | } | ||
90 | |||
91 | impl Profiler { | ||
92 | pub fn detail(mut self, detail: impl FnOnce() -> String) -> Profiler { | ||
93 | if self.label.is_some() { | ||
94 | self.detail = Some(detail()) | ||
95 | } | ||
96 | self | ||
97 | } | ||
98 | } | ||
99 | |||
100 | /// Set profiling filter. It specifies descriptions allowed to profile. | ||
101 | /// This is helpful when call stack has too many nested profiling scopes. | ||
102 | /// Additionally filter can specify maximum depth of profiling scopes nesting. | ||
103 | /// | ||
104 | /// #Example | ||
105 | /// ``` | ||
106 | /// use ra_prof::{set_filter, Filter}; | ||
107 | /// let f = Filter::from_spec("profile1|profile2@2"); | ||
108 | /// set_filter(f); | ||
109 | /// ``` | ||
110 | fn set_filter(f: Filter) { | ||
111 | PROFILING_ENABLED.store(f.depth > 0, Ordering::SeqCst); | ||
112 | let set: HashSet<_> = f.allowed.iter().cloned().collect(); | ||
113 | let mut old = FILTER.write().unwrap(); | ||
114 | let filter_data = FilterData { | ||
115 | depth: f.depth, | ||
116 | allowed: set, | ||
117 | longer_than: f.longer_than, | ||
118 | version: old.version + 1, | ||
119 | }; | ||
120 | *old = filter_data; | ||
121 | } | ||
122 | |||
123 | struct Filter { | ||
124 | depth: usize, | ||
125 | allowed: Vec<String>, | ||
126 | longer_than: Duration, | ||
127 | } | ||
128 | |||
129 | impl Filter { | ||
130 | fn from_spec(mut spec: &str) -> Filter { | ||
131 | let longer_than = if let Some(idx) = spec.rfind('>') { | ||
132 | let longer_than = spec[idx + 1..].parse().expect("invalid profile longer_than"); | ||
133 | spec = &spec[..idx]; | ||
134 | Duration::from_millis(longer_than) | ||
135 | } else { | ||
136 | Duration::new(0, 0) | ||
137 | }; | ||
138 | |||
139 | let depth = if let Some(idx) = spec.rfind('@') { | ||
140 | let depth: usize = spec[idx + 1..].parse().expect("invalid profile depth"); | ||
141 | spec = &spec[..idx]; | ||
142 | depth | ||
143 | } else { | ||
144 | 999 | ||
145 | }; | ||
146 | let allowed = | ||
147 | if spec == "*" { Vec::new() } else { spec.split('|').map(String::from).collect() }; | ||
148 | Filter::new(depth, allowed, longer_than) | ||
149 | } | ||
150 | |||
151 | pub fn disabled() -> Filter { | ||
152 | Filter::new(0, Vec::new(), Duration::new(0, 0)) | ||
153 | } | ||
154 | |||
155 | pub fn new(depth: usize, allowed: Vec<String>, longer_than: Duration) -> Filter { | ||
156 | Filter { depth, allowed, longer_than } | ||
157 | } | ||
158 | } | ||
159 | |||
160 | struct ProfileStack { | ||
161 | starts: Vec<Instant>, | ||
162 | messages: Vec<Message>, | ||
163 | filter_data: FilterData, | ||
164 | } | ||
165 | |||
166 | struct Message { | ||
167 | level: usize, | ||
168 | duration: Duration, | ||
169 | label: Label, | ||
170 | detail: Option<String>, | ||
171 | } | ||
172 | |||
173 | impl ProfileStack { | ||
174 | fn new() -> ProfileStack { | ||
175 | ProfileStack { starts: Vec::new(), messages: Vec::new(), filter_data: Default::default() } | ||
176 | } | ||
177 | } | ||
178 | |||
179 | #[derive(Default, Clone)] | ||
180 | struct FilterData { | ||
181 | depth: usize, | ||
182 | version: usize, | ||
183 | allowed: HashSet<String>, | ||
184 | longer_than: Duration, | ||
185 | } | ||
186 | |||
187 | static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false); | ||
188 | |||
189 | static FILTER: Lazy<RwLock<FilterData>> = Lazy::new(Default::default); | ||
190 | |||
191 | thread_local!(static PROFILE_STACK: RefCell<ProfileStack> = RefCell::new(ProfileStack::new())); | ||
192 | |||
193 | impl Drop for Profiler { | ||
194 | fn drop(&mut self) { | ||
195 | match self { | ||
196 | Profiler { label: Some(label), detail } => { | ||
197 | PROFILE_STACK.with(|stack| { | ||
198 | let mut stack = stack.borrow_mut(); | ||
199 | let start = stack.starts.pop().unwrap(); | ||
200 | let duration = start.elapsed(); | ||
201 | let level = stack.starts.len(); | ||
202 | stack.messages.push(Message { level, duration, label, detail: detail.take() }); | ||
203 | if level == 0 { | ||
204 | let stdout = stderr(); | ||
205 | let longer_than = stack.filter_data.longer_than; | ||
206 | // Convert to millis for comparison to avoid problems with rounding | ||
207 | // (otherwise we could print `0ms` despite user's `>0` filter when | ||
208 | // `duration` is just a few nanos). | ||
209 | if duration.as_millis() > longer_than.as_millis() { | ||
210 | print(&stack.messages, longer_than, &mut stdout.lock()); | ||
211 | } | ||
212 | stack.messages.clear(); | ||
213 | } | ||
214 | }); | ||
215 | } | ||
216 | Profiler { label: None, .. } => (), | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | |||
221 | fn print(msgs: &[Message], longer_than: Duration, out: &mut impl Write) { | ||
222 | if msgs.is_empty() { | ||
223 | return; | ||
224 | } | ||
225 | let children_map = idx_to_children(msgs); | ||
226 | let root_idx = msgs.len() - 1; | ||
227 | print_for_idx(root_idx, &children_map, msgs, longer_than, out); | ||
228 | } | ||
229 | |||
230 | fn print_for_idx( | ||
231 | current_idx: usize, | ||
232 | children_map: &[Vec<usize>], | ||
233 | msgs: &[Message], | ||
234 | longer_than: Duration, | ||
235 | out: &mut impl Write, | ||
236 | ) { | ||
237 | let current = &msgs[current_idx]; | ||
238 | let current_indent = " ".repeat(current.level); | ||
239 | let detail = current.detail.as_ref().map(|it| format!(" @ {}", it)).unwrap_or_default(); | ||
240 | writeln!( | ||
241 | out, | ||
242 | "{}{:5}ms - {}{}", | ||
243 | current_indent, | ||
244 | current.duration.as_millis(), | ||
245 | current.label, | ||
246 | detail, | ||
247 | ) | ||
248 | .expect("printing profiling info"); | ||
249 | |||
250 | let longer_than_millis = longer_than.as_millis(); | ||
251 | let children_indices = &children_map[current_idx]; | ||
252 | let mut accounted_for = Duration::default(); | ||
253 | let mut short_children = BTreeMap::new(); // Use `BTreeMap` to get deterministic output. | ||
254 | |||
255 | for child_idx in children_indices.iter() { | ||
256 | let child = &msgs[*child_idx]; | ||
257 | if child.duration.as_millis() > longer_than_millis { | ||
258 | print_for_idx(*child_idx, children_map, msgs, longer_than, out); | ||
259 | } else { | ||
260 | let pair = short_children.entry(child.label).or_insert((Duration::default(), 0)); | ||
261 | pair.0 += child.duration; | ||
262 | pair.1 += 1; | ||
263 | } | ||
264 | accounted_for += child.duration; | ||
265 | } | ||
266 | |||
267 | for (child_msg, (duration, count)) in short_children.iter() { | ||
268 | let millis = duration.as_millis(); | ||
269 | writeln!(out, " {}{:5}ms - {} ({} calls)", current_indent, millis, child_msg, count) | ||
270 | .expect("printing profiling info"); | ||
271 | } | ||
272 | |||
273 | let unaccounted_millis = (current.duration - accounted_for).as_millis(); | ||
274 | if !children_indices.is_empty() | ||
275 | && unaccounted_millis > 0 | ||
276 | && unaccounted_millis > longer_than_millis | ||
277 | { | ||
278 | writeln!(out, " {}{:5}ms - ???", current_indent, unaccounted_millis) | ||
279 | .expect("printing profiling info"); | ||
280 | } | ||
281 | } | ||
282 | |||
283 | /// Returns a mapping from an index in the `msgs` to the vector with the indices of its children. | ||
284 | /// | ||
285 | /// This assumes that the entries in `msgs` are in the order of when the calls to `profile` finish. | ||
286 | /// In other words, a postorder of the call graph. In particular, the root is the last element of | ||
287 | /// `msgs`. | ||
288 | fn idx_to_children(msgs: &[Message]) -> Vec<Vec<usize>> { | ||
289 | // Initialize with the index of the root; `msgs` and `ancestors` should be never empty. | ||
290 | assert!(!msgs.is_empty()); | ||
291 | let mut ancestors = vec![msgs.len() - 1]; | ||
292 | let mut result: Vec<Vec<usize>> = vec![vec![]; msgs.len()]; | ||
293 | for (idx, msg) in msgs[..msgs.len() - 1].iter().enumerate().rev() { | ||
294 | // We need to find the parent of the current message, i.e., the last ancestor that has a | ||
295 | // level lower than the current message. | ||
296 | while msgs[*ancestors.last().unwrap()].level >= msg.level { | ||
297 | ancestors.pop(); | ||
298 | } | ||
299 | result[*ancestors.last().unwrap()].push(idx); | ||
300 | ancestors.push(idx); | ||
301 | } | ||
302 | // Note that above we visited all children from the last to the first one. Let's reverse vectors | ||
303 | // to get the more natural order where the first element is the first child. | ||
304 | for vec in result.iter_mut() { | ||
305 | vec.reverse(); | ||
306 | } | ||
307 | result | ||
308 | } | ||
309 | |||
310 | #[cfg(test)] | ||
311 | mod tests { | ||
312 | use super::*; | ||
313 | |||
314 | #[test] | ||
315 | fn test_basic_profile() { | ||
316 | let s = vec!["profile1".to_string(), "profile2".to_string()]; | ||
317 | let f = Filter::new(2, s, Duration::new(0, 0)); | ||
318 | set_filter(f); | ||
319 | profiling_function1(); | ||
320 | } | ||
321 | |||
322 | fn profiling_function1() { | ||
323 | let _p = profile("profile1"); | ||
324 | profiling_function2(); | ||
325 | } | ||
326 | |||
327 | fn profiling_function2() { | ||
328 | let _p = profile("profile2"); | ||
329 | } | ||
330 | |||
331 | #[test] | ||
332 | fn test_longer_than() { | ||
333 | let mut result = vec![]; | ||
334 | let msgs = vec![ | ||
335 | Message { level: 1, duration: Duration::from_nanos(3), label: "bar", detail: None }, | ||
336 | Message { level: 1, duration: Duration::from_nanos(2), label: "bar", detail: None }, | ||
337 | Message { level: 0, duration: Duration::from_millis(1), label: "foo", detail: None }, | ||
338 | ]; | ||
339 | print(&msgs, Duration::from_millis(0), &mut result); | ||
340 | // The calls to `bar` are so short that they'll be rounded to 0ms and should get collapsed | ||
341 | // when printing. | ||
342 | assert_eq!( | ||
343 | std::str::from_utf8(&result).unwrap(), | ||
344 | " 1ms - foo\n 0ms - bar (2 calls)\n" | ||
345 | ); | ||
346 | } | ||
347 | |||
348 | #[test] | ||
349 | fn test_unaccounted_for_topmost() { | ||
350 | let mut result = vec![]; | ||
351 | let msgs = vec![ | ||
352 | Message { level: 1, duration: Duration::from_millis(2), label: "bar", detail: None }, | ||
353 | Message { level: 0, duration: Duration::from_millis(5), label: "foo", detail: None }, | ||
354 | ]; | ||
355 | print(&msgs, Duration::from_millis(0), &mut result); | ||
356 | assert_eq!( | ||
357 | std::str::from_utf8(&result).unwrap().lines().collect::<Vec<_>>(), | ||
358 | vec![ | ||
359 | " 5ms - foo", | ||
360 | " 2ms - bar", | ||
361 | " 3ms - ???", | ||
362 | // Dummy comment to improve formatting | ||
363 | ] | ||
364 | ); | ||
365 | } | ||
366 | |||
367 | #[test] | ||
368 | fn test_unaccounted_for_multiple_levels() { | ||
369 | let mut result = vec![]; | ||
370 | let msgs = vec![ | ||
371 | Message { level: 2, duration: Duration::from_millis(3), label: "baz", detail: None }, | ||
372 | Message { level: 1, duration: Duration::from_millis(5), label: "bar", detail: None }, | ||
373 | Message { level: 2, duration: Duration::from_millis(2), label: "baz", detail: None }, | ||
374 | Message { level: 1, duration: Duration::from_millis(4), label: "bar", detail: None }, | ||
375 | Message { level: 0, duration: Duration::from_millis(9), label: "foo", detail: None }, | ||
376 | ]; | ||
377 | print(&msgs, Duration::from_millis(0), &mut result); | ||
378 | assert_eq!( | ||
379 | std::str::from_utf8(&result).unwrap().lines().collect::<Vec<_>>(), | ||
380 | vec![ | ||
381 | " 9ms - foo", | ||
382 | " 5ms - bar", | ||
383 | " 3ms - baz", | ||
384 | " 2ms - ???", | ||
385 | " 4ms - bar", | ||
386 | " 2ms - baz", | ||
387 | " 2ms - ???", | ||
388 | ] | ||
389 | ); | ||
390 | } | ||
391 | } | ||