mirror of
https://github.com/ClementTsang/bottom
synced 2024-11-22 12:13:06 +00:00
other: slightly reduce the CPU time spent for draws (#918)
* other: group all dataset draws in a time chart We used to draw each data set separately as a new canvas. Now, in one canvas, we draw all datasets. Note that this changes how dataset lines are drawn - rather than drawing one on top of another, it now draws kinda all at once. This effect is *kinda* a bit better IMO, but it might also look a bit more cluttered. * other: optimize truncate_text Flamegraphs showed that this area seems to be a bit heavy at times with some inefficient use of iterators and collection. This change should hopefully optimize this a bit by reducing some collections or reallocations. There can also be some further optimizations with less allocations from callers. * Reduce some redundant draws
This commit is contained in:
parent
913c9ed5c6
commit
9c3e60e74f
12 changed files with 118 additions and 72 deletions
|
@ -41,6 +41,8 @@ doctest = true
|
|||
doc = true
|
||||
|
||||
[profile.release]
|
||||
# debug = true
|
||||
# strip = false
|
||||
debug = 0
|
||||
strip = "symbols"
|
||||
lto = true
|
||||
|
|
|
@ -32,6 +32,7 @@ pub async fn get_ram_data() -> crate::utils::error::Result<Option<MemHarvest>> {
|
|||
let (mem_total_in_kib, mem_used_in_kib) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// TODO: [OPT] is this efficient?
|
||||
use smol::fs::read_to_string;
|
||||
let meminfo = read_to_string("/proc/meminfo").await?;
|
||||
|
||||
|
|
|
@ -73,11 +73,9 @@ impl Default for CanvasColours {
|
|||
Style::default().fg(Color::LightCyan),
|
||||
Style::default().fg(Color::LightGreen),
|
||||
Style::default().fg(Color::LightBlue),
|
||||
Style::default().fg(Color::LightRed),
|
||||
Style::default().fg(Color::Cyan),
|
||||
Style::default().fg(Color::Green),
|
||||
Style::default().fg(Color::Blue),
|
||||
Style::default().fg(Color::Red),
|
||||
],
|
||||
border_style: Style::default().fg(text_colour),
|
||||
highlighted_border_style: Style::default().fg(HIGHLIGHT_COLOUR),
|
||||
|
|
|
@ -4,6 +4,7 @@ use concat_string::concat_string;
|
|||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
symbols::Marker,
|
||||
terminal::Frame,
|
||||
};
|
||||
|
||||
|
@ -209,8 +210,13 @@ impl Painter {
|
|||
" CPU ".into()
|
||||
};
|
||||
|
||||
let marker = if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
};
|
||||
|
||||
TimeGraph {
|
||||
use_dot: app_state.app_config_fields.use_dot,
|
||||
x_bounds,
|
||||
hide_x_labels,
|
||||
y_bounds: Y_BOUNDS,
|
||||
|
@ -221,6 +227,7 @@ impl Painter {
|
|||
is_expanded: app_state.is_expanded,
|
||||
title_style: self.colours.widget_title_style,
|
||||
legend_constraints: None,
|
||||
marker,
|
||||
}
|
||||
.draw_time_graph(f, draw_loc, &points);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::borrow::Cow;
|
|||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Rect},
|
||||
symbols::Marker,
|
||||
terminal::Frame,
|
||||
};
|
||||
|
||||
|
@ -104,8 +105,13 @@ impl Painter {
|
|||
points
|
||||
};
|
||||
|
||||
let marker = if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
};
|
||||
|
||||
TimeGraph {
|
||||
use_dot: app_state.app_config_fields.use_dot,
|
||||
x_bounds,
|
||||
hide_x_labels,
|
||||
y_bounds: Y_BOUNDS,
|
||||
|
@ -116,6 +122,7 @@ impl Painter {
|
|||
is_expanded: app_state.is_expanded,
|
||||
title_style: self.colours.widget_title_style,
|
||||
legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
|
||||
marker,
|
||||
}
|
||||
.draw_time_graph(f, draw_loc, &points);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
symbols::Marker,
|
||||
terminal::Frame,
|
||||
text::Text,
|
||||
widgets::{Block, Borders, Row, Table},
|
||||
|
@ -142,8 +143,13 @@ impl Painter {
|
|||
]
|
||||
};
|
||||
|
||||
let marker = if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
};
|
||||
|
||||
TimeGraph {
|
||||
use_dot: app_state.app_config_fields.use_dot,
|
||||
x_bounds,
|
||||
hide_x_labels,
|
||||
y_bounds,
|
||||
|
@ -154,6 +160,7 @@ impl Painter {
|
|||
is_expanded: app_state.is_expanded,
|
||||
title_style: self.colours.widget_title_style,
|
||||
legend_constraints: Some(legend_constraints),
|
||||
marker,
|
||||
}
|
||||
.draw_time_graph(f, draw_loc, &points);
|
||||
}
|
||||
|
|
|
@ -221,7 +221,11 @@ where
|
|||
.iter()
|
||||
.zip(&self.state.calculated_widths)
|
||||
.filter_map(|(column, &width)| {
|
||||
data_row.to_cell(column.inner(), width)
|
||||
if width > 0 {
|
||||
data_row.to_cell(column.inner(), width)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -22,9 +22,6 @@ pub struct GraphData<'a> {
|
|||
}
|
||||
|
||||
pub struct TimeGraph<'a> {
|
||||
/// Whether to use a dot marker over the default braille markers.
|
||||
pub use_dot: bool,
|
||||
|
||||
/// The min and max x boundaries. Expects a f64 representing the time range in milliseconds.
|
||||
pub x_bounds: [u64; 2],
|
||||
|
||||
|
@ -54,6 +51,10 @@ pub struct TimeGraph<'a> {
|
|||
|
||||
/// Any legend constraints.
|
||||
pub legend_constraints: Option<(Constraint, Constraint)>,
|
||||
|
||||
/// The marker type. Unlike tui-rs' native charts, we assume
|
||||
/// only a single type of market.
|
||||
pub marker: Marker,
|
||||
}
|
||||
|
||||
impl<'a> TimeGraph<'a> {
|
||||
|
@ -131,18 +132,7 @@ impl<'a> TimeGraph<'a> {
|
|||
|
||||
// This is some ugly manual loop unswitching. Maybe unnecessary.
|
||||
// TODO: Optimize this step. Cut out unneeded points.
|
||||
let data = if self.use_dot {
|
||||
graph_data
|
||||
.iter()
|
||||
.map(|data| create_dataset(data, Marker::Dot))
|
||||
.collect()
|
||||
} else {
|
||||
graph_data
|
||||
.iter()
|
||||
.map(|data| create_dataset(data, Marker::Braille))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let data = graph_data.iter().map(create_dataset).collect();
|
||||
let block = Block::default()
|
||||
.title(self.generate_title(draw_loc))
|
||||
.borders(Borders::ALL)
|
||||
|
@ -164,7 +154,7 @@ impl<'a> TimeGraph<'a> {
|
|||
}
|
||||
|
||||
/// Creates a new [`Dataset`].
|
||||
fn create_dataset<'a>(data: &'a GraphData<'a>, marker: Marker) -> Dataset<'a> {
|
||||
fn create_dataset<'a>(data: &'a GraphData<'a>) -> Dataset<'a> {
|
||||
let GraphData {
|
||||
points,
|
||||
style,
|
||||
|
@ -174,8 +164,7 @@ fn create_dataset<'a>(data: &'a GraphData<'a>, marker: Marker) -> Dataset<'a> {
|
|||
let dataset = Dataset::default()
|
||||
.style(*style)
|
||||
.data(points)
|
||||
.graph_type(GraphType::Line)
|
||||
.marker(marker);
|
||||
.graph_type(GraphType::Line);
|
||||
|
||||
if let Some(name) = name {
|
||||
dataset.name(name.as_ref())
|
||||
|
@ -191,6 +180,7 @@ mod test {
|
|||
use tui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols::Marker,
|
||||
text::{Span, Spans},
|
||||
};
|
||||
|
||||
|
@ -206,7 +196,6 @@ mod test {
|
|||
fn create_time_graph() -> TimeGraph<'static> {
|
||||
TimeGraph {
|
||||
title: " Network ".into(),
|
||||
use_dot: true,
|
||||
x_bounds: [0, 15000],
|
||||
hide_x_labels: false,
|
||||
y_bounds: [0.0, 100.5],
|
||||
|
@ -216,6 +205,7 @@ mod test {
|
|||
is_expanded: false,
|
||||
title_style: Style::default().fg(Color::Cyan),
|
||||
legend_constraints: None,
|
||||
marker: Marker::Braille,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use tui::{
|
|||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
symbols::{self, Marker},
|
||||
text::{Span, Spans},
|
||||
widgets::{
|
||||
canvas::{Canvas, Line, Points},
|
||||
|
@ -75,8 +75,6 @@ pub struct Dataset<'a> {
|
|||
name: Cow<'a, str>,
|
||||
/// A reference to the actual data
|
||||
data: &'a [Point],
|
||||
/// Symbol used for each points of this dataset
|
||||
marker: symbols::Marker,
|
||||
/// Determines graph type used for drawing points
|
||||
graph_type: GraphType,
|
||||
/// Style used to plot this dataset
|
||||
|
@ -88,7 +86,6 @@ impl<'a> Default for Dataset<'a> {
|
|||
Dataset {
|
||||
name: Cow::from(""),
|
||||
data: &[],
|
||||
marker: symbols::Marker::Dot,
|
||||
graph_type: GraphType::Scatter,
|
||||
style: Style::default(),
|
||||
}
|
||||
|
@ -110,11 +107,6 @@ impl<'a> Dataset<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||
self.graph_type = graph_type;
|
||||
self
|
||||
|
@ -173,10 +165,12 @@ pub struct TimeChart<'a> {
|
|||
legend_style: Style,
|
||||
/// Constraints used to determine whether the legend should be shown or not
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
/// The marker type.
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
pub const DEFAULT_LEGEND_CONSTRAINTS: (Constraint, Constraint) =
|
||||
(Constraint::Ratio(1, 4), Constraint::Ratio(1, 4));
|
||||
(Constraint::Ratio(1, 4), Constraint::Length(4));
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<'a> TimeChart<'a> {
|
||||
|
@ -192,6 +186,7 @@ impl<'a> TimeChart<'a> {
|
|||
legend_style: Default::default(),
|
||||
datasets,
|
||||
hidden_legend_constraints: DEFAULT_LEGEND_CONSTRAINTS,
|
||||
marker: Marker::Braille,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,6 +215,11 @@ impl<'a> TimeChart<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn marker(mut self, marker: Marker) -> TimeChart<'a> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||
pub fn hidden_legend_constraints(
|
||||
mut self, constraints: (Constraint, Constraint),
|
||||
|
@ -422,14 +422,14 @@ impl<'a> Widget for TimeChart<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Probably better to move the dataset inside.
|
||||
for dataset in &self.datasets {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.marker(dataset.marker)
|
||||
.paint(|ctx| {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.paint(|ctx| {
|
||||
for dataset in &self.datasets {
|
||||
let color = dataset.style.fg.unwrap_or(Color::Reset);
|
||||
|
||||
let start_bound = self.x_axis.bounds[0];
|
||||
let end_bound = self.x_axis.bounds[1];
|
||||
|
||||
|
@ -438,11 +438,6 @@ impl<'a> Widget for TimeChart<'a> {
|
|||
|
||||
let data_slice = &dataset.data[start_index..end_index];
|
||||
|
||||
ctx.draw(&Points {
|
||||
coords: data_slice,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
|
||||
if let Some(interpolate_start) = interpolate_start {
|
||||
if let (Some(older_point), Some(newer_point)) = (
|
||||
dataset.data.get(interpolate_start),
|
||||
|
@ -453,18 +448,18 @@ impl<'a> Widget for TimeChart<'a> {
|
|||
interpolate_point(older_point, newer_point, self.x_axis.bounds[0]),
|
||||
);
|
||||
|
||||
ctx.draw(&Points {
|
||||
coords: &[interpolated_point],
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
|
||||
if let GraphType::Line = dataset.graph_type {
|
||||
ctx.draw(&Line {
|
||||
x1: interpolated_point.0,
|
||||
y1: interpolated_point.1,
|
||||
x2: newer_point.0,
|
||||
y2: newer_point.1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
} else {
|
||||
ctx.draw(&Points {
|
||||
coords: &[interpolated_point],
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -477,9 +472,14 @@ impl<'a> Widget for TimeChart<'a> {
|
|||
y1: data[0].1,
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ctx.draw(&Points {
|
||||
coords: data_slice,
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(interpolate_end) = interpolate_end {
|
||||
|
@ -492,25 +492,25 @@ impl<'a> Widget for TimeChart<'a> {
|
|||
interpolate_point(older_point, newer_point, self.x_axis.bounds[1]),
|
||||
);
|
||||
|
||||
ctx.draw(&Points {
|
||||
coords: &[interpolated_point],
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
});
|
||||
|
||||
if let GraphType::Line = dataset.graph_type {
|
||||
ctx.draw(&Line {
|
||||
x1: older_point.0,
|
||||
y1: older_point.1,
|
||||
x2: interpolated_point.0,
|
||||
y2: interpolated_point.1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
} else {
|
||||
ctx.draw(&Points {
|
||||
coords: &[interpolated_point],
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
|
|
|
@ -522,6 +522,8 @@ pub fn create_collection_thread(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [OPT] this feels like it might not be totally optimal. Hm.
|
||||
futures::executor::block_on(data_state.update_data());
|
||||
|
||||
// Yet another check to bail if needed...
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::text::Text;
|
||||
use tui::text::{Span, Spans, Text};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub const KILO_LIMIT: u64 = 1000;
|
||||
|
@ -99,14 +98,37 @@ pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
|||
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
|
||||
pub fn truncate_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {
|
||||
let width = width.into();
|
||||
let graphemes: Vec<&str> = UnicodeSegmentation::graphemes(content, true).collect();
|
||||
let mut graphemes = UnicodeSegmentation::graphemes(content, true);
|
||||
let grapheme_len = {
|
||||
let (_, upper) = graphemes.size_hint();
|
||||
match upper {
|
||||
Some(upper) => upper,
|
||||
None => graphemes.clone().count(), // Don't think this ever fires.
|
||||
}
|
||||
};
|
||||
|
||||
if graphemes.len() > width && width > 0 {
|
||||
// Truncate with ellipsis
|
||||
let first_n = graphemes[..(width - 1)].concat();
|
||||
Text::raw(concat_string!(first_n, "…"))
|
||||
let text = if grapheme_len > width {
|
||||
let mut text = String::with_capacity(width);
|
||||
// Truncate with ellipsis.
|
||||
|
||||
// Use a hack to reduce the size to size `width`. Think of it like removing
|
||||
// The last `grapheme_len - width` graphemes, which reduces the length to
|
||||
// `width` long.
|
||||
//
|
||||
// This is a way to get around the currently experimental`advance_back_by`.
|
||||
graphemes.nth_back(grapheme_len - width);
|
||||
|
||||
text.push_str(graphemes.as_str());
|
||||
text.push('…');
|
||||
|
||||
text
|
||||
} else {
|
||||
Text::raw(content.to_string())
|
||||
content.to_string()
|
||||
};
|
||||
|
||||
// TODO: [OPT] maybe add interning here?
|
||||
Text {
|
||||
lines: vec![Spans(vec![Span::raw(text)])],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,4 +178,9 @@ mod test {
|
|||
y.sort_by(|a, b| sort_partial_fn(true)(a, b));
|
||||
assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncation() {
|
||||
// TODO: Add tests for `truncate_text`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,6 +221,7 @@ impl ProcWidgetData {
|
|||
|
||||
impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
fn to_cell<'a>(&'a self, column: &ProcColumn, calculated_width: u16) -> Option<Text<'a>> {
|
||||
// TODO: Optimize the string allocations here...
|
||||
Some(truncate_text(
|
||||
&match column {
|
||||
ProcColumn::CpuPercent => {
|
||||
|
|
Loading…
Reference in a new issue