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.rs246
1 files changed, 246 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..2b8a90363
--- /dev/null
+++ b/crates/ra_prof/src/hprof.rs
@@ -0,0 +1,246 @@
1//! Simple hierarchical profiler
2use once_cell::sync::Lazy;
3use std::{
4 cell::RefCell,
5 collections::{BTreeMap, HashSet},
6 io::{stderr, Write},
7 sync::{
8 atomic::{AtomicBool, Ordering},
9 RwLock,
10 },
11 time::{Duration, Instant},
12};
13
14use crate::tree::{Idx, Tree};
15
16/// Filtering syntax
17/// env RA_PROFILE=* // dump everything
18/// env RA_PROFILE=foo|bar|baz // enabled only selected entries
19/// env RA_PROFILE=*@3>10 // dump everything, up to depth 3, if it takes more than 10 ms
20pub fn init() {
21 let spec = std::env::var("RA_PROFILE").unwrap_or_default();
22 init_from(&spec);
23}
24
25pub fn init_from(spec: &str) {
26 let filter = if spec.is_empty() { Filter::disabled() } else { Filter::from_spec(spec) };
27 filter.install();
28}
29
30pub type Label = &'static str;
31
32/// This function starts a profiling scope in the current execution stack with a given description.
33/// It returns a Profile structure and measure elapsed time between this method invocation and Profile structure drop.
34/// 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.
35/// Profiling information is being printed in the stderr.
36///
37/// # Example
38/// ```
39/// use ra_prof::{profile, set_filter, Filter};
40///
41/// let f = Filter::from_spec("profile1|profile2@2");
42/// set_filter(f);
43/// profiling_function1();
44///
45/// fn profiling_function1() {
46/// let _p = profile("profile1");
47/// profiling_function2();
48/// }
49///
50/// fn profiling_function2() {
51/// let _p = profile("profile2");
52/// }
53/// ```
54/// This will print in the stderr the following:
55/// ```text
56/// 0ms - profile
57/// 0ms - profile2
58/// ```
59pub fn profile(label: Label) -> Profiler {
60 assert!(!label.is_empty());
61 let enabled = PROFILING_ENABLED.load(Ordering::Relaxed)
62 && PROFILE_STACK.with(|stack| stack.borrow_mut().push(label));
63 let label = if enabled { Some(label) } else { None };
64 Profiler { label, detail: None }
65}
66
67pub struct Profiler {
68 label: Option<Label>,
69 detail: Option<String>,
70}
71
72impl Profiler {
73 pub fn detail(mut self, detail: impl FnOnce() -> String) -> Profiler {
74 if self.label.is_some() {
75 self.detail = Some(detail())
76 }
77 self
78 }
79}
80
81impl Drop for Profiler {
82 fn drop(&mut self) {
83 match self {
84 Profiler { label: Some(label), detail } => {
85 PROFILE_STACK.with(|stack| {
86 stack.borrow_mut().pop(label, detail.take());
87 });
88 }
89 Profiler { label: None, .. } => (),
90 }
91 }
92}
93
94static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false);
95static FILTER: Lazy<RwLock<Filter>> = Lazy::new(Default::default);
96thread_local!(static PROFILE_STACK: RefCell<ProfileStack> = RefCell::new(ProfileStack::new()));
97
98#[derive(Default, Clone, Debug)]
99struct Filter {
100 depth: usize,
101 allowed: HashSet<String>,
102 longer_than: Duration,
103 version: usize,
104}
105
106impl Filter {
107 fn disabled() -> Filter {
108 Filter::default()
109 }
110
111 fn from_spec(mut spec: &str) -> Filter {
112 let longer_than = if let Some(idx) = spec.rfind('>') {
113 let longer_than = spec[idx + 1..].parse().expect("invalid profile longer_than");
114 spec = &spec[..idx];
115 Duration::from_millis(longer_than)
116 } else {
117 Duration::new(0, 0)
118 };
119
120 let depth = if let Some(idx) = spec.rfind('@') {
121 let depth: usize = spec[idx + 1..].parse().expect("invalid profile depth");
122 spec = &spec[..idx];
123 depth
124 } else {
125 999
126 };
127 let allowed =
128 if spec == "*" { HashSet::new() } else { spec.split('|').map(String::from).collect() };
129 Filter { depth, allowed, longer_than, version: 0 }
130 }
131
132 fn install(mut self) {
133 PROFILING_ENABLED.store(self.depth > 0, Ordering::SeqCst);
134 let mut old = FILTER.write().unwrap();
135 self.version = old.version + 1;
136 *old = self;
137 }
138}
139
140struct ProfileStack {
141 starts: Vec<Instant>,
142 filter: Filter,
143 messages: Tree<Message>,
144}
145
146#[derive(Default)]
147struct Message {
148 duration: Duration,
149 label: Label,
150 detail: Option<String>,
151}
152
153impl ProfileStack {
154 fn new() -> ProfileStack {
155 ProfileStack { starts: Vec::new(), messages: Tree::default(), filter: Default::default() }
156 }
157
158 fn push(&mut self, label: Label) -> bool {
159 if self.starts.is_empty() {
160 if let Ok(f) = FILTER.try_read() {
161 if f.version > self.filter.version {
162 self.filter = f.clone();
163 }
164 };
165 }
166 if self.starts.len() > self.filter.depth {
167 return false;
168 }
169 let allowed = &self.filter.allowed;
170 if self.starts.is_empty() && !allowed.is_empty() && !allowed.contains(label) {
171 return false;
172 }
173
174 self.starts.push(Instant::now());
175 self.messages.start();
176 true
177 }
178
179 pub fn pop(&mut self, label: Label, detail: Option<String>) {
180 let start = self.starts.pop().unwrap();
181 let duration = start.elapsed();
182 let level = self.starts.len();
183 self.messages.finish(Message { duration, label, detail });
184 if level == 0 {
185 let longer_than = self.filter.longer_than;
186 // Convert to millis for comparison to avoid problems with rounding
187 // (otherwise we could print `0ms` despite user's `>0` filter when
188 // `duration` is just a few nanos).
189 if duration.as_millis() > longer_than.as_millis() {
190 let stderr = stderr();
191 if let Some(root) = self.messages.root() {
192 print(&self.messages, root, 0, longer_than, &mut stderr.lock());
193 }
194 }
195 self.messages.clear();
196 assert!(self.starts.is_empty())
197 }
198 }
199}
200
201fn print(
202 tree: &Tree<Message>,
203 curr: Idx<Message>,
204 level: u32,
205 longer_than: Duration,
206 out: &mut impl Write,
207) {
208 let current_indent = " ".repeat(level as usize);
209 let detail = tree[curr].detail.as_ref().map(|it| format!(" @ {}", it)).unwrap_or_default();
210 writeln!(
211 out,
212 "{}{:5}ms - {}{}",
213 current_indent,
214 tree[curr].duration.as_millis(),
215 tree[curr].label,
216 detail,
217 )
218 .expect("printing profiling info");
219
220 let mut accounted_for = Duration::default();
221 let mut short_children = BTreeMap::new(); // Use `BTreeMap` to get deterministic output.
222 for child in tree.children(curr) {
223 accounted_for += tree[child].duration;
224
225 if tree[child].duration.as_millis() > longer_than.as_millis() {
226 print(tree, child, level + 1, longer_than, out)
227 } else {
228 let (total_duration, cnt) =
229 short_children.entry(tree[child].label).or_insert((Duration::default(), 0));
230 *total_duration += tree[child].duration;
231 *cnt += 1;
232 }
233 }
234
235 for (child_msg, (duration, count)) in short_children.iter() {
236 let millis = duration.as_millis();
237 writeln!(out, " {}{:5}ms - {} ({} calls)", current_indent, millis, child_msg, count)
238 .expect("printing profiling info");
239 }
240
241 let unaccounted = tree[curr].duration - accounted_for;
242 if tree.children(curr).next().is_some() && unaccounted > longer_than {
243 writeln!(out, " {}{:5}ms - ???", current_indent, unaccounted.as_millis())
244 .expect("printing profiling info");
245 }
246}