table: Add measure to table. (#577)

- Improve to not calc extra rows if `stripe` not enable.
- Improve to not change offset, when scrollbar drag changed less than
1px.
- Improve render cells to use `Vec::with_capacity` to prepare enough
space.

```bash
ZED_MEASUREMENTS=1 MTL_HUD_ENABLED=1 cargo run --release --example table
```
This commit is contained in:
Jason Lee 2025-01-24 18:59:01 +08:00 committed by GitHub
parent 86db990b51
commit 1689f4208f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 58 deletions

View file

@ -4,8 +4,8 @@ use std::time::Duration;
use fake::Fake; use fake::Fake;
use gpui::{ use gpui::{
actions, div, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement, actions, div, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement,
IntoElement, ParentElement, Render, RenderOnce, Styled, Subscription, Task, Timer, View, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task,
ViewContext, VisualContext, WindowContext, Timer, View, ViewContext, VisualContext, WindowContext,
}; };
use ui::{ use ui::{
@ -19,22 +19,31 @@ use ui::{
actions!(list_story, [SelectedCompany]); actions!(list_story, [SelectedCompany]);
#[derive(Clone)] #[derive(Clone, Default)]
struct Company { struct Company {
name: String, name: SharedString,
industry: String, industry: SharedString,
last_done: f64, last_done: f64,
prev_close: f64, prev_close: f64,
change_percent: f64,
change_percent_str: SharedString,
last_done_str: SharedString,
prev_close_str: SharedString,
// description: String, // description: String,
} }
impl Company { impl Company {
fn random_update(&mut self) { fn prepare(mut self) -> Self {
self.last_done = self.prev_close * (1.0 + (-0.2..0.2).fake::<f64>()); self.change_percent = (self.last_done - self.prev_close) / self.prev_close;
self.change_percent_str = format!("{:.2}%", self.change_percent).into();
self.last_done_str = format!("{:.2}", self.last_done).into();
self.prev_close_str = format!("{:.2}", self.prev_close).into();
self
} }
fn change_percent(&self) -> f64 { fn random_update(&mut self) {
(self.last_done - self.prev_close) / self.prev_close self.last_done = self.prev_close * (1.0 + (-0.2..0.2).fake::<f64>());
} }
} }
@ -65,7 +74,7 @@ impl RenderOnce for CompanyListItem {
cx.theme().foreground cx.theme().foreground
}; };
let trend_color = match self.company.change_percent() { let trend_color = match self.company.change_percent {
change if change > 0.0 => hsl(0.0, 79.0, 53.0), change if change > 0.0 => hsl(0.0, 79.0, 53.0),
change if change < 0.0 => hsl(100.0, 79.0, 53.0), change if change < 0.0 => hsl(100.0, 79.0, 53.0),
_ => cx.theme().foreground, _ => cx.theme().foreground,
@ -114,7 +123,7 @@ impl RenderOnce for CompanyListItem {
div() div()
.w(px(65.)) .w(px(65.))
.text_color(text_color) .text_color(text_color)
.child(format!("{:.2}", self.company.last_done)), .child(self.company.last_done_str.clone()),
) )
.child( .child(
h_flex().w(px(65.)).justify_end().child( h_flex().w(px(65.)).justify_end().child(
@ -124,7 +133,7 @@ impl RenderOnce for CompanyListItem {
.text_size(px(12.)) .text_size(px(12.))
.px_1() .px_1()
.text_color(trend_color) .text_color(trend_color)
.child(format!("{:.2}%", self.company.change_percent())), .child(self.company.change_percent_str.clone()),
), ),
), ),
), ),
@ -193,8 +202,6 @@ impl ListDelegate for CompanyListDelegate {
} }
fn load_more(&mut self, cx: &mut ViewContext<List<Self>>) { fn load_more(&mut self, cx: &mut ViewContext<List<Self>>) {
self.loading = true;
cx.spawn(|view, mut cx| async move { cx.spawn(|view, mut cx| async move {
// Simulate network request, delay 1s to load data. // Simulate network request, delay 1s to load data.
Timer::after(Duration::from_secs(1)).await; Timer::after(Duration::from_secs(1)).await;
@ -206,7 +213,6 @@ impl ListDelegate for CompanyListDelegate {
.companies .companies
.extend((0..200).map(|_| random_company())); .extend((0..200).map(|_| random_company()));
_ = view.delegate_mut().perform_search(&query, cx); _ = view.delegate_mut().perform_search(&query, cx);
view.delegate_mut().loading = false;
view.delegate_mut().is_eof = view.delegate().companies.len() >= 6000; view.delegate_mut().is_eof = view.delegate().companies.len() >= 6000;
}); });
}) })
@ -325,12 +331,17 @@ impl ListStory {
fn random_company() -> Company { fn random_company() -> Company {
let last_done = (0.0..999.0).fake::<f64>(); let last_done = (0.0..999.0).fake::<f64>();
let prev_close = last_done * (-0.1..0.1).fake::<f64>(); let prev_close = last_done * (-0.1..0.1).fake::<f64>();
Company { Company {
name: fake::faker::company::en::CompanyName().fake(), name: fake::faker::company::en::CompanyName()
industry: fake::faker::company::en::Industry().fake(), .fake::<String>()
.into(),
industry: fake::faker::company::en::Industry().fake::<String>().into(),
last_done, last_done,
prev_close, prev_close,
..Default::default()
} }
.prepare()
} }
impl FocusableView for ListStory { impl FocusableView for ListStory {

View file

@ -373,6 +373,14 @@ impl TableDelegate for StockTableDelegate {
.menu("Size XSmall", Box::new(ChangeSize(Size::XSmall))) .menu("Size XSmall", Box::new(ChangeSize(Size::XSmall)))
} }
/// NOTE: Performance metrics
///
/// last render 561 cells total: 232.745µs, avg: 414ns
/// frame duration: 8.825083ms
///
/// This is means render the full table cells takes 232.745µs. Then 232.745µs / 8.82ms = 2.6% of the frame duration.
///
/// If we improve the td rendering, we can reduce the time to render the full table cells.
fn render_td( fn render_td(
&self, &self,
row_ix: usize, row_ix: usize,
@ -578,8 +586,7 @@ impl TableStory {
// Spawn a background to random refresh the list // Spawn a background to random refresh the list
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
loop { loop {
let delay = (80..150).fake::<u64>(); Timer::after(time::Duration::from_millis(33)).await;
Timer::after(time::Duration::from_millis(delay)).await;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if !this.refresh_data { if !this.refresh_data {

View file

@ -107,3 +107,8 @@ pub fn locale() -> impl Deref<Target = str> {
pub fn set_locale(locale: &str) { pub fn set_locale(locale: &str) {
rust_i18n::set_locale(locale) rust_i18n::set_locale(locale)
} }
#[inline]
pub(crate) fn measure_enable() -> bool {
std::env::var("ZED_MEASUREMENTS").is_ok()
}

View file

@ -760,8 +760,12 @@ impl Element for Scrollbar {
) )
}; };
scroll_handle.set_offset(offset); if (scroll_handle.offset().y - offset.y).abs() > px(1.)
cx.notify(Some(view_id)); || (scroll_handle.offset().x - offset.x).abs() > px(1.)
{
scroll_handle.set_offset(offset);
cx.notify(Some(view_id));
}
} }
} }
}); });

