Skip to content

Commit 5859505

Browse files
committed
feature: add disk i/o time series chart
1 parent 7d9b710 commit 5859505

24 files changed

Lines changed: 723 additions & 71 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ That said, these are more guidelines rather than hard rules, though the project
4141
- [#2053](https://github.com/ClementTsang/bottom/pull/2053): Add a configurable default sort column for the process widget (`processes.default_sort` or `--process-default-sort`).
4242
- [#2066](https://github.com/ClementTsang/bottom/pull/2066): Add search support in the help dialog.
4343
- [#1791](https://github.com/ClementTsang/bottom/pull/1791), [#2072](https://github.com/ClementTsang/bottom/pull/2072): Add support for using a short name for the GPU in memory usage.
44+
- [#2073](https://github.com/ClementTsang/bottom/pull/2073): Add disk I/O time series graph.
4445

4546
### Changes
4647

@@ -53,6 +54,7 @@ That said, these are more guidelines rather than hard rules, though the project
5354
- [#2064](https://github.com/ClementTsang/bottom/pull/2064): Move CPU config file options from `[flags]` to be under `[cpu]`.
5455
- [#2064](https://github.com/ClementTsang/bottom/pull/2064): Move memory config file options from `[flags]` to be under `[memory_graph]`.
5556
- [#2066](https://github.com/ClementTsang/bottom/pull/2066): Take cgroup into account for CPU usage% calculations.
57+
- [#2074](https://github.com/ClementTsang/bottom/pull/2074): Treat uppercase 'Q' as a quit shortcut.
5658

5759
### Other
5860

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Disk I/O Graph
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Disk I/O Graph Widget

docs/mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ nav:
168168
- "Disk Widget": usage/widgets/disk.md
169169
- "Temperature Widget": usage/widgets/temperature-table.md
170170
- "Temperature Graph Widget": usage/widgets/temperature-graph.md
171+
- "Disk I/O Graph Widget": usage/widgets/disk-io-graph.md
171172
- "Battery Widget": usage/widgets/battery.md
172173
- "Auto-Complete": usage/autocomplete.md
173174
- "Configuration":
@@ -181,6 +182,7 @@ nav:
181182
- "Processes Widget": configuration/config-file/processes.md
182183
- "Temperature Table Widget": configuration/config-file/temperature-table.md
183184
- "Temperature Graph Widget": configuration/config-file/temperature-graph.md
185+
- "Disk I/O Graph Widget": configuration/config-file/disk-io-graph.md
184186
- "Flags": configuration/config-file/flags.md
185187
- "Layout": configuration/config-file/layout.md
186188
- "Styling": configuration/config-file/styling.md

src/app.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub struct AppConfigFields {
8181
pub default_temp_sort_column: Option<TempWidgetColumn>,
8282
pub default_disk_sort_column: Option<DiskWidgetColumn>,
8383
pub temperature_legend_position: Option<LegendPosition>,
84+
pub disk_io_legend_position: Option<LegendPosition>,
8485
}
8586

8687
/// For filtering out information
@@ -2073,6 +2074,14 @@ impl App {
20732074
{
20742075
Some(widget_state.graph.state_mut())
20752076
}
2077+
BottomWidgetType::DiskIoGraph
2078+
if let Some(widget_state) = self
2079+
.states
2080+
.disk_io_graph_state
2081+
.get_mut_widget_state(self.current_widget.widget_id) =>
2082+
{
2083+
Some(widget_state.graph.state_mut())
2084+
}
20762085
_ => None,
20772086
}
20782087
}

src/app/data/store.rs

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::{
33
vec::Vec,
44
};
55

6+
use rustc_hash::FxHashSet;
7+
68
use super::{ProcessData, TimeSeriesData};
79
#[cfg(feature = "battery")]
810
use crate::collection::batteries;
@@ -42,7 +44,7 @@ pub struct StoredData {
4244
pub process_data: ProcessData,
4345
/// TODO: (points_rework_v1) Might be a better way to do this without having
4446
/// to store here?
45-
pub prev_io: Vec<(u64, u64)>,
47+
pub prev_io: Vec<Option<(u64, u64)>>,
4648
pub disk_harvest: Vec<DiskWidgetData>,
4749
pub temp_data: Vec<TempWidgetData>,
4850
#[cfg(feature = "battery")]
@@ -153,6 +155,41 @@ impl StoredData {
153155
if let Some(disks) = data.disks {
154156
if let Some(io) = data.io {
155157
self.eat_disks(disks, io, harvested_time);
158+
159+
if !settings.use_basic_mode && used_widgets.use_disk_graph {
160+
let mut not_visited: FxHashSet<String> =
161+
self.time_series_data.disk_io_read.keys().cloned().collect();
162+
163+
for disk in &self.disk_harvest {
164+
not_visited.remove(&disk.name);
165+
166+
macro_rules! push_or_break {
167+
($map:expr, $val:expr) => {
168+
let entry = $map.entry(disk.name.clone()).or_default();
169+
match $val {
170+
Some(v) => entry.push(v as f64),
171+
None => entry.insert_break(),
172+
}
173+
};
174+
}
175+
176+
push_or_break!(self.time_series_data.disk_io_read, disk.io_read_rate_bytes);
177+
push_or_break!(
178+
self.time_series_data.disk_io_write,
179+
disk.io_write_rate_bytes
180+
);
181+
}
182+
183+
for name in not_visited {
184+
if let Some(entry) = self.time_series_data.disk_io_read.get_mut(&name) {
185+
entry.insert_break();
186+
}
187+
188+
if let Some(entry) = self.time_series_data.disk_io_write.get_mut(&name) {
189+
entry.insert_break();
190+
}
191+
}
192+
}
156193
}
157194
}
158195

@@ -182,8 +219,11 @@ impl StoredData {
182219

183220
let prev_io_diff = disks.len().saturating_sub(self.prev_io.len());
184221
self.prev_io.reserve(prev_io_diff);
185-
self.prev_io.extend((0..prev_io_diff).map(|_| (0, 0)));
222+
self.prev_io.extend((0..prev_io_diff).map(|_| None));
186223

224+
// FIXME: prev_io is indexed by position (itx), not by device name. If the OS
225+
// ever returns disks in a different order between collections, rates will be
226+
// silently computed against the wrong previous value. Should be keyed by name.
187227
for (itx, device) in disks.into_iter().enumerate() {
188228
let Some(checked_name) = ({
189229
#[cfg(target_os = "windows")]
@@ -245,19 +285,21 @@ impl StoredData {
245285
let (mut io_read_rate_bytes, mut io_write_rate_bytes) = (None, None);
246286
if let Some(Some(io_device)) = io_device {
247287
if let Some(prev_io) = self.prev_io.get_mut(itx) {
248-
io_read_rate_bytes = Some(
249-
((io_device.read_bytes.saturating_sub(prev_io.0)) as f64
250-
/ time_since_last_harvest)
251-
.round() as u64,
252-
);
253-
254-
io_write_rate_bytes = Some(
255-
((io_device.write_bytes.saturating_sub(prev_io.1)) as f64
256-
/ time_since_last_harvest)
257-
.round() as u64,
258-
);
259-
260-
*prev_io = (io_device.read_bytes, io_device.write_bytes);
288+
if let Some((prev_read, prev_write)) = *prev_io {
289+
io_read_rate_bytes = Some(
290+
((io_device.read_bytes.saturating_sub(prev_read)) as f64
291+
/ time_since_last_harvest)
292+
.round() as u64,
293+
);
294+
295+
io_write_rate_bytes = Some(
296+
((io_device.write_bytes.saturating_sub(prev_write)) as f64
297+
/ time_since_last_harvest)
298+
.round() as u64,
299+
);
300+
}
301+
302+
*prev_io = Some((io_device.read_bytes, io_device.write_bytes));
261303
}
262304
}
263305

src/app/data/time_series.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ pub struct TimeSeriesData {
6565
///
6666
/// TODO: Maybe make this use TypedTemperature?
6767
pub temperature: HashMap<String, ChunkedData<f32>>,
68+
69+
/// Disk I/O read rate data, keyed by device name.
70+
pub disk_io_read: HashMap<String, ChunkedData<f64>>,
71+
72+
/// Disk I/O write rate data, keyed by device name.
73+
pub disk_io_write: HashMap<String, ChunkedData<f64>>,
6874
}
6975

7076
impl TimeSeriesData {
@@ -297,5 +303,27 @@ impl TimeSeriesData {
297303
true
298304
}
299305
});
306+
307+
self.disk_io_read.retain(|_, data| {
308+
let _ = data.prune(end);
309+
310+
if data.no_elements() {
311+
false
312+
} else {
313+
data.shrink_to_fit();
314+
true
315+
}
316+
});
317+
318+
self.disk_io_write.retain(|_, data| {
319+
let _ = data.prune(end);
320+
321+
if data.no_elements() {
322+
false
323+
} else {
324+
data.shrink_to_fit();
325+
true
326+
}
327+
});
300328
}
301329
}

src/app/layout_manager.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ pub enum BottomWidgetType {
943943
Temp,
944944
TempGraph,
945945
Disk,
946+
DiskIoGraph,
946947
BasicCpu,
947948
BasicMem,
948949
BasicNet,
@@ -958,7 +959,7 @@ impl BottomWidgetType {
958959

959960
pub fn is_widget_graph(&self) -> bool {
960961
use BottomWidgetType::*;
961-
matches!(self, Cpu | Net | Mem | TempGraph)
962+
matches!(self, Cpu | Net | Mem | TempGraph | DiskIoGraph)
962963
}
963964

964965
pub fn get_pretty_name(&self) -> &str {
@@ -990,6 +991,7 @@ impl std::str::FromStr for BottomWidgetType {
990991
"temp" | "temperature" => Ok(BottomWidgetType::Temp),
991992
"temp_graph" | "temperature_graph" => Ok(BottomWidgetType::TempGraph),
992993
"disk" => Ok(BottomWidgetType::Disk),
994+
"disk_io_graph" => Ok(BottomWidgetType::DiskIoGraph),
993995
"empty" => Ok(BottomWidgetType::Empty),
994996
#[cfg(feature = "battery")]
995997
"battery" | "batt" => Ok(BottomWidgetType::Battery),
@@ -1015,6 +1017,8 @@ Supported widget names:
10151017
+--------------------------------+
10161018
| disk |
10171019
+--------------------------------+
1020+
| disk_io_graph |
1021+
+--------------------------------+
10181022
| batt, battery |
10191023
+--------------------------------+
10201024
| empty |
@@ -1043,6 +1047,8 @@ Supported widget names:
10431047
+--------------------------------+
10441048
| disk |
10451049
+--------------------------------+
1050+
| disk_io_graph |
1051+
+--------------------------------+
10461052
| empty |
10471053
+--------------------------------+
10481054
",
@@ -1064,5 +1070,6 @@ pub struct UsedWidgets {
10641070
pub use_disk: bool,
10651071
pub use_temp: bool,
10661072
pub use_temp_graph: bool,
1073+
pub use_disk_graph: bool,
10671074
pub use_battery: bool,
10681075
}

src/app/states.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use crate::{
55
constants,
66
utils::input::InputFieldState,
77
widgets::{
8-
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
9-
ProcWidgetState, TempGraphWidgetState, TempWidgetState, query::ProcessQuery,
8+
BatteryWidgetState, CpuWidgetState, DiskIoGraphWidgetState, DiskTableWidget,
9+
MemWidgetState, NetWidgetState, ProcWidgetState, TempGraphWidgetState, TempWidgetState,
10+
query::ProcessQuery,
1011
},
1112
};
1213

@@ -18,6 +19,7 @@ pub struct AppWidgetStates {
1819
pub temp_state: TempState,
1920
pub temp_graph_state: TempGraphStates,
2021
pub disk_state: DiskState,
22+
pub disk_io_graph_state: DiskIoGraphStates,
2123
pub battery_state: AppBatteryState,
2224
pub basic_table_widget_state: Option<BasicTableWidgetState>,
2325
}
@@ -192,6 +194,21 @@ impl TempGraphStates {
192194
}
193195
}
194196

197+
/// Holds per-widget state for all disk I/O graph instances in the layout.
198+
pub struct DiskIoGraphStates {
199+
pub widget_states: HashMap<u64, DiskIoGraphWidgetState>,
200+
}
201+
202+
impl DiskIoGraphStates {
203+
pub fn init(widget_states: HashMap<u64, DiskIoGraphWidgetState>) -> Self {
204+
DiskIoGraphStates { widget_states }
205+
}
206+
207+
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskIoGraphWidgetState> {
208+
self.widget_states.get_mut(&widget_id)
209+
}
210+
}
211+
195212
pub struct DiskState {
196213
pub widget_states: HashMap<u64, DiskTableWidget>,
197214
}

src/canvas.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ impl Painter {
281281
rect[0],
282282
app_state.current_widget.widget_id,
283283
),
284+
DiskIoGraph => self.draw_disk_io_graph(
285+
f,
286+
app_state,
287+
rect[0],
288+
app_state.current_widget.widget_id,
289+
),
284290
_ => {}
285291
}
286292
} else if app_state.app_config_fields.use_basic_mode {
@@ -401,6 +407,9 @@ impl Painter {
401407
vertical_chunks[3],
402408
widget_id,
403409
),
410+
DiskIoGraph => {
411+
self.draw_disk_io_graph(f, app_state, vertical_chunks[3], widget_id)
412+
}
404413
_ => {}
405414
}
406415
}
@@ -482,6 +491,9 @@ impl Painter {
482491
TempGraph => {
483492
self.draw_temperature_graph(f, app_state, *draw_loc, widget.widget_id)
484493
}
494+
DiskIoGraph => {
495+
self.draw_disk_io_graph(f, app_state, *draw_loc, widget.widget_id)
496+
}
485497
_ => {}
486498
}
487499
}

0 commit comments

Comments
 (0)