aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_prof/src/hprof.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_prof/src/hprof.rs')
-rw-r--r--crates/ra_prof/src/hprof.rs391
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
2use 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
13use 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
19pub fn init() {
20 let spec = std::env::var("RA_PROFILE").unwrap_or_default();
21 init_from(&spec);
22}
23
24pub 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
29pub 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/// ```
58pub 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
86pub struct Profiler {
87 label: Option<Label>,
88 detail: Option<String>,
89}
90
91impl 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/// ```
110fn 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
123struct Filter {
124 depth: usize,
125 allowed: Vec<String>,
126 longer_than: Duration,
127}
128
129impl 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
160struct ProfileStack {
161 starts: Vec<Instant>,
162 messages: Vec<Message>,
163 filter_data: FilterData,
164}
165
166struct Message {
167 level: usize,
168 duration: Duration,
169 label: Label,
170 detail: Option<String>,
171}
172
173impl ProfileStack {
174 fn new() -> ProfileStack {
175 ProfileStack { starts: Vec::new(), messages: Vec::new(), filter_data: Default::default() }
176 }
177}
178
179#[derive(Default, Clone)]
180struct FilterData {
181 depth: usize,
182 version: usize,
183 allowed: HashSet<String>,
184 longer_than: Duration,
185}
186
187static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false);
188
189static FILTER: Lazy<RwLock<FilterData>> = Lazy::new(Default::default);
190
191thread_local!(static PROFILE_STACK: RefCell<ProfileStack> = RefCell::new(ProfileStack::new()));
192
193impl 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
221fn 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
230fn 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`.
288fn 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)]
311mod 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}