1use crate::{
2 Format, RunContext, Runnable,
3 util::{open_path, print_json},
4};
5use anyhow::{Context, Result, bail};
6use btrfs_uapi::{
7 device::{DeviceStats, device_info_all, device_stats},
8 filesystem::filesystem_info,
9};
10use clap::Parser;
11use cols::Cols;
12use serde::Serialize;
13use std::{os::unix::io::AsFd, path::PathBuf};
14
15#[derive(Parser, Debug)]
24#[allow(clippy::doc_markdown)]
25pub struct DeviceStatsCommand {
26 #[clap(long, short)]
28 pub check: bool,
29
30 #[clap(long, short = 'z', conflicts_with = "offline")]
32 pub reset: bool,
33
34 #[clap(short = 'T')]
36 pub tabular: bool,
37
38 #[clap(long)]
40 pub offline: bool,
41
42 pub path: PathBuf,
44}
45
46#[derive(Serialize)]
47struct StatsJson {
48 device: String,
49 devid: u64,
50 write_io_errs: u64,
51 read_io_errs: u64,
52 flush_io_errs: u64,
53 corruption_errs: u64,
54 generation_errs: u64,
55}
56
57impl StatsJson {
58 fn from_uapi(path: &str, stats: &DeviceStats) -> Self {
59 Self {
60 device: path.to_string(),
61 devid: stats.devid,
62 write_io_errs: stats.write_errs,
63 read_io_errs: stats.read_errs,
64 flush_io_errs: stats.flush_errs,
65 corruption_errs: stats.corruption_errs,
66 generation_errs: stats.generation_errs,
67 }
68 }
69
70 fn from_disk(
71 path: &str,
72 devid: u64,
73 stats: &btrfs_disk::items::DeviceStats,
74 ) -> Self {
75 Self {
76 device: path.to_string(),
77 devid,
78 write_io_errs: stats.values.first().map_or(0, |v| v.1),
79 read_io_errs: stats.values.get(1).map_or(0, |v| v.1),
80 flush_io_errs: stats.values.get(2).map_or(0, |v| v.1),
81 corruption_errs: stats.values.get(3).map_or(0, |v| v.1),
82 generation_errs: stats.values.get(4).map_or(0, |v| v.1),
83 }
84 }
85
86 fn is_clean(&self) -> bool {
87 self.write_io_errs == 0
88 && self.read_io_errs == 0
89 && self.flush_io_errs == 0
90 && self.corruption_errs == 0
91 && self.generation_errs == 0
92 }
93}
94
95impl Runnable for DeviceStatsCommand {
96 fn supported_formats(&self) -> &[Format] {
97 &[Format::Text, Format::Json, Format::Modern]
98 }
99
100 fn run(&self, ctx: &RunContext) -> Result<()> {
101 if self.offline {
102 return self.run_offline(ctx.format);
103 }
104
105 let file = open_path(&self.path)?;
106 let fd = file.as_fd();
107
108 let fs = filesystem_info(fd).with_context(|| {
109 format!(
110 "failed to get filesystem info for '{}'",
111 self.path.display()
112 )
113 })?;
114
115 let devices = device_info_all(fd, &fs).with_context(|| {
116 format!("failed to get device info for '{}'", self.path.display())
117 })?;
118
119 if devices.is_empty() {
120 bail!("no devices found for '{}'", self.path.display());
121 }
122
123 let mut all_stats: Vec<StatsJson> = Vec::new();
124 let mut any_nonzero = false;
125
126 for dev in &devices {
127 let stats =
128 device_stats(fd, dev.devid, self.reset).with_context(|| {
129 format!(
130 "failed to get stats for device {} ({})",
131 dev.devid, dev.path
132 )
133 })?;
134
135 let entry = StatsJson::from_uapi(&dev.path, &stats);
136 if !entry.is_clean() {
137 any_nonzero = true;
138 }
139 all_stats.push(entry);
140 }
141
142 match ctx.format {
143 Format::Modern => print_stats_modern(&all_stats),
144 Format::Text if self.tabular => {
145 print_stats_tabular(&all_stats);
146 }
147 Format::Text => {
148 for s in &all_stats {
149 print_stats_text(s);
150 }
151 }
152 Format::Json => {
153 print_json("device-stats", &all_stats)?;
154 }
155 }
156
157 if self.check && any_nonzero {
158 bail!("one or more devices have non-zero error counters");
159 }
160
161 Ok(())
162 }
163}
164
165impl DeviceStatsCommand {
166 fn run_offline(&self, format: Format) -> Result<()> {
167 use btrfs_disk::{
168 items::DeviceStats as DiskDeviceStats,
169 reader::{self, Traversal},
170 tree::{KeyType, TreeBlock},
171 };
172
173 let file = open_path(&self.path)?;
174 let fs = reader::filesystem_open(file).with_context(|| {
175 format!(
176 "failed to open btrfs filesystem on '{}'",
177 self.path.display()
178 )
179 })?;
180
181 let dev_tree_id = u64::from(btrfs_disk::raw::BTRFS_DEV_TREE_OBJECTID);
182 let (dev_root, _) = fs
183 .tree_roots
184 .get(&dev_tree_id)
185 .context("device tree not found")?;
186 let dev_root = *dev_root;
187
188 let header_size = std::mem::size_of::<btrfs_disk::raw::btrfs_header>();
189 let path_str = self.path.display().to_string();
190
191 let mut all_stats: Vec<StatsJson> = Vec::new();
192 let mut block_reader = fs.reader;
193
194 reader::tree_walk(
195 &mut block_reader,
196 dev_root,
197 Traversal::Dfs,
198 &mut |block| {
199 if let TreeBlock::Leaf { items, data, .. } = block {
200 for item in items {
201 if item.key.key_type == KeyType::PersistentItem {
202 let start = header_size + item.offset as usize;
203 let end = start + item.size as usize;
204 if end <= data.len() {
205 let ds =
206 DiskDeviceStats::parse(&data[start..end]);
207 let devid = item.key.offset;
208 all_stats.push(StatsJson::from_disk(
209 &path_str, devid, &ds,
210 ));
211 }
212 }
213 }
214 }
215 },
216 )
217 .with_context(|| {
218 format!("failed to walk device tree on '{}'", self.path.display())
219 })?;
220
221 if all_stats.is_empty() {
222 all_stats.push(StatsJson::from_disk(
223 &path_str,
224 1,
225 &DiskDeviceStats::parse(&[]),
226 ));
227 }
228
229 let any_nonzero = all_stats.iter().any(|s| !s.is_clean());
230
231 match format {
232 Format::Modern => print_stats_modern(&all_stats),
233 Format::Text if self.tabular => {
234 print_stats_tabular(&all_stats);
235 }
236 Format::Text => {
237 for s in &all_stats {
238 let label = format!("{}.devid.{}", s.device, s.devid);
239 print_stats_text_labeled(&label, s);
240 }
241 }
242 Format::Json => {
243 print_json("device-stats", &all_stats)?;
244 }
245 }
246
247 if self.check && any_nonzero {
248 bail!("one or more devices have non-zero error counters");
249 }
250
251 Ok(())
252 }
253}
254
255#[derive(Cols)]
256struct StatsRow {
257 #[column(header = "ID", right)]
258 devid: u64,
259 #[column(header = "WRITE_ERR", right)]
260 write_errs: u64,
261 #[column(header = "READ_ERR", right)]
262 read_errs: u64,
263 #[column(header = "FLUSH_ERR", right)]
264 flush_errs: u64,
265 #[column(header = "CORRUPT_ERR", right)]
266 corruption_errs: u64,
267 #[column(header = "GEN_ERR", right)]
268 generation_errs: u64,
269 #[column(header = "PATH", wrap)]
270 path: String,
271}
272
273impl StatsRow {
274 fn from_json(s: &StatsJson) -> Self {
275 Self {
276 devid: s.devid,
277 path: s.device.clone(),
278 write_errs: s.write_io_errs,
279 read_errs: s.read_io_errs,
280 flush_errs: s.flush_io_errs,
281 corruption_errs: s.corruption_errs,
282 generation_errs: s.generation_errs,
283 }
284 }
285}
286
287fn print_stats_modern(stats: &[StatsJson]) {
289 let rows: Vec<StatsRow> = stats.iter().map(StatsRow::from_json).collect();
290 let mut out = std::io::stdout().lock();
291 let _ = StatsRow::print_table(&rows, &mut out);
292}
293
294fn print_stats_tabular(stats: &[StatsJson]) {
296 const HEADERS: [&str; 7] = [
297 "Id",
298 "Path",
299 "Write errors",
300 "Read errors",
301 "Flush errors",
302 "Corruption errors",
303 "Generation errors",
304 ];
305
306 let rows: Vec<[String; 7]> = stats
307 .iter()
308 .map(|s| {
309 [
310 s.devid.to_string(),
311 s.device.clone(),
312 s.write_io_errs.to_string(),
313 s.read_io_errs.to_string(),
314 s.flush_io_errs.to_string(),
315 s.corruption_errs.to_string(),
316 s.generation_errs.to_string(),
317 ]
318 })
319 .collect();
320
321 let mut widths = HEADERS.map(str::len);
322 for row in &rows {
323 for (i, cell) in row.iter().enumerate() {
324 widths[i] = widths[i].max(cell.len());
325 }
326 }
327
328 for (i, hdr) in HEADERS.iter().enumerate() {
330 if i > 0 {
331 print!(" ");
332 }
333 print!("{hdr:<w$}", w = widths[i]);
334 }
335 println!();
336
337 for (i, w) in widths.iter().enumerate() {
339 if i > 0 {
340 print!(" ");
341 }
342 print!("{}", "-".repeat(*w));
343 }
344 println!();
345
346 for row in &rows {
348 for (i, cell) in row.iter().enumerate() {
349 if i > 0 {
350 print!(" ");
351 }
352 print!("{cell:>w$}", w = widths[i]);
353 }
354 println!();
355 }
356}
357
358fn print_stats_text(s: &StatsJson) {
359 let p = &s.device;
360 println!("[{p}].{:<24} {}", "write_io_errs", s.write_io_errs);
361 println!("[{p}].{:<24} {}", "read_io_errs", s.read_io_errs);
362 println!("[{p}].{:<24} {}", "flush_io_errs", s.flush_io_errs);
363 println!("[{p}].{:<24} {}", "corruption_errs", s.corruption_errs);
364 println!("[{p}].{:<24} {}", "generation_errs", s.generation_errs);
365}
366
367fn print_stats_text_labeled(label: &str, s: &StatsJson) {
368 println!("[{label}].{:<24} {}", "write_io_errs", s.write_io_errs);
369 println!("[{label}].{:<24} {}", "read_io_errs", s.read_io_errs);
370 println!("[{label}].{:<24} {}", "flush_io_errs", s.flush_io_errs);
371 println!("[{label}].{:<24} {}", "corruption_errs", s.corruption_errs);
372 println!("[{label}].{:<24} {}", "generation_errs", s.generation_errs);
373}