miniprofiler_ui: Improve MiniProfiler to use uniform list (#43457)

Jason Lee created

Release Notes:

- N/A

---

- Apply uniform_list for timing list for performance.
- Add paddings for window.
- Add space to `ms`, before: `100ms` after `100 ms`.

## Before 

<img width="1392" height="860" alt="image"
src="https://github.com/user-attachments/assets/9706a96f-7093-4d4f-832f-306948a9b17b"
/>

## After 

<img width="1392" height="864" alt="image"
src="https://github.com/user-attachments/assets/38df1b71-15e7-4101-b0c9-ecdcdb7752d7"
/>

Change summary

crates/gpui/src/elements/uniform_list.rs      |   7 
crates/miniprofiler_ui/src/miniprofiler_ui.rs | 144 +++++++++++---------
2 files changed, 84 insertions(+), 67 deletions(-)

Detailed changes

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -11,7 +11,7 @@ use crate::{
     StyleRefinement, Styled, Window, point, size,
 };
 use smallvec::SmallVec;
-use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc, usize};
 
 use super::ListHorizontalSizingBehavior;
 
@@ -235,6 +235,11 @@ impl UniformListScrollHandle {
             false
         }
     }
+
+    /// Scroll to the bottom of the list.
+    pub fn scroll_to_bottom(&self) {
+        self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom);
+    }
 }
 
 impl Styled for UniformList {

crates/miniprofiler_ui/src/miniprofiler_ui.rs 🔗

@@ -1,21 +1,22 @@
 use std::{
     ops::Range,
     path::PathBuf,
+    rc::Rc,
     time::{Duration, Instant},
 };
 
 use gpui::{
-    App, AppContext, ClipboardItem, Context, Entity, Hsla, InteractiveElement, IntoElement,
-    ParentElement, Render, ScrollHandle, SerializedTaskTiming, SharedString,
-    StatefulInteractiveElement, Styled, Task, TaskTiming, TitlebarOptions, WindowBounds,
-    WindowHandle, WindowOptions, div, prelude::FluentBuilder, px, relative, size,
+    App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
+    ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement,
+    Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle,
+    WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
 };
 use util::ResultExt;
 use workspace::{
     Workspace,
     ui::{
-        ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, ToggleState, Tooltip,
-        WithScrollbar, h_flex, v_flex,
+        ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Divider,
+        ScrollableHandle as _, ToggleState, Tooltip, WithScrollbar, h_flex, v_flex,
     },
 };
 use zed_actions::OpenPerformanceProfiler;
@@ -95,7 +96,7 @@ pub struct ProfilerWindow {
     data: DataMode,
     include_self_timings: ToggleState,
     autoscroll: bool,
-    scroll_handle: ScrollHandle,
+    scroll_handle: UniformListScrollHandle,
     workspace: Option<WindowHandle<Workspace>>,
     _refresh: Option<Task<()>>,
 }
@@ -111,7 +112,7 @@ impl ProfilerWindow {
             data: DataMode::Realtime(None),
             include_self_timings: ToggleState::Unselected,
             autoscroll: true,
-            scroll_handle: ScrollHandle::new(),
+            scroll_handle: UniformListScrollHandle::default(),
             workspace: workspace_handle,
             _refresh: Some(Self::begin_listen(cx)),
         });
@@ -128,16 +129,7 @@ impl ProfilerWindow {
                     .get_current_thread_timings();
 
                 this.update(cx, |this: &mut ProfilerWindow, cx| {
-                    let scroll_offset = this.scroll_handle.offset();
-                    let max_offset = this.scroll_handle.max_offset();
-                    this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
-
                     this.data = DataMode::Realtime(Some(data));
-
-                    if this.autoscroll {
-                        this.scroll_handle.scroll_to_bottom();
-                    }
-
                     cx.notify();
                 })
                 .ok();
@@ -157,12 +149,7 @@ impl ProfilerWindow {
         }
     }
 
-    fn render_timing(
-        &self,
-        value_range: Range<Instant>,
-        item: TimingBar,
-        cx: &App,
-    ) -> impl IntoElement {
+    fn render_timing(value_range: Range<Instant>, item: TimingBar, cx: &App) -> Div {
         let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32;
 
         let remap = value_range
@@ -227,10 +214,10 @@ impl ProfilerWindow {
             )
             .child(
                 div()
-                    .min_w(px(60.0))
+                    .min_w(px(70.))
                     .flex_shrink_0()
                     .text_right()
-                    .child(format!("{:.1}ms", time_ms)),
+                    .child(format!("{:.1} ms", time_ms)),
             )
     }
 }
