//! Disk I/O plugin. use crate::collectors::{SECTOR_SIZE, read_diskstats}; use crate::fields; use glances_core::plugin::{Field, HistoryItem, Plugin, PluginModel, UpdateContext}; use glances_core::{Args, Config, Decoration}; use serde_json::{Value, json}; pub struct DiskioPlugin { pub model: PluginModel, } impl DiskioPlugin { pub fn new(config: &Config, args: &Args) -> Self { let fields = fields! { "disk_name" => Field::new("Disk name."), "read_count" => Field::new("Number of reads.").rate().unit("number"), "write_count" => Field::new("Number of writes.").rate().unit("number"), "read_bytes" => Field::new("Number of bytes read.").rate().unit("byte"), "write_bytes" => Field::new("Number of bytes written.").rate().unit("byte"), "read_time" => Field::new("Time spent reading.").rate().unit("millisecond"), "write_time" => Field::new("Time spent writing.").rate().unit("millisecond"), "read_latency" => Field::new("Mean time spent reading per operation.").unit("millisecond"), "write_latency" => Field::new("Mean time spent writing per operation.").unit("millisecond"), }; let items_history = vec![ HistoryItem::new("read_bytes_rate_per_sec", "Bytes read per second"), HistoryItem::new("write_bytes_rate_per_sec", "Bytes write per second"), ]; let mut model = PluginModel::new( "diskio", fields, items_history, Value::Array(Vec::new()), config, args.time.unwrap_or(2.0), args.disable_history, args.disable_config_exec, ); model.set_item_key("disk_name"); model.hide_zero = config.get_bool_value("diskio", "hide_zero", false); model.hide_threshold_bytes = config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64; model.hide_zero_fields = vec!["read_bytes_rate_per_sec".to_string(), "write_bytes_rate_per_sec".to_string()]; Self { model } } } impl Plugin for DiskioPlugin { fn name(&self) -> &str { "diskio" } fn model(&self) -> &PluginModel { &self.model } fn model_mut(&mut self) -> &mut PluginModel { &mut self.model } fn get_key(&self) -> Option<&'static str> { Some("disk_name") } fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result { let mut stats = Vec::new(); for (disk_name, disk_stat) in read_diskstats() { if !ctx.args.diskio_show_ramfs && disk_name.starts_with("ram") { continue; } if !self.model.is_display(&disk_name, "") { continue; } let mut item = serde_json::Map::new(); item.insert("read_count".to_string(), json!(disk_stat.read_count)); item.insert("write_count".to_string(), json!(disk_stat.write_count)); item.insert("read_bytes".to_string(), json!(disk_stat.sectors_read * SECTOR_SIZE)); item.insert("write_bytes".to_string(), json!(disk_stat.sectors_written * SECTOR_SIZE)); item.insert("read_time".to_string(), json!(disk_stat.read_time)); item.insert("write_time".to_string(), json!(disk_stat.write_time)); item.insert("key".to_string(), json!("disk_name")); item.insert("disk_name".to_string(), json!(disk_name)); if let Some(alias) = self.model.has_alias(&disk_name) { item.insert("alias".to_string(), json!(alias)); } stats.push(Value::Object(item)); } Ok(Value::Array(stats)) } fn post_collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<()> { if let Value::Array(items) = &mut self.model.stats { for item in items.iter_mut() { let Some(item) = item.as_object_mut() else { continue; }; let read_time = item.get("read_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let read_count = item.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let write_time = item.get("write_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let write_count = item.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let read_latency = if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 { (read_time / read_count).floor() as i64 } else { 0 }; let write_latency = if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 { (write_time / write_count).floor() as i64 } else { 0 }; item.insert("read_latency".to_string(), json!(read_latency)); item.insert("write_latency".to_string(), json!(write_latency)); } } Ok(()) } fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); let items: Vec<(String, f64, f64, f64, f64)> = match &self.model.stats { Value::Array(list) => list .iter() .filter_map(|item| { let name = item.get("disk_name")?.as_str()?.to_string(); let read_rate = item.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let write_rate = item.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let read_latency = item.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0); let write_latency = item.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0); Some((name, read_rate, write_rate, read_latency, write_latency)) }) .collect(), _ => Vec::new(), }; for (disk_name, read_rate, write_rate, read_latency, write_latency) in items { let alert_rx = self.model.get_alert(read_rate, 0.0, 100.0, true, false, Some("rx"), Some(&disk_name), false); let alert_tx = self.model.get_alert(write_rate, 0.0, 100.0, true, false, Some("tx"), Some(&disk_name), false); for field in ["read_bytes", "read_bytes_rate_per_sec"] { self.model.set_view_decoration(Some(&disk_name), field, alert_rx); } for field in ["write_bytes", "write_bytes_rate_per_sec"] { self.model.set_view_decoration(Some(&disk_name), field, alert_tx); } let mut alert_latency_rx = self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), Some(&disk_name), false); let mut alert_latency_tx = self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), Some(&disk_name), false); if alert_latency_rx == Decoration::Default { alert_latency_rx = self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), None, false); } if alert_latency_tx == Decoration::Default { alert_latency_tx = self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), None, false); } self.model.set_view_decoration(Some(&disk_name), "read_latency", alert_latency_rx); self.model.set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx); } } fn display_curse(&self) -> bool { true } fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { return ret; } let name_max_width = ctx.max_width.saturating_sub(13); ret.push(self.model.curse_add_line( format!("{:8}", "IOR/s"), glances_core::Decoration::Default, false, false, false)); ret.push(self.model.curse_add_line(format!("{:>7}", "IOW/s"), glances_core::Decoration::Default, false, false, false)); } else if ctx.args.diskio_latency { ret.push(self.model.curse_add_line(format!("{:>8}", "ms/opR"), glances_core::Decoration::Default, false, false, false)); ret.push(self.model.curse_add_line(format!("{:>7}", "ms/opW"), glances_core::Decoration::Default, false, false, false)); } else { ret.push(self.model.curse_add_line(format!("{:>8}", "R/s"), glances_core::Decoration::Default, false, false, false)); ret.push(self.model.curse_add_line(format!("{:>7}", "W/s"), glances_core::Decoration::Default, false, false, false)); } let stats = self.model.sorted_stats(); let Some(items) = stats.as_array() else { return ret; }; for i in items { let disk_real_name = i.get("disk_name").and_then(Value::as_str).unwrap_or("").to_string(); if self .model .hide_zero_fields .iter() .all(|field| self.model.view_bool(Some(&disk_real_name), field, "hidden")) { continue; } let mut disk_name = i .get("alias") .and_then(Value::as_str) .map(|s| s.to_string()) .unwrap_or_else(|| disk_real_name.clone()); if disk_name.chars().count() > name_max_width { disk_name = format!("{}_", disk_name.chars().take(name_max_width).collect::()); } ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( format!("{:7}"), self.model.view_decoration(Some(&disk_real_name), "read_count"), false, false, false)); ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_count"), false, false, false)); } else if ctx.args.diskio_latency { let rx = self.model.auto_unit(i.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K"); let tx = self.model.auto_unit(i.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K"); ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_latency"), false, false, false)); ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_latency"), false, false, false)); } else { let rx = self.model.auto_unit(i.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K"); let tx = self.model.auto_unit(i.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K"); ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_bytes"), false, false, false)); ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_bytes"), false, false, false)); } } ret } }