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}