refactor: unify some time series graph logic, part 1 (#2050)

Part 1 of a series of PRs to unify and clean up time series graph logic. This PR mostly just unifies the zoom logic and common states.
This commit is contained in:
Clement Tsang
2026-05-04 06:54:07 +00:00
committed by GitHub
parent 797129156d
commit 9aa3e24b9a
25 changed files with 514 additions and 446 deletions
+123 -200
View File
@@ -13,7 +13,7 @@ pub use states::*;
use crate::{
canvas::{
components::time_graph::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog,
components::time_series::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog,
},
constants,
options::config::flags::TableGap,
@@ -23,8 +23,6 @@ use crate::{
},
};
const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds
#[derive(Debug, Clone, Eq, PartialEq, Default, Copy)]
pub enum AxisScaling {
#[default]
@@ -194,10 +192,35 @@ impl App {
self.data_store.reset();
// Reset zoom
self.reset_cpu_zoom();
self.reset_mem_zoom();
self.reset_net_zoom();
// Reset zoom.
// TODO: Make this suck less... should just make it so that calling reset fixes this all (including above too).
for widget_state in self.states.cpu_state.widget_states.values_mut() {
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
for widget_state in self.states.mem_state.widget_states.values_mut() {
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
for widget_state in self.states.net_state.widget_states.values_mut() {
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
for widget_state in self.states.temp_graph_state.widget_states.values_mut() {
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
}
pub fn should_get_widget_bounds(&self) -> bool {
@@ -1966,81 +1989,45 @@ impl App {
fn zoom_out(&mut self) {
match self.current_widget.widget_type {
BottomWidgetType::Cpu => {
if let Some(cpu_widget_state) = self
if let Some(widget_state) = self
.states
.cpu_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = cpu_widget_state
.current_display_time
.saturating_add(self.app_config_fields.time_interval);
if new_time <= self.app_config_fields.retention_ms {
cpu_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
cpu_widget_state.autohide_timer = Some(Instant::now());
}
} else if cpu_widget_state.current_display_time
!= self.app_config_fields.retention_ms
{
cpu_widget_state.current_display_time = self.app_config_fields.retention_ms;
if self.app_config_fields.autohide_time {
cpu_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_out(
self.app_config_fields.time_interval,
self.app_config_fields.retention_ms,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Mem => {
if let Some(mem_widget_state) = self
if let Some(widget_state) = self
.states
.mem_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = mem_widget_state
.current_display_time
.saturating_add(self.app_config_fields.time_interval);
if new_time <= self.app_config_fields.retention_ms {
mem_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
mem_widget_state.autohide_timer = Some(Instant::now());
}
} else if mem_widget_state.current_display_time
!= self.app_config_fields.retention_ms
{
mem_widget_state.current_display_time = self.app_config_fields.retention_ms;
if self.app_config_fields.autohide_time {
mem_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_out(
self.app_config_fields.time_interval,
self.app_config_fields.retention_ms,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Net => {
if let Some(net_widget_state) = self
if let Some(widget_state) = self
.states
.net_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = net_widget_state
.current_display_time
.saturating_add(self.app_config_fields.time_interval);
if new_time <= self.app_config_fields.retention_ms {
net_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
net_widget_state.autohide_timer = Some(Instant::now());
}
} else if net_widget_state.current_display_time
!= self.app_config_fields.retention_ms
{
net_widget_state.current_display_time = self.app_config_fields.retention_ms;
if self.app_config_fields.autohide_time {
net_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_out(
self.app_config_fields.time_interval,
self.app_config_fields.retention_ms,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::TempGraph => {
@@ -2049,23 +2036,11 @@ impl App {
.temp_graph_state
.get_mut_widget_state(self.current_widget.widget_id)
{
let new_time = widget_state
.current_display_time
.saturating_add(self.app_config_fields.time_interval);
if new_time <= self.app_config_fields.retention_ms {
widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
widget_state.autohide_timer = Some(Instant::now());
}
} else if widget_state.current_display_time
!= self.app_config_fields.retention_ms
{
widget_state.current_display_time = self.app_config_fields.retention_ms;
if self.app_config_fields.autohide_time {
widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_out(
self.app_config_fields.time_interval,
self.app_config_fields.retention_ms,
self.app_config_fields.autohide_time,
);
}
}
_ => {}
@@ -2075,75 +2050,42 @@ impl App {
fn zoom_in(&mut self) {
match self.current_widget.widget_type {
BottomWidgetType::Cpu => {
if let Some(cpu_widget_state) = self
if let Some(widget_state) = self
.states
.cpu_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = cpu_widget_state
.current_display_time
.saturating_sub(self.app_config_fields.time_interval);
if new_time >= STALE_MIN_MILLISECONDS {
cpu_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
cpu_widget_state.autohide_timer = Some(Instant::now());
}
} else if cpu_widget_state.current_display_time != STALE_MIN_MILLISECONDS {
cpu_widget_state.current_display_time = STALE_MIN_MILLISECONDS;
if self.app_config_fields.autohide_time {
cpu_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_in(
self.app_config_fields.time_interval,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Mem => {
if let Some(mem_widget_state) = self
if let Some(widget_state) = self
.states
.mem_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = mem_widget_state
.current_display_time
.saturating_sub(self.app_config_fields.time_interval);
if new_time >= STALE_MIN_MILLISECONDS {
mem_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
mem_widget_state.autohide_timer = Some(Instant::now());
}
} else if mem_widget_state.current_display_time != STALE_MIN_MILLISECONDS {
mem_widget_state.current_display_time = STALE_MIN_MILLISECONDS;
if self.app_config_fields.autohide_time {
mem_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_in(
self.app_config_fields.time_interval,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Net => {
if let Some(net_widget_state) = self
if let Some(widget_state) = self
.states
.net_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
let new_time = net_widget_state
.current_display_time
.saturating_sub(self.app_config_fields.time_interval);
if new_time >= STALE_MIN_MILLISECONDS {
net_widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
net_widget_state.autohide_timer = Some(Instant::now());
}
} else if net_widget_state.current_display_time != STALE_MIN_MILLISECONDS {
net_widget_state.current_display_time = STALE_MIN_MILLISECONDS;
if self.app_config_fields.autohide_time {
net_widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_in(
self.app_config_fields.time_interval,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::TempGraph => {
@@ -2152,88 +2094,69 @@ impl App {
.temp_graph_state
.get_mut_widget_state(self.current_widget.widget_id)
{
let new_time = widget_state
.current_display_time
.saturating_sub(self.app_config_fields.time_interval);
if new_time >= STALE_MIN_MILLISECONDS {
widget_state.current_display_time = new_time;
if self.app_config_fields.autohide_time {
widget_state.autohide_timer = Some(Instant::now());
}
} else if widget_state.current_display_time != STALE_MIN_MILLISECONDS {
widget_state.current_display_time = STALE_MIN_MILLISECONDS;
if self.app_config_fields.autohide_time {
widget_state.autohide_timer = Some(Instant::now());
}
}
widget_state.time_series_state.zoom_in(
self.app_config_fields.time_interval,
self.app_config_fields.autohide_time,
);
}
}
_ => {}
}
}
fn reset_cpu_zoom(&mut self) {
if let Some(cpu_widget_state) = self
.states
.cpu_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
cpu_widget_state.current_display_time = self.app_config_fields.default_time_value;
if self.app_config_fields.autohide_time {
cpu_widget_state.autohide_timer = Some(Instant::now());
}
}
}
fn reset_mem_zoom(&mut self) {
if let Some(mem_widget_state) = self
.states
.mem_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
mem_widget_state.current_display_time = self.app_config_fields.default_time_value;
if self.app_config_fields.autohide_time {
mem_widget_state.autohide_timer = Some(Instant::now());
}
}
}
fn reset_net_zoom(&mut self) {
if let Some(net_widget_state) = self
.states
.net_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
net_widget_state.current_display_time = self.app_config_fields.default_time_value;
if self.app_config_fields.autohide_time {
net_widget_state.autohide_timer = Some(Instant::now());
}
}
}
fn reset_temp_graph_zoom(&mut self) {
if let Some(widget_state) = self
.states
.temp_graph_state
.get_mut_widget_state(self.current_widget.widget_id)
{
widget_state.current_display_time = self.app_config_fields.default_time_value;
if self.app_config_fields.autohide_time {
widget_state.autohide_timer = Some(Instant::now());
}
}
}
fn reset_zoom(&mut self) {
match self.current_widget.widget_type {
BottomWidgetType::Cpu => self.reset_cpu_zoom(),
BottomWidgetType::Mem => self.reset_mem_zoom(),
BottomWidgetType::Net => self.reset_net_zoom(),
BottomWidgetType::TempGraph => self.reset_temp_graph_zoom(),
BottomWidgetType::Cpu => {
if let Some(widget_state) = self
.states
.cpu_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Mem => {
if let Some(widget_state) = self
.states
.mem_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::Net => {
if let Some(widget_state) = self
.states
.net_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
}
BottomWidgetType::TempGraph => {
if let Some(widget_state) = self
.states
.temp_graph_state
.get_mut_widget_state(self.current_widget.widget_id)
{
widget_state.time_series_state.reset_zoom(
self.app_config_fields.default_time_value,
self.app_config_fields.autohide_time,
);
}
}
_ => {}
}
}
+4 -4
View File
@@ -27,7 +27,7 @@ use crate::{
pub struct StoredData {
// FIXME: (points_rework_v1) we could be able to remove this with some more refactoring.
pub last_update_time: Instant,
pub timeseries_data: TimeSeriesData,
pub time_series_data: TimeSeriesData,
pub network_harvest: NetworkHarvest,
pub ram_harvest: Option<MemData>,
pub swap_harvest: Option<MemData>,
@@ -53,7 +53,7 @@ impl Default for StoredData {
fn default() -> Self {
StoredData {
last_update_time: Instant::now(),
timeseries_data: TimeSeriesData::default(),
time_series_data: TimeSeriesData::default(),
network_harvest: NetworkHarvest::default(),
ram_harvest: None,
#[cfg(not(target_os = "windows"))]
@@ -100,7 +100,7 @@ impl StoredData {
}
if !settings.use_basic_mode {
self.timeseries_data
self.time_series_data
.add(&data, used_widgets, settings, filters);
}
@@ -345,7 +345,7 @@ impl DataStore {
/// Clean data.
pub fn clean_data(&mut self, max_duration: Duration) {
self.main.timeseries_data.prune(max_duration);
self.main.time_series_data.prune(max_duration);
}
/// Reset data state.
+1 -1
View File
@@ -3,5 +3,5 @@
pub mod data_table;
pub mod pipe_gauge;
pub mod scroll_bar;
pub mod time_graph;
pub mod time_series;
pub mod widget_carousel;
@@ -1,2 +0,0 @@
//! A variant of a [`crate::canvas::components::time_graph::TimeGraph`] that
//! automatically adjusts the y-axis based on the data provided.
@@ -11,7 +11,7 @@ use tui::{
widgets::{BorderType, GraphType},
};
use crate::canvas::{components::time_graph::*, drawing_utils::widget_block};
use crate::canvas::{components::time_series::*, drawing_utils::widget_block};
/// Represents the data required by the [`TimeGraph`].
///
@@ -238,7 +238,7 @@ mod test {
};
use super::{AxisBound, ChartScaling, TimeGraph};
use crate::canvas::components::time_graph::Axis;
use crate::canvas::components::time_series::Axis;
const Y_LABELS: [Cow<'static, str>; 3] = [
Cow::Borrowed("0%"),
@@ -246,7 +246,7 @@ mod test {
Cow::Borrowed("100%"),
];
fn create_time_graph() -> TimeGraph<'static> {
fn create_time_series() -> TimeGraph<'static> {
TimeGraph {
title: " Network ".into(),
x_min: -15000.0,
@@ -268,8 +268,8 @@ mod test {
}
#[test]
fn time_graph_gen_x_axis() {
let tg = create_time_graph();
fn time_series_gen_x_axis() {
let tg = create_time_series();
let style = Style::default().fg(Color::Red);
let x_axis = tg.generate_x_axis();
@@ -283,8 +283,8 @@ mod test {
}
#[test]
fn time_graph_gen_y_axis() {
let tg = create_time_graph();
fn time_series_gen_y_axis() {
let tg = create_time_series();
let style = Style::default().fg(Color::Red);
let y_axis = tg.generate_y_axis();
@@ -0,0 +1,2 @@
//! A variant of a [`crate::canvas::components::time_series::TimeGraph`] that
//! automatically adjusts the y-axis based on the data provided.
@@ -7,7 +7,7 @@ use tui::symbols::Marker;
use crate::{
app::AppConfigFields,
canvas::components::time_graph::{
canvas::components::time_series::{
AxisBound, ChartScaling, LegendConstraints, LegendPosition, TimeGraph,
variants::get_border_style,
},
@@ -1,8 +1,8 @@
//! A [`tui::widgets::Chart`] but slightly more specialized to show
//! right-aligned timeseries data.
//! right-aligned time_series data.
//!
//! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs);
//! the specializations are factored out to `time_graph/points.rs`.
//! the specializations are factored out to `time_series/points.rs`.
mod canvas;
mod grid;
@@ -27,7 +27,7 @@ use tui::{
use unicode_width::UnicodeWidthStr;
use crate::{
canvas::components::time_graph::LegendConstraints,
canvas::components::time_series::LegendConstraints,
utils::general::{saturating_log2, saturating_log10},
};
@@ -115,7 +115,7 @@ mod test {
use super::*;
#[test]
fn time_graph_test_interpolation() {
fn time_series_test_interpolation() {
let data = [(-3.0, 8.0), (-1.0, 6.0), (0.0, 5.0)];
assert_eq!(interpolate_point(&data[1], &data[2], 0.0), 5.0);
+5 -5
View File
@@ -9,7 +9,7 @@ use crate::{
Painter,
components::{
data_table::{DrawInfo, SelectionState},
time_graph::{GraphData, PercentTimeGraph},
time_series::{GraphData, PercentTimeGraph},
},
drawing_utils::should_hide_x_label,
},
@@ -122,8 +122,8 @@ impl Painter {
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
let current_scroll_position = cpu_widget_state.table.state.current_index;
let cpu_entries = &data.cpu_harvest;
let cpu_points = &data.timeseries_data.cpu;
let time = &data.timeseries_data.time;
let cpu_points = &data.time_series_data.cpu;
let time = &data.time_series_data.time;
if current_scroll_position == ALL_POSITION {
// This case ensures the other cases cannot have the position be equal to 0.
@@ -177,7 +177,7 @@ impl Painter {
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut cpu_widget_state.autohide_timer,
cpu_widget_state.time_series_state.autohide_timer_mut(),
draw_loc,
);
@@ -206,7 +206,7 @@ impl Painter {
};
PercentTimeGraph {
display_range: cpu_widget_state.current_display_time,
display_range: cpu_widget_state.time_series_state.current_display_time(),
hide_x_labels,
app_config_fields: &app_state.app_config_fields,
current_widget: app_state.current_widget.widget_id,
+10 -10
View File
@@ -10,7 +10,7 @@ use crate::{
app::{App, data::Values},
canvas::{
Painter,
components::time_graph::{GraphData, LegendConstraints, PercentTimeGraph},
components::time_series::{GraphData, LegendConstraints, PercentTimeGraph},
drawing_utils::should_hide_x_label,
},
collection::memory::MemData,
@@ -60,7 +60,7 @@ impl Painter {
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut mem_state.autohide_timer,
mem_state.time_series_state.autohide_timer_mut(),
draw_loc,
);
let graph_data = {
@@ -85,8 +85,8 @@ impl Painter {
}
let mut points = Vec::with_capacity(size);
let timeseries = &data.timeseries_data;
let time = &timeseries.time;
let time_series = &data.time_series_data;
let time = &time_series.time;
// TODO: Add a "no data" option here/to time graph if there is no entries
graph_data(
@@ -94,7 +94,7 @@ impl Painter {
"RAM",
data.ram_harvest.as_ref(),
time,
&timeseries.ram,
&time_series.ram,
self.styles.ram_style,
);
@@ -103,7 +103,7 @@ impl Painter {
"SWP",
data.swap_harvest.as_ref(),
time,
&timeseries.swap,
&time_series.swap,
self.styles.swap_style,
);
@@ -114,7 +114,7 @@ impl Painter {
"CACHE", // TODO: Figure out how to line this up better
data.cache_harvest.as_ref(),
time,
&timeseries.cache_mem,
&time_series.cache_mem,
self.styles.cache_style,
);
}
@@ -126,7 +126,7 @@ impl Painter {
"ARC",
data.arc_harvest.as_ref(),
time,
&timeseries.arc_mem,
&time_series.arc_mem,
self.styles.arc_style,
);
}
@@ -137,7 +137,7 @@ impl Painter {
let gpu_styles = &self.styles.gpu_colours;
for (name, harvest) in &data.gpu_harvest {
if let Some(gpu_data) = data.timeseries_data.gpu_mem.get(name) {
if let Some(gpu_data) = data.time_series_data.gpu_mem.get(name) {
let style = {
if gpu_styles.is_empty() {
Style::default()
@@ -165,7 +165,7 @@ impl Painter {
};
PercentTimeGraph {
display_range: mem_state.current_display_time,
display_range: mem_state.time_series_state.current_display_time(),
hide_x_labels,
app_config_fields: &app_state.app_config_fields,
current_widget: app_state.current_widget.widget_id,
+11 -7
View File
@@ -10,7 +10,7 @@ use crate::{
app::{App, AppConfigFields, AxisScaling},
canvas::{
Painter,
components::time_graph::{
components::time_series::{
AxisBound, ChartScaling, GraphData, LegendConstraints, TimeGraph,
},
drawing_utils::{should_hide_x_label, widget_block},
@@ -63,16 +63,18 @@ impl Painter {
{
let shared_data = app_state.data_store.get_data();
let network_latest_data = &(shared_data.network_harvest);
let rx_points = &(shared_data.timeseries_data.rx);
let tx_points = &(shared_data.timeseries_data.tx);
let times = &(shared_data.timeseries_data.time);
let time_start = -(network_widget_state.current_display_time as f64);
let rx_points = &(shared_data.time_series_data.rx);
let tx_points = &(shared_data.time_series_data.tx);
let times = &(shared_data.time_series_data.time);
let time_start = -(network_widget_state
.time_series_state
.current_display_time() as f64);
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut network_widget_state.autohide_timer,
network_widget_state.time_series_state.autohide_timer_mut(),
draw_loc,
);
@@ -81,7 +83,9 @@ impl Painter {
let cache = &mut network_widget_state.height_cache;
cache.get_or_update(
last_time,
network_widget_state.current_display_time,
network_widget_state
.time_series_state
.current_display_time(),
[rx_points, tx_points].into_iter(),
times,
)
+6 -6
View File
@@ -8,7 +8,7 @@ use crate::{
app::{App, AppConfigFields},
canvas::{
Painter,
components::time_graph::{
components::time_series::{
AxisBound, ChartScaling, GraphData, LegendConstraints, TimeGraph,
},
drawing_utils::should_hide_x_label,
@@ -25,15 +25,15 @@ impl Painter {
.get_mut_widget_state(widget_id)
{
let shared_data = app_state.data_store.get_data();
let points = &(shared_data.timeseries_data.temperature);
let times = &(shared_data.timeseries_data.time);
let time_start = -(widget_state.current_display_time as f64);
let points = &(shared_data.time_series_data.temperature);
let times = &(shared_data.time_series_data.time);
let time_start = -(widget_state.time_series_state.current_display_time() as f64);
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
let hide_x_labels = should_hide_x_label(
app_state.app_config_fields.hide_time,
app_state.app_config_fields.autohide_time,
&mut widget_state.autohide_timer,
widget_state.time_series_state.autohide_timer_mut(),
draw_loc,
);
@@ -42,7 +42,7 @@ impl Painter {
let cache = &mut widget_state.height_cache;
cache.get_or_update(
last_time,
widget_state.current_display_time,
widget_state.time_series_state.current_display_time(),
points.values(),
times,
)
+2 -2
View File
@@ -32,7 +32,7 @@ use self::{
};
use crate::{
app::{filter::Filter, layout_manager::*, *},
canvas::components::time_graph::LegendPosition,
canvas::components::time_series::LegendPosition,
constants::*,
utils::data_units::DataUnit,
widgets::*,
@@ -1112,7 +1112,7 @@ mod test {
use crate::{
app::App,
args::BottomArgs,
canvas::components::time_graph::LegendPosition,
canvas::components::time_series::LegendPosition,
options::{
OptionError, config::flags::GeneralConfig, get_default_time_value, get_retention,
get_update_rate, parse_legend_position, try_parse_ms,
+5
View File
@@ -0,0 +1,5 @@
//! Common widget code.
mod time_series;
pub use time_series::*;
+309
View File
@@ -0,0 +1,309 @@
use std::{
cmp::{max, min},
time::{Duration, Instant},
};
use timeless::data::ChunkedData;
const STALE_MIN_MILLISECONDS: u64 = Duration::from_secs(30).as_millis() as u64;
/// A time_series graph widget displays data over a period of time.
pub struct TimeseriesState {
current_display_time: u64,
autohide_timer: Option<Instant>,
}
impl TimeseriesState {
/// Create a new [`TimeseriesState`] that displays starting from `starting_time`.
pub fn new(starting_time: u64) -> Self {
Self {
current_display_time: starting_time,
autohide_timer: None,
}
}
/// Set the autohide timer.
pub fn with_autohide_timer(mut self, autohide_timer: Option<Instant>) -> Self {
self.autohide_timer = autohide_timer;
self
}
/// Get a mutable reference to the autohide timer.
pub fn autohide_timer_mut(&mut self) -> &mut Option<Instant> {
&mut self.autohide_timer
}
/// Get the current display time.
pub fn current_display_time(&self) -> u64 {
self.current_display_time
}
/// Zoom in on the x-axis (reducing the time range shown).
pub fn zoom_in(&mut self, time_interval: u64, autohide_time: bool) {
let new_time = self.current_display_time.saturating_sub(time_interval);
self.current_display_time = max(new_time, STALE_MIN_MILLISECONDS);
self.maybe_start_autohide(autohide_time);
}
/// Zoom out on the x-axis (increasing the time range shown).
pub fn zoom_out(&mut self, time_interval: u64, retention_ms: u64, autohide_time: bool) {
let new_time = self.current_display_time.saturating_add(time_interval);
self.current_display_time = min(new_time, retention_ms);
self.maybe_start_autohide(autohide_time);
}
/// Reset the zoom level to the default.
pub fn reset_zoom(&mut self, default_time_value: u64, autohide_time: bool) {
self.current_display_time = default_time_value;
self.maybe_start_autohide(autohide_time);
}
/// Set the autohide timer if needed.
fn maybe_start_autohide(&mut self, autohide_time: bool) {
if autohide_time {
self.autohide_timer = Some(Instant::now());
}
}
}
struct GraphHeightCacheInner {
best_point: (Instant, f64),
right_edge: Instant,
period: u64,
}
#[derive(Default)]
pub struct GraphHeightCache {
inner: Option<GraphHeightCacheInner>,
}
impl GraphHeightCache {
/// Get the cached height if it exists, or set it otherwise.
pub(crate) fn get_or_update<
'a,
F: Into<f64> + Clone + Copy + 'a,
S: Iterator<Item = &'a ChunkedData<F>>,
>(
&mut self, last_time: &Instant, current_display_time: u64, sources: S, times: &[Instant],
) -> f64 {
let visible_duration = Duration::from_millis(current_display_time);
let (mut biggest, mut biggest_time, oldest_to_check) = if let Some(GraphHeightCacheInner {
best_point,
right_edge,
period,
}) = self.inner.as_ref()
&& *period == current_display_time
&& last_time.duration_since(best_point.0) < visible_duration
{
(best_point.1, best_point.0, *right_edge)
} else {
let visible_duration = Duration::from_millis(current_display_time);
let visible_left_bound = match last_time.checked_sub(visible_duration) {
Some(v) => v,
None => {
// On some systems (like Windows) it can be possible that the
// current display time
// causes subtraction to fail if, for example, the uptime of the
// system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.
//
// As such, we instead take the oldest visible time. This is a
// bit inefficient, but
// since it should only happen rarely, it should be fine.
times
.iter()
.take_while(|t| last_time.duration_since(**t) < visible_duration)
.last()
.cloned()
.unwrap_or(*last_time)
}
};
(0.0, visible_left_bound, visible_left_bound)
};
for source in sources {
for (&time, &v) in source
.iter_along_base(times)
.rev()
.take_while(|&(&time, _)| time >= oldest_to_check)
{
let v = v.into();
if v > biggest {
biggest = v;
biggest_time = time;
}
}
}
self.inner = Some(GraphHeightCacheInner {
best_point: (biggest_time, biggest),
right_edge: *last_time,
period: current_display_time,
});
biggest
}
}
#[cfg(test)]
mod time_series_tests {
use super::*;
#[test]
fn zoom_in_decreases_display_time() {
let mut state = TimeseriesState {
current_display_time: 60_000,
autohide_timer: None,
};
state.zoom_in(15_000, false);
assert_eq!(state.current_display_time, 45_000);
}
#[test]
fn zoom_in_clamps_at_minimum() {
let mut state = TimeseriesState {
current_display_time: 35_000,
autohide_timer: None,
};
state.zoom_in(15_000, false);
assert_eq!(state.current_display_time, STALE_MIN_MILLISECONDS); // 30_000
}
#[test]
fn zoom_out_increases_display_time() {
let mut state = TimeseriesState {
current_display_time: 60_000,
autohide_timer: None,
};
state.zoom_out(15_000, 300_000, false);
assert_eq!(state.current_display_time, 75_000);
}
#[test]
fn zoom_out_clamps_at_retention() {
let mut state = TimeseriesState {
current_display_time: 290_000,
autohide_timer: None,
};
state.zoom_out(15_000, 300_000, false);
assert_eq!(state.current_display_time, 300_000);
}
#[test]
fn reset_zoom_restores_default() {
let mut state = TimeseriesState {
current_display_time: 120_000,
autohide_timer: None,
};
state.reset_zoom(60_000, false);
assert_eq!(state.current_display_time, 60_000);
}
#[test]
fn autohide_armed_on_change() {
let mut state = TimeseriesState {
current_display_time: 60_000,
autohide_timer: None,
};
state.zoom_in(15_000, true);
assert!(state.autohide_timer.is_some());
}
}
#[cfg(test)]
mod graph_height_tests {
use super::*;
fn build(times: &[Instant], values: &[f64]) -> ChunkedData<f64> {
assert_eq!(times.len(), values.len());
let mut data = ChunkedData::default();
for &v in values {
data.push(v);
}
data
}
#[test]
fn empty_sources_returns_zero() {
let mut cache = GraphHeightCache::default();
let last_time = Instant::now();
let times: Vec<Instant> = vec![];
let sources: Vec<ChunkedData<f64>> = vec![];
let result = cache.get_or_update(&last_time, 1_000, sources.iter(), &times);
assert_eq!(result, 0.0);
assert!(cache.inner.is_some());
}
#[test]
fn picks_max_across_sources() {
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(300),
now - Duration::from_millis(200),
now - Duration::from_millis(100),
now,
];
let a = build(&times, &[1.0, 2.0, 3.0, 4.0]);
let b = build(&times, &[10.0, 5.0, 0.5, 0.25]);
let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), &times);
assert_eq!(result, 10.0);
}
#[test]
fn cache_hit_skips_older_points() {
// On a cache hit, only points after `right_edge` are rescanned. So if
// we pass a fresh source whose only large values are *older* than the
// previous `last_time`, the cached max should still win.
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(200),
now - Duration::from_millis(100),
now,
];
let first = build(&times, &[3.0, 5.0, 7.0]);
let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), &times);
assert_eq!(first_result, 7.0);
// Older points carry huge values; newest is small. A full rescan would
// return 1000.0 — a true cache hit returns the cached 7.0.
let second = build(&times, &[1000.0, 999.0, 4.0]);
let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), &times);
assert_eq!(second_result, 7.0);
}
#[test]
fn cache_invalidates_on_period_change() {
// First call uses a small window that excludes the older high value.
// Second call uses a larger window that should include it; a stale
// cache would miss it and return the previous max.
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(500),
now - Duration::from_millis(50),
now,
];
let data = build(&times, &[100.0, 5.0, 7.0]);
let first = cache.get_or_update(&now, 100, [&data].into_iter(), &times);
assert_eq!(first, 7.0);
let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), &times);
assert_eq!(second, 100.0);
}
}
+5 -5
View File
@@ -14,6 +14,7 @@ use crate::{
},
collection::cpu::{CpuData, CpuDataType},
options::config::{cpu::CpuDefault, style::Styles},
widgets::TimeseriesState,
};
pub enum CpuWidgetColumn {
@@ -125,16 +126,15 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
}
pub struct CpuWidgetState {
pub current_display_time: u64,
pub time_series_state: TimeseriesState,
pub is_legend_hidden: bool,
pub autohide_timer: Option<Instant>,
pub table: DataTable<CpuWidgetTableData, CpuWidgetColumn>,
pub force_update_data: bool,
}
impl CpuWidgetState {
pub(crate) fn new(
config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64,
config: &AppConfigFields, default_selection: CpuDefault, starting_time: u64,
autohide_timer: Option<Instant>, colours: &Styles,
) -> Self {
let columns = [
@@ -168,9 +168,9 @@ impl CpuWidgetState {
}
CpuWidgetState {
current_display_time,
time_series_state: TimeseriesState::new(starting_time)
.with_autohide_timer(autohide_timer),
is_legend_hidden: false,
autohide_timer,
table,
force_update_data: false,
}
+6 -5
View File
@@ -1,15 +1,16 @@
use std::time::Instant;
use crate::widgets::TimeseriesState;
pub struct MemWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
pub time_series_state: TimeseriesState,
}
impl MemWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
pub fn init(starting_time: u64, autohide_timer: Option<Instant>) -> Self {
MemWidgetState {
current_display_time,
autohide_timer,
time_series_state: TimeseriesState::new(starting_time)
.with_autohide_timer(autohide_timer),
}
}
}
+2 -172
View File
@@ -1,4 +1,5 @@
pub mod battery_info;
mod common;
pub mod cpu_graph;
pub mod disk_table;
pub mod mem_graph;
@@ -7,9 +8,8 @@ pub mod process_table;
pub mod temperature_graph;
pub mod temperature_table;
use std::time::{Duration, Instant};
pub use battery_info::*;
pub(crate) use common::*;
pub use cpu_graph::*;
pub use disk_table::*;
pub use mem_graph::*;
@@ -17,173 +17,3 @@ pub use network_graph::*;
pub use process_table::*;
pub use temperature_graph::*;
pub use temperature_table::*;
use timeless::data::ChunkedData;
struct GraphHeightCacheInner {
best_point: (Instant, f64),
right_edge: Instant,
period: u64,
}
#[derive(Default)]
pub struct GraphHeightCache {
inner: Option<GraphHeightCacheInner>,
}
impl GraphHeightCache {
/// Get the cached height if it exists, or set it otherwise.
pub(crate) fn get_or_update<
'a,
F: Into<f64> + Clone + Copy + 'a,
S: Iterator<Item = &'a ChunkedData<F>>,
>(
&mut self, last_time: &Instant, current_display_time: u64, sources: S, times: &[Instant],
) -> f64 {
let visible_duration = Duration::from_millis(current_display_time);
let (mut biggest, mut biggest_time, oldest_to_check) = if let Some(GraphHeightCacheInner {
best_point,
right_edge,
period,
}) = self.inner.as_ref()
&& *period == current_display_time
&& last_time.duration_since(best_point.0) < visible_duration
{
(best_point.1, best_point.0, *right_edge)
} else {
let visible_duration = Duration::from_millis(current_display_time);
let visible_left_bound = match last_time.checked_sub(visible_duration) {
Some(v) => v,
None => {
// On some systems (like Windows) it can be possible that the
// current display time
// causes subtraction to fail if, for example, the uptime of the
// system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.
//
// As such, we instead take the oldest visible time. This is a
// bit inefficient, but
// since it should only happen rarely, it should be fine.
times
.iter()
.take_while(|t| last_time.duration_since(**t) < visible_duration)
.last()
.cloned()
.unwrap_or(*last_time)
}
};
(0.0, visible_left_bound, visible_left_bound)
};
for source in sources {
for (&time, &v) in source
.iter_along_base(times)
.rev()
.take_while(|&(&time, _)| time >= oldest_to_check)
{
let v = v.into();
if v > biggest {
biggest = v;
biggest_time = time;
}
}
}
self.inner = Some(GraphHeightCacheInner {
best_point: (biggest_time, biggest),
right_edge: *last_time,
period: current_display_time,
});
biggest
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build(times: &[Instant], values: &[f64]) -> ChunkedData<f64> {
assert_eq!(times.len(), values.len());
let mut data = ChunkedData::default();
for &v in values {
data.push(v);
}
data
}
#[test]
fn empty_sources_returns_zero() {
let mut cache = GraphHeightCache::default();
let last_time = Instant::now();
let times: Vec<Instant> = vec![];
let sources: Vec<ChunkedData<f64>> = vec![];
let result = cache.get_or_update(&last_time, 1_000, sources.iter(), &times);
assert_eq!(result, 0.0);
assert!(cache.inner.is_some());
}
#[test]
fn picks_max_across_sources() {
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(300),
now - Duration::from_millis(200),
now - Duration::from_millis(100),
now,
];
let a = build(&times, &[1.0, 2.0, 3.0, 4.0]);
let b = build(&times, &[10.0, 5.0, 0.5, 0.25]);
let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), &times);
assert_eq!(result, 10.0);
}
#[test]
fn cache_hit_skips_older_points() {
// On a cache hit, only points after `right_edge` are rescanned. So if
// we pass a fresh source whose only large values are *older* than the
// previous `last_time`, the cached max should still win.
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(200),
now - Duration::from_millis(100),
now,
];
let first = build(&times, &[3.0, 5.0, 7.0]);
let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), &times);
assert_eq!(first_result, 7.0);
// Older points carry huge values; newest is small. A full rescan would
// return 1000.0 — a true cache hit returns the cached 7.0.
let second = build(&times, &[1000.0, 999.0, 4.0]);
let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), &times);
assert_eq!(second_result, 7.0);
}
#[test]
fn cache_invalidates_on_period_change() {
// First call uses a small window that excludes the older high value.
// Second call uses a larger window that should include it; a stale
// cache would miss it and return the previous max.
let mut cache = GraphHeightCache::default();
let now = Instant::now();
let times = vec![
now - Duration::from_millis(500),
now - Duration::from_millis(50),
now,
];
let data = build(&times, &[100.0, 5.0, 7.0]);
let first = cache.get_or_update(&now, 100, [&data].into_iter(), &times);
assert_eq!(first, 7.0);
let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), &times);
assert_eq!(second, 100.0);
}
}
+5 -6
View File
@@ -1,18 +1,17 @@
use std::time::Instant;
use crate::widgets::GraphHeightCache;
use crate::widgets::{GraphHeightCache, TimeseriesState};
pub struct NetWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
pub time_series_state: TimeseriesState,
pub height_cache: GraphHeightCache,
}
impl NetWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
pub fn init(starting_time: u64, autohide_timer: Option<Instant>) -> Self {
NetWidgetState {
current_display_time,
autohide_timer,
time_series_state: TimeseriesState::new(starting_time)
.with_autohide_timer(autohide_timer),
height_cache: GraphHeightCache::default(),
}
}
+6 -9
View File
@@ -2,23 +2,20 @@
use std::time::Instant;
use crate::widgets::GraphHeightCache;
use crate::widgets::{GraphHeightCache, TimeseriesState};
/// A timeseries graph widget displaying temperature usage over time.
/// A time_series graph widget displaying temperature usage over time.
pub struct TempGraphWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
pub time_series_state: TimeseriesState,
pub height_cache: GraphHeightCache,
pub max_temp: Option<f32>,
}
impl TempGraphWidgetState {
pub fn new(
current_display_time: u64, autohide_timer: Option<Instant>, max_temp: Option<f32>,
) -> Self {
pub fn new(starting_time: u64, autohide_timer: Option<Instant>, max_temp: Option<f32>) -> Self {
TempGraphWidgetState {
current_display_time,
autohide_timer,
time_series_state: TimeseriesState::new(starting_time)
.with_autohide_timer(autohide_timer),
height_cache: GraphHeightCache::default(),
max_temp,
}