diff options
Diffstat (limited to 'crates/profile')
-rw-r--r-- | crates/profile/src/google_cpu_profiler.rs | 19 | ||||
-rw-r--r-- | crates/profile/src/hprof.rs | 107 | ||||
-rw-r--r-- | crates/profile/src/lib.rs | 39 |
3 files changed, 135 insertions, 30 deletions
diff --git a/crates/profile/src/google_cpu_profiler.rs b/crates/profile/src/google_cpu_profiler.rs index db865c65b..cae6caeaa 100644 --- a/crates/profile/src/google_cpu_profiler.rs +++ b/crates/profile/src/google_cpu_profiler.rs | |||
@@ -14,26 +14,31 @@ extern "C" { | |||
14 | fn ProfilerStop(); | 14 | fn ProfilerStop(); |
15 | } | 15 | } |
16 | 16 | ||
17 | static PROFILER_STATE: AtomicUsize = AtomicUsize::new(OFF); | ||
18 | const OFF: usize = 0; | 17 | const OFF: usize = 0; |
19 | const ON: usize = 1; | 18 | const ON: usize = 1; |
20 | const PENDING: usize = 2; | 19 | const PENDING: usize = 2; |
21 | 20 | ||
22 | pub fn start(path: &Path) { | 21 | fn transition(current: usize, new: usize) -> bool { |
23 | if PROFILER_STATE.compare_and_swap(OFF, PENDING, Ordering::SeqCst) != OFF { | 22 | static STATE: AtomicUsize = AtomicUsize::new(OFF); |
23 | |||
24 | STATE.compare_exchange(current, new, Ordering::SeqCst, Ordering::SeqCst).is_ok() | ||
25 | } | ||
26 | |||
27 | pub(crate) fn start(path: &Path) { | ||
28 | if !transition(OFF, PENDING) { | ||
24 | panic!("profiler already started"); | 29 | panic!("profiler already started"); |
25 | } | 30 | } |
26 | let path = CString::new(path.display().to_string()).unwrap(); | 31 | let path = CString::new(path.display().to_string()).unwrap(); |
27 | if unsafe { ProfilerStart(path.as_ptr()) } == 0 { | 32 | if unsafe { ProfilerStart(path.as_ptr()) } == 0 { |
28 | panic!("profiler failed to start") | 33 | panic!("profiler failed to start") |
29 | } | 34 | } |
30 | assert!(PROFILER_STATE.compare_and_swap(PENDING, ON, Ordering::SeqCst) == PENDING); | 35 | assert!(transition(PENDING, ON)); |
31 | } | 36 | } |
32 | 37 | ||
33 | pub fn stop() { | 38 | pub(crate) fn stop() { |
34 | if PROFILER_STATE.compare_and_swap(ON, PENDING, Ordering::SeqCst) != ON { | 39 | if !transition(ON, PENDING) { |
35 | panic!("profiler is not started") | 40 | panic!("profiler is not started") |
36 | } | 41 | } |
37 | unsafe { ProfilerStop() }; | 42 | unsafe { ProfilerStop() }; |
38 | assert!(PROFILER_STATE.compare_and_swap(PENDING, OFF, Ordering::SeqCst) == PENDING); | 43 | assert!(transition(PENDING, OFF)); |
39 | } | 44 | } |
diff --git a/crates/profile/src/hprof.rs b/crates/profile/src/hprof.rs index 29d2ed518..5fdb37206 100644 --- a/crates/profile/src/hprof.rs +++ b/crates/profile/src/hprof.rs | |||
@@ -1,5 +1,4 @@ | |||
1 | //! Simple hierarchical profiler | 1 | //! Simple hierarchical profiler |
2 | use once_cell::sync::Lazy; | ||
3 | use std::{ | 2 | use std::{ |
4 | cell::RefCell, | 3 | cell::RefCell, |
5 | collections::{BTreeMap, HashSet}, | 4 | collections::{BTreeMap, HashSet}, |
@@ -12,6 +11,8 @@ use std::{ | |||
12 | time::{Duration, Instant}, | 11 | time::{Duration, Instant}, |
13 | }; | 12 | }; |
14 | 13 | ||
14 | use once_cell::sync::Lazy; | ||
15 | |||
15 | use crate::tree::{Idx, Tree}; | 16 | use crate::tree::{Idx, Tree}; |
16 | 17 | ||
17 | /// Filtering syntax | 18 | /// Filtering syntax |
@@ -56,18 +57,32 @@ type Label = &'static str; | |||
56 | /// 0ms - profile | 57 | /// 0ms - profile |
57 | /// 0ms - profile2 | 58 | /// 0ms - profile2 |
58 | /// ``` | 59 | /// ``` |
60 | #[inline] | ||
59 | pub fn span(label: Label) -> ProfileSpan { | 61 | pub fn span(label: Label) -> ProfileSpan { |
60 | assert!(!label.is_empty()); | 62 | debug_assert!(!label.is_empty()); |
61 | 63 | ||
62 | if PROFILING_ENABLED.load(Ordering::Relaxed) | 64 | let enabled = PROFILING_ENABLED.load(Ordering::Relaxed); |
63 | && PROFILE_STACK.with(|stack| stack.borrow_mut().push(label)) | 65 | if enabled && with_profile_stack(|stack| stack.push(label)) { |
64 | { | ||
65 | ProfileSpan(Some(ProfilerImpl { label, detail: None })) | 66 | ProfileSpan(Some(ProfilerImpl { label, detail: None })) |
66 | } else { | 67 | } else { |
67 | ProfileSpan(None) | 68 | ProfileSpan(None) |
68 | } | 69 | } |
69 | } | 70 | } |
70 | 71 | ||
72 | #[inline] | ||
73 | pub fn heartbeat_span() -> HeartbeatSpan { | ||
74 | let enabled = PROFILING_ENABLED.load(Ordering::Relaxed); | ||
75 | HeartbeatSpan::new(enabled) | ||
76 | } | ||
77 | |||
78 | #[inline] | ||
79 | pub fn heartbeat() { | ||
80 | let enabled = PROFILING_ENABLED.load(Ordering::Relaxed); | ||
81 | if enabled { | ||
82 | with_profile_stack(|it| it.heartbeat(1)); | ||
83 | } | ||
84 | } | ||
85 | |||
71 | pub struct ProfileSpan(Option<ProfilerImpl>); | 86 | pub struct ProfileSpan(Option<ProfilerImpl>); |
72 | 87 | ||
73 | struct ProfilerImpl { | 88 | struct ProfilerImpl { |
@@ -85,20 +100,48 @@ impl ProfileSpan { | |||
85 | } | 100 | } |
86 | 101 | ||
87 | impl Drop for ProfilerImpl { | 102 | impl Drop for ProfilerImpl { |
103 | #[inline] | ||
104 | fn drop(&mut self) { | ||
105 | with_profile_stack(|it| it.pop(self.label, self.detail.take())); | ||
106 | } | ||
107 | } | ||
108 | |||
109 | pub struct HeartbeatSpan { | ||
110 | enabled: bool, | ||
111 | } | ||
112 | |||
113 | impl HeartbeatSpan { | ||
114 | #[inline] | ||
115 | pub fn new(enabled: bool) -> Self { | ||
116 | if enabled { | ||
117 | with_profile_stack(|it| it.heartbeats(true)) | ||
118 | } | ||
119 | Self { enabled } | ||
120 | } | ||
121 | } | ||
122 | |||
123 | impl Drop for HeartbeatSpan { | ||
88 | fn drop(&mut self) { | 124 | fn drop(&mut self) { |
89 | PROFILE_STACK.with(|it| it.borrow_mut().pop(self.label, self.detail.take())); | 125 | if self.enabled { |
126 | with_profile_stack(|it| it.heartbeats(false)) | ||
127 | } | ||
90 | } | 128 | } |
91 | } | 129 | } |
92 | 130 | ||
93 | static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false); | 131 | static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false); |
94 | static FILTER: Lazy<RwLock<Filter>> = Lazy::new(Default::default); | 132 | static FILTER: Lazy<RwLock<Filter>> = Lazy::new(Default::default); |
95 | thread_local!(static PROFILE_STACK: RefCell<ProfileStack> = RefCell::new(ProfileStack::new())); | 133 | |
134 | fn with_profile_stack<T>(f: impl FnOnce(&mut ProfileStack) -> T) -> T { | ||
135 | thread_local!(static STACK: RefCell<ProfileStack> = RefCell::new(ProfileStack::new())); | ||
136 | STACK.with(|it| f(&mut *it.borrow_mut())) | ||
137 | } | ||
96 | 138 | ||
97 | #[derive(Default, Clone, Debug)] | 139 | #[derive(Default, Clone, Debug)] |
98 | struct Filter { | 140 | struct Filter { |
99 | depth: usize, | 141 | depth: usize, |
100 | allowed: HashSet<String>, | 142 | allowed: HashSet<String>, |
101 | longer_than: Duration, | 143 | longer_than: Duration, |
144 | heartbeat_longer_than: Duration, | ||
102 | version: usize, | 145 | version: usize, |
103 | } | 146 | } |
104 | 147 | ||
@@ -115,6 +158,7 @@ impl Filter { | |||
115 | } else { | 158 | } else { |
116 | Duration::new(0, 0) | 159 | Duration::new(0, 0) |
117 | }; | 160 | }; |
161 | let heartbeat_longer_than = longer_than; | ||
118 | 162 | ||
119 | let depth = if let Some(idx) = spec.rfind('@') { | 163 | let depth = if let Some(idx) = spec.rfind('@') { |
120 | let depth: usize = spec[idx + 1..].parse().expect("invalid profile depth"); | 164 | let depth: usize = spec[idx + 1..].parse().expect("invalid profile depth"); |
@@ -125,7 +169,7 @@ impl Filter { | |||
125 | }; | 169 | }; |
126 | let allowed = | 170 | let allowed = |
127 | if spec == "*" { HashSet::new() } else { spec.split('|').map(String::from).collect() }; | 171 | if spec == "*" { HashSet::new() } else { spec.split('|').map(String::from).collect() }; |
128 | Filter { depth, allowed, longer_than, version: 0 } | 172 | Filter { depth, allowed, longer_than, heartbeat_longer_than, version: 0 } |
129 | } | 173 | } |
130 | 174 | ||
131 | fn install(mut self) { | 175 | fn install(mut self) { |
@@ -137,9 +181,15 @@ impl Filter { | |||
137 | } | 181 | } |
138 | 182 | ||
139 | struct ProfileStack { | 183 | struct ProfileStack { |
140 | starts: Vec<Instant>, | 184 | frames: Vec<Frame>, |
141 | filter: Filter, | 185 | filter: Filter, |
142 | messages: Tree<Message>, | 186 | messages: Tree<Message>, |
187 | heartbeats: bool, | ||
188 | } | ||
189 | |||
190 | struct Frame { | ||
191 | t: Instant, | ||
192 | heartbeats: u32, | ||
143 | } | 193 | } |
144 | 194 | ||
145 | #[derive(Default)] | 195 | #[derive(Default)] |
@@ -151,35 +201,49 @@ struct Message { | |||
151 | 201 | ||
152 | impl ProfileStack { | 202 | impl ProfileStack { |
153 | fn new() -> ProfileStack { | 203 | fn new() -> ProfileStack { |
154 | ProfileStack { starts: Vec::new(), messages: Tree::default(), filter: Default::default() } | 204 | ProfileStack { |
205 | frames: Vec::new(), | ||
206 | messages: Tree::default(), | ||
207 | filter: Default::default(), | ||
208 | heartbeats: false, | ||
209 | } | ||
155 | } | 210 | } |
156 | 211 | ||
157 | fn push(&mut self, label: Label) -> bool { | 212 | fn push(&mut self, label: Label) -> bool { |
158 | if self.starts.is_empty() { | 213 | if self.frames.is_empty() { |
159 | if let Ok(f) = FILTER.try_read() { | 214 | if let Ok(f) = FILTER.try_read() { |
160 | if f.version > self.filter.version { | 215 | if f.version > self.filter.version { |
161 | self.filter = f.clone(); | 216 | self.filter = f.clone(); |
162 | } | 217 | } |
163 | }; | 218 | }; |
164 | } | 219 | } |
165 | if self.starts.len() > self.filter.depth { | 220 | if self.frames.len() > self.filter.depth { |
166 | return false; | 221 | return false; |
167 | } | 222 | } |
168 | let allowed = &self.filter.allowed; | 223 | let allowed = &self.filter.allowed; |
169 | if self.starts.is_empty() && !allowed.is_empty() && !allowed.contains(label) { | 224 | if self.frames.is_empty() && !allowed.is_empty() && !allowed.contains(label) { |
170 | return false; | 225 | return false; |
171 | } | 226 | } |
172 | 227 | ||
173 | self.starts.push(Instant::now()); | 228 | self.frames.push(Frame { t: Instant::now(), heartbeats: 0 }); |
174 | self.messages.start(); | 229 | self.messages.start(); |
175 | true | 230 | true |
176 | } | 231 | } |
177 | 232 | ||
178 | fn pop(&mut self, label: Label, detail: Option<String>) { | 233 | fn pop(&mut self, label: Label, detail: Option<String>) { |
179 | let start = self.starts.pop().unwrap(); | 234 | let frame = self.frames.pop().unwrap(); |
180 | let duration = start.elapsed(); | 235 | let duration = frame.t.elapsed(); |
236 | |||
237 | if self.heartbeats { | ||
238 | self.heartbeat(frame.heartbeats); | ||
239 | let avg_span = duration / (frame.heartbeats + 1); | ||
240 | if avg_span > self.filter.heartbeat_longer_than { | ||
241 | eprintln!("Too few heartbeats {} ({}/{:?})?", label, frame.heartbeats, duration) | ||
242 | } | ||
243 | } | ||
244 | |||
181 | self.messages.finish(Message { duration, label, detail }); | 245 | self.messages.finish(Message { duration, label, detail }); |
182 | if self.starts.is_empty() { | 246 | if self.frames.is_empty() { |
183 | let longer_than = self.filter.longer_than; | 247 | let longer_than = self.filter.longer_than; |
184 | // Convert to millis for comparison to avoid problems with rounding | 248 | // Convert to millis for comparison to avoid problems with rounding |
185 | // (otherwise we could print `0ms` despite user's `>0` filter when | 249 | // (otherwise we could print `0ms` despite user's `>0` filter when |
@@ -192,6 +256,15 @@ impl ProfileStack { | |||
192 | self.messages.clear(); | 256 | self.messages.clear(); |
193 | } | 257 | } |
194 | } | 258 | } |
259 | |||
260 | fn heartbeats(&mut self, yes: bool) { | ||
261 | self.heartbeats = yes; | ||
262 | } | ||
263 | fn heartbeat(&mut self, n: u32) { | ||
264 | if let Some(frame) = self.frames.last_mut() { | ||
265 | frame.heartbeats += n; | ||
266 | } | ||
267 | } | ||
195 | } | 268 | } |
196 | 269 | ||
197 | fn print( | 270 | fn print( |
diff --git a/crates/profile/src/lib.rs b/crates/profile/src/lib.rs index 79dba47d5..a31fb8f43 100644 --- a/crates/profile/src/lib.rs +++ b/crates/profile/src/lib.rs | |||
@@ -10,7 +10,7 @@ mod tree; | |||
10 | use std::cell::RefCell; | 10 | use std::cell::RefCell; |
11 | 11 | ||
12 | pub use crate::{ | 12 | pub use crate::{ |
13 | hprof::{init, init_from, span}, | 13 | hprof::{heartbeat, heartbeat_span, init, init_from, span}, |
14 | memory_usage::{Bytes, MemoryUsage}, | 14 | memory_usage::{Bytes, MemoryUsage}, |
15 | stop_watch::{StopWatch, StopWatchSpan}, | 15 | stop_watch::{StopWatch, StopWatchSpan}, |
16 | }; | 16 | }; |
@@ -52,7 +52,7 @@ impl Drop for Scope { | |||
52 | /// Usage: | 52 | /// Usage: |
53 | /// 1. Install gpref_tools (https://github.com/gperftools/gperftools), probably packaged with your Linux distro. | 53 | /// 1. Install gpref_tools (https://github.com/gperftools/gperftools), probably packaged with your Linux distro. |
54 | /// 2. Build with `cpu_profiler` feature. | 54 | /// 2. Build with `cpu_profiler` feature. |
55 | /// 3. Tun the code, the *raw* output would be in the `./out.profile` file. | 55 | /// 3. Run the code, the *raw* output would be in the `./out.profile` file. |
56 | /// 4. Install pprof for visualization (https://github.com/google/pprof). | 56 | /// 4. Install pprof for visualization (https://github.com/google/pprof). |
57 | /// 5. Bump sampling frequency to once per ms: `export CPUPROFILE_FREQUENCY=1000` | 57 | /// 5. Bump sampling frequency to once per ms: `export CPUPROFILE_FREQUENCY=1000` |
58 | /// 6. Use something like `pprof -svg target/release/rust-analyzer ./out.profile` to see the results. | 58 | /// 6. Use something like `pprof -svg target/release/rust-analyzer ./out.profile` to see the results. |
@@ -60,8 +60,17 @@ impl Drop for Scope { | |||
60 | /// For example, here's how I run profiling on NixOS: | 60 | /// For example, here's how I run profiling on NixOS: |
61 | /// | 61 | /// |
62 | /// ```bash | 62 | /// ```bash |
63 | /// $ nix-shell -p gperftools --run \ | 63 | /// $ bat -p shell.nix |
64 | /// 'cargo run --release -p rust-analyzer -- parse < ~/projects/rustbench/parser.rs > /dev/null' | 64 | /// with import <nixpkgs> {}; |
65 | /// mkShell { | ||
66 | /// buildInputs = [ gperftools ]; | ||
67 | /// shellHook = '' | ||
68 | /// export LD_LIBRARY_PATH="${gperftools}/lib:" | ||
69 | /// ''; | ||
70 | /// } | ||
71 | /// $ set -x CPUPROFILE_FREQUENCY 1000 | ||
72 | /// $ nix-shell --run 'cargo test --release --package rust-analyzer --lib -- benchmarks::benchmark_integrated_highlighting --exact --nocapture' | ||
73 | /// $ pprof -svg target/release/deps/rust_analyzer-8739592dc93d63cb crates/rust-analyzer/out.profile > profile.svg | ||
65 | /// ``` | 74 | /// ``` |
66 | /// | 75 | /// |
67 | /// See this diff for how to profile completions: | 76 | /// See this diff for how to profile completions: |
@@ -81,7 +90,9 @@ pub fn cpu_span() -> CpuSpan { | |||
81 | 90 | ||
82 | #[cfg(not(feature = "cpu_profiler"))] | 91 | #[cfg(not(feature = "cpu_profiler"))] |
83 | { | 92 | { |
84 | eprintln!("cpu_profiler feature is disabled") | 93 | eprintln!( |
94 | r#"cpu profiling is disabled, uncomment `default = [ "cpu_profiler" ]` in Cargo.toml to enable."# | ||
95 | ) | ||
85 | } | 96 | } |
86 | 97 | ||
87 | CpuSpan { _private: () } | 98 | CpuSpan { _private: () } |
@@ -91,7 +102,23 @@ impl Drop for CpuSpan { | |||
91 | fn drop(&mut self) { | 102 | fn drop(&mut self) { |
92 | #[cfg(feature = "cpu_profiler")] | 103 | #[cfg(feature = "cpu_profiler")] |
93 | { | 104 | { |
94 | google_cpu_profiler::stop() | 105 | google_cpu_profiler::stop(); |
106 | let profile_data = std::env::current_dir().unwrap().join("out.profile"); | ||
107 | eprintln!("Profile data saved to:\n\n {}\n", profile_data.display()); | ||
108 | let mut cmd = std::process::Command::new("pprof"); | ||
109 | cmd.arg("-svg").arg(std::env::current_exe().unwrap()).arg(&profile_data); | ||
110 | let out = cmd.output(); | ||
111 | |||
112 | match out { | ||
113 | Ok(out) if out.status.success() => { | ||
114 | let svg = profile_data.with_extension("svg"); | ||
115 | std::fs::write(&svg, &out.stdout).unwrap(); | ||
116 | eprintln!("Profile rendered to:\n\n {}\n", svg.display()); | ||
117 | } | ||
118 | _ => { | ||
119 | eprintln!("Failed to run:\n\n {:?}\n", cmd); | ||
120 | } | ||
121 | } | ||
95 | } | 122 | } |
96 | } | 123 | } |
97 | } | 124 | } |