mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-08-24 04:16:30 +00:00
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:
+123
-200
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
+7
-7
@@ -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.
|
||||
+1
-1
@@ -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,
|
||||
},
|
||||
+3
-3
@@ -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},
|
||||
};
|
||||
|
||||
+1
-1
@@ -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);
|
||||
@@ -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,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 = ×eries.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,
|
||||
×eries.ram,
|
||||
&time_series.ram,
|
||||
self.styles.ram_style,
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ impl Painter {
|
||||
"SWP",
|
||||
data.swap_harvest.as_ref(),
|
||||
time,
|
||||
×eries.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,
|
||||
×eries.cache_mem,
|
||||
&time_series.cache_mem,
|
||||
self.styles.cache_style,
|
||||
);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl Painter {
|
||||
"ARC",
|
||||
data.arc_harvest.as_ref(),
|
||||
time,
|
||||
×eries.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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Common widget code.
|
||||
|
||||
mod time_series;
|
||||
|
||||
pub use time_series::*;
|
||||
@@ -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(), ×);
|
||||
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(×, &[1.0, 2.0, 3.0, 4.0]);
|
||||
let b = build(×, &[10.0, 5.0, 0.5, 0.25]);
|
||||
|
||||
let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), ×);
|
||||
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(×, &[3.0, 5.0, 7.0]);
|
||||
|
||||
let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), ×);
|
||||
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(×, &[1000.0, 999.0, 4.0]);
|
||||
let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), ×);
|
||||
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(×, &[100.0, 5.0, 7.0]);
|
||||
|
||||
let first = cache.get_or_update(&now, 100, [&data].into_iter(), ×);
|
||||
assert_eq!(first, 7.0);
|
||||
|
||||
let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), ×);
|
||||
assert_eq!(second, 100.0);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(), ×);
|
||||
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(×, &[1.0, 2.0, 3.0, 4.0]);
|
||||
let b = build(×, &[10.0, 5.0, 0.5, 0.25]);
|
||||
|
||||
let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), ×);
|
||||
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(×, &[3.0, 5.0, 7.0]);
|
||||
|
||||
let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), ×);
|
||||
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(×, &[1000.0, 999.0, 4.0]);
|
||||
let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), ×);
|
||||
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(×, &[100.0, 5.0, 7.0]);
|
||||
|
||||
let first = cache.get_or_update(&now, 100, [&data].into_iter(), ×);
|
||||
assert_eq!(first, 7.0);
|
||||
|
||||
let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), ×);
|
||||
assert_eq!(second, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user