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

View file

@ -373,6 +373,14 @@ impl TableDelegate for StockTableDelegate {
.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(
&self,
row_ix: usize,
@ -578,8 +586,7 @@ impl TableStory {
// Spawn a background to random refresh the list
cx.spawn(move |this, mut cx| async move {
loop {
let delay = (80..150).fake::<u64>();
Timer::after(time::Duration::from_millis(delay)).await;
Timer::after(time::Duration::from_millis(33)).await;
this.update(&mut cx, |this, cx| {
if !this.refresh_data {

View file

@ -107,3 +107,8 @@ pub fn locale() -> impl Deref<Target = str> {
pub fn set_locale(locale: &str) {
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);
cx.notify(Some(view_id));
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|| (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::{
context_menu::ContextMenuExt,
@ -172,6 +172,7 @@ pub struct Table<D: TableDelegate> {
/// The visible range of the rows and columns.
visible_range: VisibleRangeState,
_measure: Vec<Duration>,
_load_more_task: Task<()>,
}
@ -374,6 +375,7 @@ where
scrollbar_visible: Edges::all(true),
visible_range: VisibleRangeState::default(),
_load_more_task: Task::ready(()),
_measure: Vec::new(),
};
this.prepare_col_groups(cx);
@ -1140,12 +1142,20 @@ where
.h_full()
.border_r_1()
.border_color(cx.theme().table_row_border)
.children((0..left_cols_count).map(|col_ix| {
self.render_col_wrap(col_ix, cx).child(
self.render_cell(col_ix, cx)
.child(self.delegate.render_td(row_ix, col_ix, cx)),
)
})),
.children({
let mut items = Vec::with_capacity(left_cols_count);
(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 {
None
@ -1170,18 +1180,22 @@ where
cx,
);
visible_range
.map(|col_ix| {
let col_ix = col_ix + left_cols_count;
table.render_col_wrap(col_ix, cx).child(
table.render_cell(col_ix, cx).child(
table
.delegate
.render_td(row_ix, col_ix, cx),
),
)
})
.collect::<Vec<_>>()
let mut items = Vec::with_capacity(
visible_range.end - visible_range.start,
);
visible_range.for_each(|col_ix| {
let col_ix = col_ix + left_cols_count;
let el = table.render_col_wrap(col_ix, cx).child(
table.render_cell(col_ix, cx).child(
table.measure_render_td(row_ix, col_ix, cx),
),
);
items.push(el);
});
items
}
},
)
@ -1250,6 +1264,48 @@ where
.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>
@ -1276,6 +1332,8 @@ where
D: TableDelegate,
{
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.measure(cx);
let view = cx.view().clone();
let vertical_scroll_handle = self.vertical_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.
let mut extra_rows_needed = 0;
if let Some(row_height) = row_height {
if row_height > px(0.) {
let actual_height = row_height * rows_count as f32;
let remaining_height = total_height - actual_height;
if remaining_height > px(0.) {
extra_rows_needed = (remaining_height / row_height).ceil() as usize;
if self.stripe {
if let Some(row_height) = row_height {
if row_height > px(0.) {
let actual_height = row_height * rows_count as f32;
let remaining_height = total_height - actual_height;
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
visible_range
.map(|row_ix| {
// Render real rows for available data
table.render_table_row(
row_ix,
rows_count,
left_cols_count,
cols_count,
cx,
)
})
.collect::<Vec<_>>()
visible_range.for_each(|row_ix| {
// Render real rows for available data
items.push(table.render_table_row(
row_ix,
rows_count,
left_cols_count,
cols_count,
cx,
));
});
items
}
},
)