Skip to main content

vole_document/store/
io.rs

1//! Physical I/O accounting for observations (Phase 11.9 review fix #1).
2//!
3//! An observation is only honest about its cost if every blob it makes the OS
4//! read is counted. Before this module, [`ObserveStats`] counted only the
5//! procedural *seed* universe while the observation silently also read the whole
6//! descriptor blob and its manifest. This is a small, shared, per-process set of
7//! byte counters — one per physical read class — so a caller can attribute the
8//! bytes actually fetched to exactly one observation.
9//!
10//! The counters are interior-mutable ([`Cell`]) and shareable via [`Rc`], so a
11//! store handle can hand a second handle to a sub-store (seed/index) and still
12//! see every read. They are **not** `Sync`; a field store is a single-threaded
13//! object, which is how the courts already use it.
14//!
15//! These counters measure bytes *fetched from the filesystem by this process*.
16//! They do not measure page-cache hits, mmap'd libraries, or CPU time; those are
17//! reported separately (or not at all) elsewhere.
18
19use std::cell::Cell;
20use std::rc::Rc;
21
22/// A shared set of physical-read byte counters, one per class.
23///
24/// Cloning shares the same counters; use [`IoCounters::handle`] to make the
25/// sharing explicit.
26#[derive(Debug, Clone, Default)]
27pub struct IoCounters {
28    inner: Rc<IoInner>,
29}
30
31#[derive(Debug, Default)]
32struct IoInner {
33    descriptor_bytes: Cell<u64>,
34    manifest_bytes: Cell<u64>,
35    index_bytes: Cell<u64>,
36    seed_bytes: Cell<u64>,
37    descriptor_reads: Cell<u64>,
38}
39
40impl IoCounters {
41    /// A fresh, independent counter set.
42    pub fn new() -> Self {
43        IoCounters {
44            inner: Rc::new(IoInner::default()),
45        }
46    }
47
48    /// A second handle that shares this counter set.
49    pub fn handle(&self) -> Self {
50        IoCounters {
51            inner: Rc::clone(&self.inner),
52        }
53    }
54
55    /// Note a descriptor blob read of `bytes` bytes.
56    pub fn add_descriptor(&self, bytes: u64) {
57        self.inner
58            .descriptor_bytes
59            .set(self.inner.descriptor_bytes.get().saturating_add(bytes));
60        self.inner
61            .descriptor_reads
62            .set(self.inner.descriptor_reads.get().saturating_add(1));
63    }
64
65    /// Note a field manifest read of `bytes` bytes.
66    pub fn add_manifest(&self, bytes: u64) {
67        self.inner
68            .manifest_bytes
69            .set(self.inner.manifest_bytes.get().saturating_add(bytes));
70    }
71
72    /// Note a hierarchical index node read of `bytes` bytes.
73    pub fn add_index(&self, bytes: u64) {
74        self.inner
75            .index_bytes
76            .set(self.inner.index_bytes.get().saturating_add(bytes));
77    }
78
79    /// Note a seed node read of `bytes` bytes.
80    pub fn add_seed(&self, bytes: u64) {
81        self.inner
82            .seed_bytes
83            .set(self.inner.seed_bytes.get().saturating_add(bytes));
84    }
85
86    /// Number of descriptor blobs fetched so far (a read *count*, not bytes).
87    pub fn descriptor_reads(&self) -> u64 {
88        self.inner.descriptor_reads.get()
89    }
90
91    /// A consistent snapshot of every counter.
92    pub fn snapshot(&self) -> IoSnapshot {
93        IoSnapshot {
94            descriptor_bytes: self.inner.descriptor_bytes.get(),
95            manifest_bytes: self.inner.manifest_bytes.get(),
96            index_bytes: self.inner.index_bytes.get(),
97            seed_bytes: self.inner.seed_bytes.get(),
98        }
99    }
100}
101
102/// A point-in-time copy of [`IoCounters`]. Subtract two to attribute bytes to an
103/// interval.
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub struct IoSnapshot {
106    /// Descriptor-blob bytes read.
107    pub descriptor_bytes: u64,
108    /// Field-manifest bytes read.
109    pub manifest_bytes: u64,
110    /// Hierarchical-index-node bytes read.
111    pub index_bytes: u64,
112    /// Seed-node bytes read.
113    pub seed_bytes: u64,
114}
115
116impl IoSnapshot {
117    /// Bytes fetched in the interval `(self, later]`, saturating at zero so a
118    /// counter reset can never underflow into a fabricated huge number.
119    pub fn delta(&self, later: &IoSnapshot) -> IoSnapshot {
120        IoSnapshot {
121            descriptor_bytes: later.descriptor_bytes.saturating_sub(self.descriptor_bytes),
122            manifest_bytes: later.manifest_bytes.saturating_sub(self.manifest_bytes),
123            index_bytes: later.index_bytes.saturating_sub(self.index_bytes),
124            seed_bytes: later.seed_bytes.saturating_sub(self.seed_bytes),
125        }
126    }
127
128    /// The total physical bytes across every class.
129    pub fn total(&self) -> u64 {
130        self.descriptor_bytes
131            .saturating_add(self.manifest_bytes)
132            .saturating_add(self.index_bytes)
133            .saturating_add(self.seed_bytes)
134    }
135
136    /// Add `other` class-by-class.
137    pub fn plus(&self, other: &IoSnapshot) -> IoSnapshot {
138        IoSnapshot {
139            descriptor_bytes: self.descriptor_bytes.saturating_add(other.descriptor_bytes),
140            manifest_bytes: self.manifest_bytes.saturating_add(other.manifest_bytes),
141            index_bytes: self.index_bytes.saturating_add(other.index_bytes),
142            seed_bytes: self.seed_bytes.saturating_add(other.seed_bytes),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn handles_share_and_delta_sums_correctly() {
153        let a = IoCounters::new();
154        let b = a.handle();
155        a.add_descriptor(100);
156        b.add_manifest(10);
157        b.add_index(7);
158        b.add_seed(3);
159        let s = a.snapshot();
160        assert_eq!(s.descriptor_bytes, 100);
161        assert_eq!(s.manifest_bytes, 10);
162        assert_eq!(s.index_bytes, 7);
163        assert_eq!(s.seed_bytes, 3);
164        assert_eq!(s.total(), 120);
165        assert_eq!(a.descriptor_reads(), 1);
166        assert_eq!(b.descriptor_reads(), 1);
167    }
168
169    #[test]
170    fn independent_counters_do_not_leak() {
171        let a = IoCounters::new();
172        let b = IoCounters::new();
173        a.add_seed(5);
174        assert_eq!(b.snapshot().total(), 0);
175    }
176
177    #[test]
178    fn delta_is_non_negative_and_additive() {
179        let c = IoCounters::new();
180        let before = c.snapshot();
181        c.add_descriptor(4);
182        c.add_seed(9);
183        let after = c.snapshot();
184        let d = before.delta(&after);
185        assert_eq!(d.descriptor_bytes, 4);
186        assert_eq!(d.seed_bytes, 9);
187        assert_eq!(d.total(), 13);
188        // A reversed interval saturates rather than underflowing.
189        assert_eq!(after.delta(&before).total(), 0);
190        assert_eq!(d.plus(&d).total(), 26);
191    }
192}