Skip to content

Commit b86d292

Browse files
committed
feat: color device Err cell by session-baseline (#6)
Each device's io_errors value is snapshotted at app startup. The cell renders bold red when the count has grown during this session (actively failing) and dim when non-zero but unchanged (historic). Lets the user tell at a glance whether a device's error count is dead history or still climbing — without bcachefs needing to add a timestamp or clear-errors knob upstream. Same logic applies to the TOTAL row.
1 parent 56d479c commit b86d292

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Built for [NASty](https://github.com/nasty-project/nasty) but works on any syste
2020
- **Multi-filesystem support** — press `f` to cycle between mounted bcachefs filesystems
2121
- **Process IO view** showing which processes are doing IO
2222
- **Journal fill %**, load average, reconcile progress
23+
- **Session-aware error counts**: the device Err column dims pre-existing counts and turns bold red only when errors grow during the current run — so you can tell at a glance whether a number is dead history or actively climbing
2324
- **Consistent color scheme**: yellow = read, blue = write, red = errors/stalls
2425

2526
## Install

src/app.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,20 @@ pub struct App {
7575
pub show_help: bool,
7676
/// Scroll offset for counter/time stats/process views.
7777
pub view_scroll: usize,
78+
/// Per-device io_errors value when first observed in this session.
79+
/// Used to color the Err cell: matching baseline = historic (dim),
80+
/// growing = active (red).
81+
pub initial_errors: std::collections::HashMap<String, u64>,
7882
}
7983

8084
impl App {
8185
pub fn new(all_fs: Vec<BcachefsFs>, fs_index: usize) -> Self {
8286
let fs = all_fs[fs_index].clone();
8387
let snap = sysfs::snapshot(&fs);
8488
let tuning = TuningState::new(&snap.options);
89+
let initial_errors = snap.devices.iter()
90+
.map(|d| (d.name.clone(), d.io_errors))
91+
.collect();
8592
Self {
8693
fs,
8794
current: snap,
@@ -112,6 +119,7 @@ impl App {
112119
view_scroll: 0,
113120
dismissed_temp: Vec::new(),
114121
dismissed_permanent: std::collections::HashSet::new(),
122+
initial_errors,
115123
}
116124
}
117125

@@ -123,6 +131,12 @@ impl App {
123131

124132
let new_snap = sysfs::snapshot(&self.fs);
125133

134+
// Baseline error counts for any newly-appearing devices so we
135+
// don't flag pre-existing errors on a device added mid-session.
136+
for d in &new_snap.devices {
137+
self.initial_errors.entry(d.name.clone()).or_insert(d.io_errors);
138+
}
139+
126140
// Compute rates from previous snapshot
127141
let rates = metrics::compute_rates(&self.current, &new_snap, dt);
128142

@@ -344,6 +358,9 @@ impl App {
344358
self.fs = self.all_fs[self.fs_index].clone();
345359
let snap = sysfs::snapshot(&self.fs);
346360
self.tuning = TuningState::new(&snap.options);
361+
self.initial_errors = snap.devices.iter()
362+
.map(|d| (d.name.clone(), d.io_errors))
363+
.collect();
347364
self.current = snap;
348365
self.previous = None;
349366
self.rates = None;

src/ui.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ fn draw_device_table(
447447
let mut sr = [0.0_f64; 5];
448448
let mut sw = [0.0_f64; 5];
449449
let mut sum_errs = 0u64;
450+
let mut sum_new_errs = 0u64;
450451

451452
struct DevData {
452453
name: String,
@@ -460,6 +461,7 @@ fn draw_device_table(
460461
read_active: bool,
461462
write_active: bool,
462463
errors: u64,
464+
new_errors: u64,
463465
util_pct: f64,
464466
}
465467
let mut devs: Vec<DevData> = Vec::new();
@@ -474,12 +476,15 @@ fn draw_device_table(
474476
sr[4] += d.read_bytes_sec;
475477
sw[4] += d.write_bytes_sec;
476478
sum_errs += d.io_errors;
479+
let baseline = app.initial_errors.get(&d.name).copied().unwrap_or(d.io_errors);
480+
let new_errors = d.io_errors.saturating_sub(baseline);
481+
sum_new_errs += new_errors;
477482
devs.push(DevData {
478483
name: d.name.clone(), label: d.label.clone(), rv, wv,
479484
read_total: d.read_bytes_sec, write_total: d.write_bytes_sec,
480485
read_lat: d.read_latency_ns, write_lat: d.write_latency_ns,
481486
read_active: d.read_active, write_active: d.write_active,
482-
errors: d.io_errors, util_pct: d.util_pct,
487+
errors: d.io_errors, new_errors, util_pct: d.util_pct,
483488
});
484489
}
485490
}
@@ -498,7 +503,16 @@ fn draw_device_table(
498503
}.style(theme::bold(theme::FG));
499504

500505
let mut rows: Vec<Row> = devs.iter().enumerate().map(|(i, d)| {
501-
let es = if d.errors > 0 { Style::default().fg(theme::RED) } else { Style::default().fg(theme::FG) };
506+
// Err cell color: red+bold if errors grew this session (active
507+
// failure), dim if non-zero but unchanged (historic), default fg
508+
// if zero.
509+
let es = if d.new_errors > 0 {
510+
Style::default().fg(theme::RED).add_modifier(Modifier::BOLD)
511+
} else if d.errors > 0 {
512+
theme::dim()
513+
} else {
514+
Style::default().fg(theme::FG)
515+
};
502516
let uc = if d.util_pct > 80.0 { theme::RED } else if d.util_pct > 50.0 { theme::YELLOW } else { theme::GREEN };
503517
let util_s = if d.util_pct > 0.0 { format!("{:.0}%", d.util_pct) } else { "\u{2014}".into() };
504518
let util_cell = Cell::new(util_s).style(Style::default().fg(uc));
@@ -519,17 +533,24 @@ fn draw_device_table(
519533
}
520534
}).collect();
521535
let total_style = theme::bold(theme::CYAN);
536+
let total_err_style = if sum_new_errs > 0 {
537+
Style::default().fg(theme::RED).add_modifier(Modifier::BOLD)
538+
} else if sum_errs > 0 {
539+
theme::dim()
540+
} else {
541+
total_style
542+
};
522543
if has_labels {
523544
rows.push(Row::new(vec![
524545
Cell::new("TOTAL").style(total_style),
525546
Cell::new(""),
526-
Cell::new(format!("{}", sum_errs)).style(total_style),
547+
Cell::new(format!("{}", sum_errs)).style(total_err_style),
527548
Cell::new(""),
528549
]));
529550
} else {
530551
rows.push(Row::new(vec![
531552
Cell::new("TOTAL").style(total_style),
532-
Cell::new(format!("{}", sum_errs)).style(total_style),
553+
Cell::new(format!("{}", sum_errs)).style(total_err_style),
533554
Cell::new(""),
534555
]));
535556
}

0 commit comments

Comments
 (0)