View file

@ -1,4 +1,4 @@
use std::{cell::Cell, ops::Range, rc::Rc}; use std::{cell::Cell, ops::Range, rc::Rc, time::Duration};
use crate::{ use crate::{
context_menu::ContextMenuExt, context_menu::ContextMenuExt,
@ -172,6 +172,7 @@ pub struct Table<D: TableDelegate> {
/// The visible range of the rows and columns. /// The visible range of the rows and columns.
visible_range: VisibleRangeState, visible_range: VisibleRangeState,
_measure: Vec<Duration>,
_load_more_task: Task<()>, _load_more_task: Task<()>,
} }
@ -374,6 +375,7 @@ where
scrollbar_visible: Edges::all(true), scrollbar_visible: Edges::all(true),
visible_range: VisibleRangeState::default(), visible_range: VisibleRangeState::default(),
_load_more_task: Task::ready(()), _load_more_task: Task::ready(()),
_measure: Vec::new(),
}; };
this.prepare_col_groups(cx); this.prepare_col_groups(cx);
@ -1140,12 +1142,20 @@ where
.h_full() .h_full()
.border_r_1() .border_r_1()
.border_color(cx.theme().table_row_border) .border_color(cx.theme().table_row_border)
.children((0..left_cols_count).map(|col_ix| { .children({
self.render_col_wrap(col_ix, cx).child( let mut items = Vec::with_capacity(left_cols_count);
self.render_cell(col_ix, cx)
.child(self.delegate.render_td(row_ix, col_ix, cx)), (0..left_cols_count).for_each(|col_ix| {
) items.push(
})), self.render_col_wrap(col_ix, cx).child(
self.render_cell(col_ix, cx)
.child(self.measure_render_td(row_ix, col_ix, cx)),
),
);
});
items
}),
) )
} else { } else {
None None
@ -1170,18 +1180,22 @@ where
cx, cx,
); );
visible_range let mut items = Vec::with_capacity(
.map(|col_ix| { visible_range.end - visible_range.start,
let col_ix = col_ix + left_cols_count; );
table.render_col_wrap(col_ix, cx).child(
table.render_cell(col_ix, cx).child( visible_range.for_each(|col_ix| {
table let col_ix = col_ix + left_cols_count;
.delegate let el = table.render_col_wrap(col_ix, cx).child(
.render_td(row_ix, col_ix, cx), table.render_cell(col_ix, cx).child(
), table.measure_render_td(row_ix, col_ix, cx),
) ),
}) );
.collect::<Vec<_>>()
items.push(el);
});
items
} }
}, },
) )
@ -1250,6 +1264,48 @@ where
.child(self.delegate.render_last_empty_col(cx)) .child(self.delegate.render_last_empty_col(cx))
} }
} }
#[inline]
fn measure_render_td(
&mut self,
row_ix: usize,
col_ix: usize,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
if !crate::measure_enable() {
return self
.delegate
.render_td(row_ix, col_ix, cx)
.into_any_element();
}
let start = std::time::Instant::now();
let el = self.delegate.render_td(row_ix, col_ix, cx);
self._measure.push(start.elapsed());
el.into_any_element()
}
fn measure(&mut self, _: &mut ViewContext<Self>) {
if !crate::measure_enable() {
return;
}
// Print avg measure time of each td
if self._measure.len() > 0 {
let total = self
._measure
.iter()
.fold(Duration::default(), |acc, d| acc + *d);
let avg = total / self._measure.len() as u32;
eprintln!(
"last render {} cells total: {:?}, avg: {:?}",
self._measure.len(),
total,
avg,
);
}
self._measure.clear();
}
} }
impl<D> Sizable for Table<D> impl<D> Sizable for Table<D>
@ -1276,6 +1332,8 @@ where
D: TableDelegate, D: TableDelegate,
{ {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.measure(cx);
let view = cx.view().clone(); let view = cx.view().clone();
let vertical_scroll_handle = self.vertical_scroll_handle.clone(); let vertical_scroll_handle = self.vertical_scroll_handle.clone();
let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
@ -1301,12 +1359,14 @@ where
// Calculate the extra rows needed to fill the table for stripe style. // Calculate the extra rows needed to fill the table for stripe style.
let mut extra_rows_needed = 0; let mut extra_rows_needed = 0;
if let Some(row_height) = row_height { if self.stripe {
if row_height > px(0.) { if let Some(row_height) = row_height {
let actual_height = row_height * rows_count as f32; if row_height > px(0.) {
let remaining_height = total_height - actual_height; let actual_height = row_height * rows_count as f32;
if remaining_height > px(0.) { let remaining_height = total_height - actual_height;
extra_rows_needed = (remaining_height / row_height).ceil() as usize; if remaining_height > px(0.) {
extra_rows_needed = (remaining_height / row_height).ceil() as usize;
}
} }
} }
} }
@ -1359,19 +1419,23 @@ where
); );
} }
let mut items = Vec::with_capacity(
visible_range.end - visible_range.start,
);
// Render fake rows to fill the table // Render fake rows to fill the table
visible_range visible_range.for_each(|row_ix| {
.map(|row_ix| { // Render real rows for available data
// Render real rows for available data items.push(table.render_table_row(
table.render_table_row( row_ix,
row_ix, rows_count,
rows_count, left_cols_count,
left_cols_count, cols_count,
cols_count, cx,
cx, ));
) });
})
.collect::<Vec<_>>() items
} }
}, },
) )