Skip to main content

fs_core/
counting_device.rs

1//! A device that counts what a driver asks of it.
2//!
3//! # Why this is in the shared crate rather than a test file
4//!
5//! Every driver in this family is about to be measured and then made
6//! faster, and a measurement is only worth having if the drivers can be
7//! compared against each other and against themselves later. Two
8//! drivers each counting reads with their own wrapper would produce two
9//! numbers that look alike and are not: one might count a read of a
10//! whole extent as one, the other as one per block, and nothing in
11//! either number would say so.
12//!
13//! One instrument, in the crate every driver already depends on. It
14//! wraps a [`BlockRead`] and forwards every call, so a driver mounted
15//! on it behaves exactly as it would otherwise.
16//!
17//! # What the numbers mean
18//!
19//! - **reads** — calls to [`BlockRead::read_at`]. This is the number a
20//!   cache moves: a metadata block read twice is two reads here and one
21//!   after a cache is put underneath.
22//! - **bytes** — the size of every buffer those calls asked to have
23//!   filled. This is not the same number as the read count, and that is
24//!   why both are here: a driver that reads a 4 KiB block to look at
25//!   8 bytes of it is one read either way, and only the byte count
26//!   shows the 4 KiB it asked the device for to use 8.
27//!
28//! Both are worth having. A change that halves reads and doubles bytes
29//! is a readahead that guessed wrong, and one number alone would call
30//! it a win.
31//!
32//! # Both numbers are what was asked for, not what moved
33//!
34//! `bytes` is the buffer a call presented, added before the call is
35//! forwarded and never adjusted afterwards, so a read the device
36//! refuses contributes its whole buffer. That is the same rule as
37//! `reads`, for the same reason: a driver looping on an out-of-range
38//! offset is exactly the shape these numbers exist to make visible, and
39//! a counter that sat still through it would hide the loop.
40//!
41//! It is worth being plain that this is the only rule available, rather
42//! than a convenience. **Bytes-moved is not reachable through
43//! [`BlockRead`] at all**: [`BlockRead::read_at`] returns `Result<()>`
44//! and carries no transfer count, so the only place a real figure ever
45//! appears is `got` inside [`crate::Error::ShortRead`] — off the
46//! success path, and absent from the other errors an over-read can
47//! produce. Counting only on `Ok` would not recover it either, because
48//! a refused read is not reliably a transfer of nothing: a
49//! [`crate::FileDevice`] copies the readable prefix into the buffer
50//! before reporting the shortfall, so a 64-byte request against a
51//! 16-byte file really moves 16 bytes, while the same request against
52//! an in-memory device moves none. Counting on `Ok` reports 0 for both;
53//! this counter reports 64 for both.
54//!
55//! Reporting the request is therefore the one answer that does not
56//! depend on which device happens to be underneath, which is what makes
57//! two drivers' numbers comparable — the whole point of the module. The
58//! price is that these counters are not a transfer total for a run with
59//! failed reads in it, and should not be read as one.
60
61use crate::block::BlockRead;
62use crate::error::Result;
63use std::sync::atomic::{AtomicU64, Ordering};
64use std::sync::Arc;
65
66/// Wraps a device and counts the reads passing through it.
67///
68/// The counters are atomic and the type is `Sync`, so a driver reading
69/// from several threads is measured correctly rather than approximately.
70pub struct CountingDevice {
71    inner: Arc<dyn BlockRead>,
72    reads: AtomicU64,
73    bytes: AtomicU64,
74}
75
76impl CountingDevice {
77    /// Wrap `inner`, counting from zero.
78    pub fn new(inner: Arc<dyn BlockRead>) -> Self {
79        CountingDevice {
80            inner,
81            reads: AtomicU64::new(0),
82            bytes: AtomicU64::new(0),
83        }
84    }
85
86    /// How many times the driver called `read_at`.
87    pub fn reads(&self) -> u64 {
88        self.reads.load(Ordering::Relaxed)
89    }
90
91    /// How many bytes those calls asked for, including the buffers of
92    /// reads the device refused.
93    ///
94    /// This is the request, not the transfer. See the module header for
95    /// why bytes-moved is not reachable through [`BlockRead`] and why
96    /// counting the request is what makes two drivers comparable.
97    pub fn bytes(&self) -> u64 {
98        self.bytes.load(Ordering::Relaxed)
99    }
100
101    /// Start counting again from zero.
102    ///
103    /// A mount reads a superblock and headers before the work being
104    /// measured begins, and counting that in makes a small operation
105    /// look like a large one. Reset after mounting, measure the
106    /// operation, read the counters.
107    pub fn reset(&self) {
108        self.reads.store(0, Ordering::Relaxed);
109        self.bytes.store(0, Ordering::Relaxed);
110    }
111}
112
113impl BlockRead for CountingDevice {
114    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
115        self.reads.fetch_add(1, Ordering::Relaxed);
116        self.bytes.fetch_add(buf.len() as u64, Ordering::Relaxed);
117        self.inner.read_at(offset, buf)
118    }
119
120    fn size_bytes(&self) -> u64 {
121        self.inner.size_bytes()
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::test_device::Bytes;
129
130    fn device(len: usize) -> Arc<CountingDevice> {
131        Arc::new(CountingDevice::new(Arc::new(Bytes::new(vec![7u8; len]))))
132    }
133
134    /// One call is one read, whatever it asked for, and the bytes are
135    /// what the buffer wanted rather than what the device holds.
136    #[test]
137    fn it_counts_calls_and_the_bytes_they_asked_for() {
138        let dev = device(4096);
139        let mut small = [0u8; 8];
140        let mut block = [0u8; 512];
141
142        dev.read_at(0, &mut small).expect("read");
143        assert_eq!((dev.reads(), dev.bytes()), (1, 8));
144
145        dev.read_at(1024, &mut block).expect("read");
146        assert_eq!(
147            (dev.reads(), dev.bytes()),
148            (2, 520),
149            "two calls, and the bytes are the sum of both buffers"
150        );
151    }
152
153    /// A read the device refuses is still a read the driver made.
154    ///
155    /// The point of the count is what the driver ASKED for, so a failed
156    /// call belongs in it: a driver looping on an out-of-range offset is
157    /// exactly the shape this is here to make visible.
158    #[test]
159    fn a_failed_read_still_counts() {
160        let dev = device(16);
161        let mut buf = [0u8; 64];
162        assert!(dev.read_at(0, &mut buf).is_err(), "past the end");
163        assert_eq!(dev.reads(), 1, "the driver asked, so it counts");
164    }
165
166    /// The byte counter follows the same rule as the read counter, and
167    /// nothing was pinning that.
168    ///
169    /// A refused read leaves three candidate answers a reader of the
170    /// module might expect, and this separates them. The device holds
171    /// 16 bytes, the call asks for 64, and the in-memory device copies
172    /// nothing before refusing:
173    ///
174    /// - 0, if the counter charged only successful reads;
175    /// - 16, if it charged what the error says was available;
176    /// - 64, the buffer presented — which is what it does.
177    ///
178    /// The read count on a failed read has a test above; the byte count
179    /// had none, and that is how the module header came to describe a
180    /// counter of bytes moved while the code counted bytes requested.
181    #[test]
182    fn bytes_counts_what_was_asked_for_including_a_failed_read() {
183        let dev = device(16);
184        let mut buf = [0u8; 64];
185
186        assert!(dev.read_at(0, &mut buf).is_err(), "past the end");
187        assert_eq!(
188            dev.bytes(),
189            64,
190            "the whole buffer the driver presented, not the 16 bytes \
191             available and not the 0 bytes delivered"
192        );
193        assert_eq!(
194            buf, [0u8; 64],
195            "this device refused without copying, so 64 bytes were \
196             charged for a transfer of none"
197        );
198    }
199
200    /// And the answer does not change when the device *did* move bytes,
201    /// which is the property that makes two drivers comparable.
202    ///
203    /// A [`crate::FileDevice`] copies the readable prefix into the
204    /// buffer before reporting the shortfall, so this same 64-byte
205    /// request against a 16-byte file genuinely transfers 16 bytes
206    /// where the in-memory device above transferred none. Both are
207    /// charged 64. A counter of bytes moved would have to report 16
208    /// here and 0 there for identical driver behaviour, which is the
209    /// "two numbers that look alike and are not" failure this module
210    /// exists to prevent.
211    #[test]
212    fn bytes_is_the_request_even_when_the_device_moved_a_prefix() {
213        use std::sync::atomic::AtomicU64;
214
215        static N: AtomicU64 = AtomicU64::new(0);
216        let path = std::env::temp_dir().join(format!(
217            "fs_core_counting_prefix_{}_{}.bin",
218            std::process::id(),
219            N.fetch_add(1, Ordering::Relaxed)
220        ));
221        std::fs::write(&path, [7u8; 16]).expect("write the fixture");
222
223        let file = crate::FileDevice::open(&path).expect("open the fixture");
224        assert_eq!(file.size_bytes(), 16, "the fixture is the size it claims");
225
226        let dev = CountingDevice::new(Arc::new(file));
227        let mut buf = [0u8; 64];
228        let err = dev.read_at(0, &mut buf).expect_err("past the end");
229
230        // Read everything out before asserting, so a failure does not
231        // leave the fixture behind.
232        let (reads, bytes) = (dev.reads(), dev.bytes());
233        let moved = buf.iter().filter(|b| **b == 7).count();
234        drop(dev);
235        std::fs::remove_file(&path).expect("remove the fixture");
236
237        match err {
238            crate::Error::ShortRead { want, got, .. } => {
239                assert_eq!((want, got), (64, 16), "asked 64, got the 16 there were");
240            }
241            other => panic!("expected ShortRead, got {other:?}"),
242        }
243        assert_eq!(moved, 16, "the file device really did copy its prefix");
244        assert_eq!(
245            (reads, bytes),
246            (1, 64),
247            "charged the request, exactly as the in-memory device was"
248        );
249    }
250
251    /// Resetting drops the mount's own reads, which is the whole reason
252    /// it exists: an operation measured with them included is measured
253    /// against a constant that has nothing to do with it.
254    #[test]
255    fn resetting_starts_the_measurement_where_the_work_does() {
256        let dev = device(4096);
257        let mut buf = [0u8; 64];
258        dev.read_at(0, &mut buf).expect("the mount's own reads");
259        dev.reset();
260        assert_eq!((dev.reads(), dev.bytes()), (0, 0));
261
262        dev.read_at(64, &mut buf).expect("the work being measured");
263        assert_eq!((dev.reads(), dev.bytes()), (1, 64));
264    }
265}