Skip to main content

monitrs_core/model/
storage.rs

1//! Filesystem capacity and block-device throughput.
2//!
3//! §7.3 and §26 both insist these are *different metrics*. A filesystem that is
4//! 95% full is not busy, and a device saturated at 100% utilization may sit on a
5//! nearly empty filesystem. They are therefore separate types, and no code path
6//! can accidentally render both as one unlabelled percentage.
7
8use crate::model::MetricState;
9use crate::units::{Percent, Rate};
10
11/// What kind of filesystem a mount point is backed by.
12///
13/// Used by the removable/virtual filter in §7.3, not for styling.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
17pub enum FilesystemKind {
18    /// A local block device.
19    Physical,
20    /// A local device that can be unplugged.
21    Removable,
22    /// A network mount such as NFS or SMB.
23    ///
24    /// Capacity reads on these can block for seconds, which is why filesystem
25    /// capacity lives in the medium tier rather than the fast one (§8.6).
26    Network,
27    /// A kernel pseudo-filesystem such as `tmpfs`, `devfs`, or `overlay`.
28    Virtual,
29    /// Not classifiable from the available information.
30    #[default]
31    Unknown,
32}
33
34impl FilesystemKind {
35    /// Whether this mount is hidden by default in the Storage screen.
36    #[must_use]
37    pub const fn hidden_by_default(self) -> bool {
38        matches!(self, Self::Virtual)
39    }
40}
41
42/// Capacity of one mounted filesystem.
43///
44/// Contains no throughput fields at all; that is [`DiskSnapshot`]'s job.
45#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub struct FilesystemSnapshot {
48    /// Where it is mounted.
49    pub mount_point: Box<str>,
50    /// The backing device, where the platform maps it.
51    pub device: Option<Box<str>>,
52    /// Filesystem type, e.g. `apfs`, `ext4`, `overlay`.
53    pub fs_type: Option<Box<str>>,
54    /// Total capacity.
55    pub total_bytes: u64,
56    /// Space available to the current user.
57    ///
58    /// Usually smaller than `total - used` because of reserved blocks.
59    pub available_bytes: MetricState<u64>,
60    /// Space in use.
61    pub used_bytes: MetricState<u64>,
62    /// Share of capacity used. Never mixed with device utilization (§7.3).
63    pub usage: MetricState<Percent>,
64    /// How the mount is classified.
65    pub kind: FilesystemKind,
66    /// Whether the mount is read-only.
67    pub read_only: bool,
68}
69
70/// Cumulative device counters, kept alongside rates so the Inspect screen can
71/// show totals as well as throughput.
72#[derive(Clone, Copy, Debug, Default, PartialEq)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize))]
74pub struct DiskTotals {
75    /// Bytes read since boot.
76    pub read_bytes: u64,
77    /// Bytes written since boot.
78    pub write_bytes: u64,
79}
80
81/// Throughput of one block device.
82///
83/// Contains no capacity fields at all; that is [`FilesystemSnapshot`]'s job.
84#[derive(Clone, Debug, PartialEq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86pub struct DiskSnapshot {
87    /// Kernel device name, e.g. `nvme0n1` or `disk0`.
88    pub device: Box<str>,
89    /// Hardware model, where reported.
90    pub model: Option<Box<str>>,
91    /// Read throughput.
92    pub read: MetricState<Rate>,
93    /// Write throughput.
94    pub write: MetricState<Rate>,
95    /// Read operations per second.
96    pub read_ops: MetricState<Rate>,
97    /// Write operations per second.
98    pub write_ops: MetricState<Rate>,
99    /// Fraction of wall time the device had at least one request in flight.
100    ///
101    /// §7.3 limits this to platforms where it is *semantically correct*: it is
102    /// derived from `/proc/diskstats` field 10 on Linux and is
103    /// [`MetricState::Unsupported`] elsewhere, because a queue-depth-based
104    /// approximation on an NVMe device is misleading rather than merely
105    /// imprecise.
106    pub busy: MetricState<Percent>,
107    /// Average in-flight request count.
108    pub queue_length: MetricState<f32>,
109    /// Cumulative counters.
110    pub totals: MetricState<DiskTotals>,
111    /// Mount points backed by this device, where the mapping is available.
112    ///
113    /// §8.6 puts this expensive mapping in the on-demand tier, so it is often
114    /// empty in a fast-tier snapshot.
115    pub mount_points: Vec<Box<str>>,
116}
117
118impl DiskSnapshot {
119    /// A device whose counters exist but whose rates need a second sample.
120    #[must_use]
121    pub fn warming_up(device: Box<str>) -> Self {
122        Self {
123            device,
124            model: None,
125            read: MetricState::WarmingUp,
126            write: MetricState::WarmingUp,
127            read_ops: MetricState::WarmingUp,
128            write_ops: MetricState::WarmingUp,
129            busy: MetricState::WarmingUp,
130            queue_length: MetricState::WarmingUp,
131            totals: MetricState::WarmingUp,
132            mount_points: Vec::new(),
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn virtual_filesystems_are_hidden_by_default_and_real_ones_are_not() {
143        assert!(FilesystemKind::Virtual.hidden_by_default());
144        for kind in [
145            FilesystemKind::Physical,
146            FilesystemKind::Removable,
147            FilesystemKind::Network,
148            FilesystemKind::Unknown,
149        ] {
150            assert!(!kind.hidden_by_default(), "{kind:?}");
151        }
152    }
153
154    #[test]
155    fn a_warming_up_device_reports_no_throughput_and_no_busy_percentage() {
156        let disk = DiskSnapshot::warming_up("nvme0n1".into());
157        assert!(disk.read.fresh().is_none());
158        assert!(disk.busy.fresh().is_none());
159        assert!(disk.mount_points.is_empty());
160    }
161
162    /// The type system is what keeps §7.3 honest: neither struct can express the
163    /// other's metric, so no widget can conflate them by accident.
164    #[test]
165    fn capacity_and_throughput_live_in_separate_types() {
166        fn assert_fields<T>(_: &T) {}
167        let fs = FilesystemSnapshot {
168            mount_point: "/".into(),
169            device: Some("disk3s1s1".into()),
170            fs_type: Some("apfs".into()),
171            total_bytes: 494_384_795_648,
172            available_bytes: MetricState::Available(120_000_000_000),
173            used_bytes: MetricState::Available(374_384_795_648),
174            usage: Percent::ratio(374_384_795_648, 494_384_795_648)
175                .map_or(MetricState::Unsupported, MetricState::Available),
176            kind: FilesystemKind::Physical,
177            read_only: false,
178        };
179        assert_fields(&fs);
180        assert!(fs.usage.fresh().is_some());
181        let disk = DiskSnapshot::warming_up("disk0".into());
182        assert_fields(&disk);
183    }
184}