@@ -241,15 +228,23 @@ impl Render for ProfilerWindow {
         window: &mut gpui::Window,
         cx: &mut gpui::Context<Self>,
     ) -> impl gpui::IntoElement {
+        let scroll_offset = self.scroll_handle.offset();
+        let max_offset = self.scroll_handle.max_offset();
+        self.autoscroll = -scroll_offset.y >= (max_offset.height - px(24.));
+        if self.autoscroll {
+            self.scroll_handle.scroll_to_bottom();
+        }
+
         v_flex()
             .id("profiler")
             .w_full()
             .h_full()
-            .gap_2()
             .bg(cx.theme().colors().surface_background)
             .text_color(cx.theme().colors().text)
             .child(
                 h_flex()
+                    .py_2()
+                    .px_4()
                     .w_full()
                     .justify_between()
                     .child(
@@ -346,53 +341,70 @@ impl Render for ProfilerWindow {
 
                 let min = e[0].start;
                 let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now());
-                div.child(
+                let timings = Rc::new(
+                    e.into_iter()
+                        .filter(|timing| {
+                            timing
+                                .end
+                                .unwrap_or_else(|| Instant::now())
+                                .duration_since(timing.start)
+                                .as_millis()
+                                >= 1
+                        })
+                        .filter(|timing| {
+                            if self.include_self_timings.selected() {
+                                true
+                            } else {
+                                !timing.location.file().ends_with("miniprofiler_ui.rs")
+                            }
+                        })
+                        .cloned()
+                        .collect::<Vec<_>>(),
+                );
+
+                div.child(Divider::horizontal()).child(
                     v_flex()
                         .id("timings.bars")
-                        .overflow_scroll()
                         .w_full()
                         .h_full()
                         .gap_2()
-                        .track_scroll(&self.scroll_handle)
-                        .on_scroll_wheel(cx.listener(|this, _, _, _cx| {
-                            let scroll_offset = this.scroll_handle.offset();
-                            let max_offset = this.scroll_handle.max_offset();
-                            this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
-                        }))
-                        .children(
-                            e.iter()
-                                .filter(|timing| {
-                                    timing
-                                        .end
-                                        .unwrap_or_else(|| Instant::now())
-                                        .duration_since(timing.start)
-                                        .as_millis()
-                                        >= 1
-                                })
-                                .filter(|timing| {
-                                    if self.include_self_timings.selected() {
-                                        true
-                                    } else {
-                                        !timing.location.file().ends_with("miniprofiler_ui.rs")
+                        .child(
+                            uniform_list("list", timings.len(), {
+                                let timings = timings.clone();
+                                move |visible_range, _, cx| {
+                                    let mut items = vec![];
+                                    for i in visible_range {
+                                        let timing = &timings[i];
+                                        let value_range =
+                                            max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
+                                                ..max;
+                                        items.push(Self::render_timing(
+                                            value_range,
+                                            TimingBar {
+                                                location: timing.location,
+                                                start: timing.start,
+                                                end: timing.end.unwrap_or_else(|| Instant::now()),
+                                                color: cx
+                                                    .theme()
+                                                    .accents()
+                                                    .color_for_index(i as u32),
+                                            },
+                                            cx,
+                                        ));
                                     }
-                                })
-                                .enumerate()
-                                .map(|(i, timing)| {
-                                    self.render_timing(
-                                        max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
-                                            ..max,
-                                        TimingBar {
-                                            location: timing.location,
-                                            start: timing.start,
-                                            end: timing.end.unwrap_or_else(|| Instant::now()),
-                                            color: cx.theme().accents().color_for_index(i as u32),
-                                        },
-                                        cx,
-                                    )
-                                }),
-                        ),
+                                    items
+                                }
+                            })
+                            .p_4()
+                            .on_scroll_wheel(cx.listener(|this, _, _, cx| {
+                                this.autoscroll = false;
+                                cx.notify();
+                            }))
+                            .track_scroll(self.scroll_handle.clone())
+                            .size_full(),
+                        )
+                        .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
                 )
-                .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
             })
     }
 }