1use std::{
2 ops::Range,
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use gpui::{
8 App, AppContext, Context, Entity, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
9 ScrollHandle, SerializedTaskTiming, StatefulInteractiveElement, Styled, Task, TaskTiming,
10 TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, div, prelude::FluentBuilder, px,
11 relative, size,
12};
13use util::ResultExt;
14use workspace::{
15 Workspace,
16 ui::{
17 ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, ToggleState,
18 WithScrollbar, h_flex, v_flex,
19 },
20};
21use zed_actions::OpenPerformanceProfiler;
22
23pub fn init(startup_time: Instant, cx: &mut App) {
24 cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| {
25 workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| {
26 let window_handle = window
27 .window_handle()
28 .downcast::<Workspace>()
29 .expect("Workspaces are root Windows");
30 open_performance_profiler(startup_time, workspace, window_handle, cx);
31 });
32 })
33 .detach();
34}
35
36fn open_performance_profiler(
37 startup_time: Instant,
38 _workspace: &mut workspace::Workspace,
39 workspace_handle: WindowHandle<Workspace>,
40 cx: &mut App,
41) {
42 let existing_window = cx
43 .windows()
44 .into_iter()
45 .find_map(|window| window.downcast::<ProfilerWindow>());
46
47 if let Some(existing_window) = existing_window {
48 existing_window
49 .update(cx, |profiler_window, window, _cx| {
50 profiler_window.workspace = Some(workspace_handle);
51 window.activate_window();
52 })
53 .log_err();
54 return;
55 }
56
57 let default_bounds = size(px(1280.), px(720.)); // 16:9
58
59 cx.open_window(
60 WindowOptions {
61 titlebar: Some(TitlebarOptions {
62 title: Some("Profiler Window".into()),
63 appears_transparent: false,
64 traffic_light_position: None,
65 }),
66 focus: true,
67 show: true,
68 is_movable: true,
69 kind: gpui::WindowKind::Normal,
70 window_background: cx.theme().window_background_appearance(),
71 window_decorations: None,
72 window_min_size: Some(default_bounds),
73 window_bounds: Some(WindowBounds::centered(default_bounds, cx)),
74 ..Default::default()
75 },
76 |_window, cx| ProfilerWindow::new(startup_time, Some(workspace_handle), cx),
77 )
78 .log_err();
79}
80
81enum DataMode {
82 Realtime(Option<Vec<TaskTiming>>),
83 Snapshot(Vec<TaskTiming>),
84}
85
86struct TimingBar {
87 location: &'static core::panic::Location<'static>,
88 start: Instant,
89 end: Instant,
90 color: Hsla,
91}
92
93pub struct ProfilerWindow {
94 startup_time: Instant,
95 data: DataMode,
96 include_self_timings: ToggleState,
97 autoscroll: bool,
98 scroll_handle: ScrollHandle,
99 workspace: Option<WindowHandle<Workspace>>,
100 _refresh: Option<Task<()>>,
101}
102
103impl ProfilerWindow {
104 pub fn new(
105 startup_time: Instant,
106 workspace_handle: Option<WindowHandle<Workspace>>,
107 cx: &mut App,
108 ) -> Entity<Self> {
109 let entity = cx.new(|cx| ProfilerWindow {
110 startup_time,
111 data: DataMode::Realtime(None),
112 include_self_timings: ToggleState::Unselected,
113 autoscroll: true,
114 scroll_handle: ScrollHandle::new(),
115 workspace: workspace_handle,
116 _refresh: Some(Self::begin_listen(cx)),
117 });
118
119 entity
120 }
121
122 fn begin_listen(cx: &mut Context<Self>) -> Task<()> {
123 cx.spawn(async move |this, cx| {
124 loop {
125 let data = cx
126 .foreground_executor()
127 .dispatcher
128 .get_current_thread_timings();
129
130 this.update(cx, |this: &mut ProfilerWindow, cx| {
131 let scroll_offset = this.scroll_handle.offset();
132 let max_offset = this.scroll_handle.max_offset();
133 this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
134
135 this.data = DataMode::Realtime(Some(data));
136
137 if this.autoscroll {
138 this.scroll_handle.scroll_to_bottom();
139 }
140
141 cx.notify();
142 })
143 .ok();
144
145 // yield to the executor
146 cx.background_executor()
147 .timer(Duration::from_micros(1))
148 .await;
149 }
150 })
151 }
152
153 fn get_timings(&self) -> Option<&Vec<TaskTiming>> {
154 match &self.data {
155 DataMode::Realtime(data) => data.as_ref(),
156 DataMode::Snapshot(data) => Some(data),
157 }
158 }
159
160 fn render_timing(
161 &self,
162 value_range: Range<Instant>,
163 item: TimingBar,
164 cx: &App,
165 ) -> impl IntoElement {
166 let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32;
167
168 let remap = value_range
169 .end
170 .duration_since(value_range.start)
171 .as_secs_f32()
172 * 1000f32;
173
174 let start = (item.start.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap;
175 let end = (item.end.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap;
176
177 let bar_width = end - start.abs();
178
179 let location = item
180 .location
181 .file()
182 .rsplit_once("/")
183 .unwrap_or(("", item.location.file()))
184 .1;
185 let location = location.rsplit_once("\\").unwrap_or(("", location)).1;
186
187 let label = format!(
188 "{}:{}:{}",
189 location,
190 item.location.line(),
191 item.location.column()
192 );
193
194 h_flex()
195 .gap_2()
196 .w_full()
197 .h(px(32.0))
198 .child(
199 div()
200 .w(px(200.0))
201 .flex_shrink_0()
202 .overflow_hidden()
203 .child(div().text_ellipsis().child(label)),
204 )
205 .child(
206 div()
207 .flex_1()
208 .h(px(24.0))
209 .bg(cx.theme().colors().background)
210 .rounded_md()
211 .p(px(2.0))
212 .relative()
213 .child(
214 div()
215 .absolute()
216 .h_full()
217 .rounded_sm()
218 .bg(item.color)
219 .left(relative(start.max(0f32)))
220 .w(relative(bar_width)),
221 ),
222 )
223 .child(
224 div()
225 .min_w(px(60.0))
226 .flex_shrink_0()
227 .text_right()
228 .child(format!("{:.1}ms", time_ms)),
229 )
230 }
231}
232
233impl Render for ProfilerWindow {
234 fn render(
235 &mut self,
236 window: &mut gpui::Window,
237 cx: &mut gpui::Context<Self>,
238 ) -> impl gpui::IntoElement {
239 v_flex()
240 .id("profiler")
241 .w_full()
242 .h_full()
243 .gap_2()
244 .bg(cx.theme().colors().surface_background)
245 .text_color(cx.theme().colors().text)
246 .child(
247 h_flex()
248 .w_full()
249 .justify_between()
250 .child(
251 h_flex()
252 .gap_2()
253 .child(
254 Button::new(
255 "switch-mode",
256 match self.data {
257 DataMode::Snapshot { .. } => "Resume",
258 DataMode::Realtime(_) => "Pause",
259 },
260 )
261 .style(ButtonStyle::Filled)
262 .on_click(cx.listener(
263 |this, _, _window, cx| {
264 match &this.data {
265 DataMode::Realtime(Some(data)) => {
266 this._refresh = None;
267 this.data = DataMode::Snapshot(data.clone());
268 }
269 DataMode::Snapshot { .. } => {
270 this._refresh = Some(Self::begin_listen(cx));
271 this.data = DataMode::Realtime(None);
272 }
273 _ => {}
274 };
275 cx.notify();
276 },
277 )),
278 )
279 .child(
280 Button::new("export-data", "Save")
281 .style(ButtonStyle::Filled)
282 .on_click(cx.listener(|this, _, _window, cx| {
283 let Some(workspace) = this.workspace else {
284 return;
285 };
286
287 let Some(data) = this.get_timings() else {
288 return;
289 };
290 let timings =
291 SerializedTaskTiming::convert(this.startup_time, &data);
292
293 let active_path = workspace
294 .read_with(cx, |workspace, cx| {
295 workspace.most_recent_active_path(cx)
296 })
297 .log_err()
298 .flatten()
299 .and_then(|p| p.parent().map(|p| p.to_owned()))
300 .unwrap_or_else(|| PathBuf::default());
301
302 let path = cx.prompt_for_new_path(
303 &active_path,
304 Some("performance_profile.miniprof"),
305 );
306
307 cx.background_spawn(async move {
308 let path = path.await;
309 let path =
310 path.log_err().and_then(|p| p.log_err()).flatten();
311
312 let Some(path) = path else {
313 return;
314 };
315
316 let Some(timings) =
317 serde_json::to_string(&timings).log_err()
318 else {
319 return;
320 };
321
322 smol::fs::write(path, &timings).await.log_err();
323 })
324 .detach();
325 })),
326 ),
327 )
328 .child(
329 Checkbox::new("include-self", self.include_self_timings)
330 .label("Include profiler timings")
331 .on_click(cx.listener(|this, checked, _window, cx| {
332 this.include_self_timings = *checked;
333 cx.notify();
334 })),
335 ),
336 )
337 .when_some(self.get_timings(), |div, e| {
338 if e.len() == 0 {
339 return div;
340 }
341
342 let min = e[0].start;
343 let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now());
344 div.child(
345 v_flex()
346 .id("timings.bars")
347 .overflow_scroll()
348 .w_full()
349 .h_full()
350 .gap_2()
351 .track_scroll(&self.scroll_handle)
352 .on_scroll_wheel(cx.listener(|this, _, _, _cx| {
353 let scroll_offset = this.scroll_handle.offset();
354 let max_offset = this.scroll_handle.max_offset();
355 this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
356 }))
357 .children(
358 e.iter()
359 .filter(|timing| {
360 timing
361 .end
362 .unwrap_or_else(|| Instant::now())
363 .duration_since(timing.start)
364 .as_millis()
365 >= 1
366 })
367 .filter(|timing| {
368 if self.include_self_timings.selected() {
369 true
370 } else {
371 !timing.location.file().ends_with("miniprofiler_ui.rs")
372 }
373 })
374 .enumerate()
375 .map(|(i, timing)| {
376 self.render_timing(
377 max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
378 ..max,
379 TimingBar {
380 location: timing.location,
381 start: timing.start,
382 end: timing.end.unwrap_or_else(|| Instant::now()),
383 color: cx.theme().accents().color_for_index(i as u32),
384 },
385 cx,
386 )
387 }),
388 ),
389 )
390 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
391 })
392 }
393}