miniprofiler_ui.rs

  1use std::{
  2    ops::Range,
  3    path::PathBuf,
  4    time::{Duration, Instant},
  5};
  6
  7use gpui::{
  8    App, AppContext, ClipboardItem, Context, Entity, Hsla, InteractiveElement, IntoElement,
  9    ParentElement, Render, ScrollHandle, SerializedTaskTiming, SharedString,
 10    StatefulInteractiveElement, Styled, Task, TaskTiming, TitlebarOptions, WindowBounds,
 11    WindowHandle, WindowOptions, div, prelude::FluentBuilder, px, relative, size,
 12};
 13use util::ResultExt;
 14use workspace::{
 15    Workspace,
 16    ui::{
 17        ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, ToggleState, Tooltip,
 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 = SharedString::from(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                    .id(label.clone())
201                    .w(px(200.0))
202                    .flex_shrink_0()
203                    .overflow_hidden()
204                    .child(div().text_ellipsis().child(label.clone()))
205                    .tooltip(Tooltip::text(label.clone()))
206                    .on_click(move |_, _, cx| {
207                        cx.write_to_clipboard(ClipboardItem::new_string(label.to_string()))
208                    }),
209            )
210            .child(
211                div()
212                    .flex_1()
213                    .h(px(24.0))
214                    .bg(cx.theme().colors().background)
215                    .rounded_md()
216                    .p(px(2.0))
217                    .relative()
218                    .child(
219                        div()
220                            .absolute()
221                            .h_full()
222                            .rounded_sm()
223                            .bg(item.color)
224                            .left(relative(start.max(0f32)))
225                            .w(relative(bar_width)),
226                    ),
227            )
228            .child(
229                div()
230                    .min_w(px(60.0))
231                    .flex_shrink_0()
232                    .text_right()
233                    .child(format!("{:.1}ms", time_ms)),
234            )
235    }
236}
237
238impl Render for ProfilerWindow {
239    fn render(
240        &mut self,
241        window: &mut gpui::Window,
242        cx: &mut gpui::Context<Self>,
243    ) -> impl gpui::IntoElement {
244        v_flex()
245            .id("profiler")
246            .w_full()
247            .h_full()
248            .gap_2()
249            .bg(cx.theme().colors().surface_background)
250            .text_color(cx.theme().colors().text)
251            .child(
252                h_flex()
253                    .w_full()
254                    .justify_between()
255                    .child(
256                        h_flex()
257                            .gap_2()
258                            .child(
259                                Button::new(
260                                    "switch-mode",
261                                    match self.data {
262                                        DataMode::Snapshot { .. } => "Resume",
263                                        DataMode::Realtime(_) => "Pause",
264                                    },
265                                )
266                                .style(ButtonStyle::Filled)
267                                .on_click(cx.listener(
268                                    |this, _, _window, cx| {
269                                        match &this.data {
270                                            DataMode::Realtime(Some(data)) => {
271                                                this._refresh = None;
272                                                this.data = DataMode::Snapshot(data.clone());
273                                            }
274                                            DataMode::Snapshot { .. } => {
275                                                this._refresh = Some(Self::begin_listen(cx));
276                                                this.data = DataMode::Realtime(None);
277                                            }
278                                            _ => {}
279                                        };
280                                        cx.notify();
281                                    },
282                                )),
283                            )
284                            .child(
285                                Button::new("export-data", "Save")
286                                    .style(ButtonStyle::Filled)
287                                    .on_click(cx.listener(|this, _, _window, cx| {
288                                        let Some(workspace) = this.workspace else {
289                                            return;
290                                        };
291
292                                        let Some(data) = this.get_timings() else {
293                                            return;
294                                        };
295                                        let timings =
296                                            SerializedTaskTiming::convert(this.startup_time, &data);
297
298                                        let active_path = workspace
299                                            .read_with(cx, |workspace, cx| {
300                                                workspace.most_recent_active_path(cx)
301                                            })
302                                            .log_err()
303                                            .flatten()
304                                            .and_then(|p| p.parent().map(|p| p.to_owned()))
305                                            .unwrap_or_else(|| PathBuf::default());
306
307                                        let path = cx.prompt_for_new_path(
308                                            &active_path,
309                                            Some("performance_profile.miniprof"),
310                                        );
311
312                                        cx.background_spawn(async move {
313                                            let path = path.await;
314                                            let path =
315                                                path.log_err().and_then(|p| p.log_err()).flatten();
316
317                                            let Some(path) = path else {
318                                                return;
319                                            };
320
321                                            let Some(timings) =
322                                                serde_json::to_string(&timings).log_err()
323                                            else {
324                                                return;
325                                            };
326
327                                            smol::fs::write(path, &timings).await.log_err();
328                                        })
329                                        .detach();
330                                    })),
331                            ),
332                    )
333                    .child(
334                        Checkbox::new("include-self", self.include_self_timings)
335                            .label("Include profiler timings")
336                            .on_click(cx.listener(|this, checked, _window, cx| {
337                                this.include_self_timings = *checked;
338                                cx.notify();
339                            })),
340                    ),
341            )
342            .when_some(self.get_timings(), |div, e| {
343                if e.len() == 0 {
344                    return div;
345                }
346
347                let min = e[0].start;
348                let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now());
349                div.child(
350                    v_flex()
351                        .id("timings.bars")
352                        .overflow_scroll()
353                        .w_full()
354                        .h_full()
355                        .gap_2()
356                        .track_scroll(&self.scroll_handle)
357                        .on_scroll_wheel(cx.listener(|this, _, _, _cx| {
358                            let scroll_offset = this.scroll_handle.offset();
359                            let max_offset = this.scroll_handle.max_offset();
360                            this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
361                        }))
362                        .children(
363                            e.iter()
364                                .filter(|timing| {
365                                    timing
366                                        .end
367                                        .unwrap_or_else(|| Instant::now())
368                                        .duration_since(timing.start)
369                                        .as_millis()
370                                        >= 1
371                                })
372                                .filter(|timing| {
373                                    if self.include_self_timings.selected() {
374                                        true
375                                    } else {
376                                        !timing.location.file().ends_with("miniprofiler_ui.rs")
377                                    }
378                                })
379                                .enumerate()
380                                .map(|(i, timing)| {
381                                    self.render_timing(
382                                        max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
383                                            ..max,
384                                        TimingBar {
385                                            location: timing.location,
386                                            start: timing.start,
387                                            end: timing.end.unwrap_or_else(|| Instant::now()),
388                                            color: cx.theme().accents().color_for_index(i as u32),
389                                        },
390                                        cx,
391                                    )
392                                }),
393                        ),
394                )
395                .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
396            })
397    }
398}