Skip to main content

ewf_image/
reader_statistics.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5#[non_exhaustive]
6/// Cumulative performance counters for one shared EWF image reader.
7///
8/// Collection is opt-in through [`crate::OpenOptions::with_reader_statistics`].
9/// Cloned [`crate::Image`] values and their cursors share the same counters.
10pub struct ReaderStatistics {
11    cursors_created: u64,
12    segment_parses: u64,
13    segment_handle_opens: u64,
14    segment_handle_reopens: u64,
15    table_checksum_bytes: u64,
16    table_checksum_nanos: u64,
17    chunk_cache_hits: u64,
18    chunk_cache_misses: u64,
19    chunk_cache_coalesced: u64,
20    table_page_cache_hits: u64,
21    table_page_cache_misses: u64,
22    segment_read_bytes: u64,
23    segment_read_nanos: u64,
24    segment_pool_wait_nanos: u64,
25    encoded_bytes_read: u64,
26    decoded_bytes: u64,
27    decompression_nanos: u64,
28}
29
30impl ReaderStatistics {
31    /// Returns the number of logical-media and single-file cursors created.
32    pub fn cursors_created(&self) -> u64 {
33        self.cursors_created
34    }
35
36    /// Returns the number of segment metadata parses performed while opening.
37    pub fn segment_parses(&self) -> u64 {
38        self.segment_parses
39    }
40
41    /// Returns the number of segment handles opened for the first time.
42    pub fn segment_handle_opens(&self) -> u64 {
43        self.segment_handle_opens
44    }
45
46    /// Returns the number of previously evicted segment handles reopened.
47    pub fn segment_handle_reopens(&self) -> u64 {
48        self.segment_handle_reopens
49    }
50
51    /// Returns the table-entry bytes processed for checksum validation.
52    pub fn table_checksum_bytes(&self) -> u64 {
53        self.table_checksum_bytes
54    }
55
56    /// Returns nanoseconds spent validating table-entry checksums.
57    pub fn table_checksum_nanos(&self) -> u64 {
58        self.table_checksum_nanos
59    }
60
61    /// Returns decoded chunk-cache hits.
62    pub fn chunk_cache_hits(&self) -> u64 {
63        self.chunk_cache_hits
64    }
65
66    /// Returns decoded chunk-cache misses.
67    pub fn chunk_cache_misses(&self) -> u64 {
68        self.chunk_cache_misses
69    }
70
71    /// Returns misses that waited for an already-running decode of the same chunk.
72    pub fn chunk_cache_coalesced(&self) -> u64 {
73        self.chunk_cache_coalesced
74    }
75
76    /// Returns table-entry page-cache hits.
77    pub fn table_page_cache_hits(&self) -> u64 {
78        self.table_page_cache_hits
79    }
80
81    /// Returns table-entry page-cache misses.
82    pub fn table_page_cache_misses(&self) -> u64 {
83        self.table_page_cache_misses
84    }
85
86    /// Returns bytes read from path-backed segment files after opening.
87    pub fn segment_read_bytes(&self) -> u64 {
88        self.segment_read_bytes
89    }
90
91    /// Returns nanoseconds spent in path-backed positioned segment reads.
92    pub fn segment_read_nanos(&self) -> u64 {
93        self.segment_read_nanos
94    }
95
96    /// Returns nanoseconds spent waiting for a bounded segment handle to become idle.
97    pub fn segment_pool_wait_nanos(&self) -> u64 {
98        self.segment_pool_wait_nanos
99    }
100
101    /// Returns encoded chunk bytes read from segment files.
102    pub fn encoded_bytes_read(&self) -> u64 {
103        self.encoded_bytes_read
104    }
105
106    /// Returns decoded logical chunk bytes produced.
107    pub fn decoded_bytes(&self) -> u64 {
108        self.decoded_bytes
109    }
110
111    /// Returns nanoseconds spent decompressing chunks.
112    pub fn decompression_nanos(&self) -> u64 {
113        self.decompression_nanos
114    }
115
116    /// Returns a field-wise saturating delta from an earlier snapshot.
117    #[must_use]
118    pub fn saturating_delta(self, earlier: Self) -> Self {
119        Self {
120            cursors_created: self.cursors_created.saturating_sub(earlier.cursors_created),
121            segment_parses: self.segment_parses.saturating_sub(earlier.segment_parses),
122            segment_handle_opens: self
123                .segment_handle_opens
124                .saturating_sub(earlier.segment_handle_opens),
125            segment_handle_reopens: self
126                .segment_handle_reopens
127                .saturating_sub(earlier.segment_handle_reopens),
128            table_checksum_bytes: self
129                .table_checksum_bytes
130                .saturating_sub(earlier.table_checksum_bytes),
131            table_checksum_nanos: self
132                .table_checksum_nanos
133                .saturating_sub(earlier.table_checksum_nanos),
134            chunk_cache_hits: self
135                .chunk_cache_hits
136                .saturating_sub(earlier.chunk_cache_hits),
137            chunk_cache_misses: self
138                .chunk_cache_misses
139                .saturating_sub(earlier.chunk_cache_misses),
140            chunk_cache_coalesced: self
141                .chunk_cache_coalesced
142                .saturating_sub(earlier.chunk_cache_coalesced),
143            table_page_cache_hits: self
144                .table_page_cache_hits
145                .saturating_sub(earlier.table_page_cache_hits),
146            table_page_cache_misses: self
147                .table_page_cache_misses
148                .saturating_sub(earlier.table_page_cache_misses),
149            segment_read_bytes: self
150                .segment_read_bytes
151                .saturating_sub(earlier.segment_read_bytes),
152            segment_read_nanos: self
153                .segment_read_nanos
154                .saturating_sub(earlier.segment_read_nanos),
155            segment_pool_wait_nanos: self
156                .segment_pool_wait_nanos
157                .saturating_sub(earlier.segment_pool_wait_nanos),
158            encoded_bytes_read: self
159                .encoded_bytes_read
160                .saturating_sub(earlier.encoded_bytes_read),
161            decoded_bytes: self.decoded_bytes.saturating_sub(earlier.decoded_bytes),
162            decompression_nanos: self
163                .decompression_nanos
164                .saturating_sub(earlier.decompression_nanos),
165        }
166    }
167}
168
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
170#[non_exhaustive]
171/// Configured and observed payload bytes for one shared EWF reader cache set.
172pub struct ReaderCacheInfo {
173    chunk_cache_capacity: u64,
174    table_entry_cache_capacity: u64,
175    table_entry_cache_current: u64,
176    table_entry_cache_peak: u64,
177}
178
179impl ReaderCacheInfo {
180    pub(crate) fn new(
181        chunk_cache_capacity_bytes: u64,
182        table_entry_cache_capacity_bytes: usize,
183        table_entry_cache_current_bytes: usize,
184        table_entry_cache_peak_bytes: usize,
185    ) -> Self {
186        Self {
187            chunk_cache_capacity: chunk_cache_capacity_bytes,
188            table_entry_cache_capacity: usize_to_u64(table_entry_cache_capacity_bytes),
189            table_entry_cache_current: usize_to_u64(table_entry_cache_current_bytes),
190            table_entry_cache_peak: usize_to_u64(table_entry_cache_peak_bytes),
191        }
192    }
193
194    /// Returns the decoded chunk-cache byte capacity.
195    pub fn chunk_cache_capacity_bytes(&self) -> u64 {
196        self.chunk_cache_capacity
197    }
198
199    /// Returns the configured table-entry cache byte capacity.
200    pub fn table_entry_cache_capacity_bytes(&self) -> u64 {
201        self.table_entry_cache_capacity
202    }
203
204    /// Returns the currently retained table-entry page payload bytes.
205    pub fn table_entry_cache_current_bytes(&self) -> u64 {
206        self.table_entry_cache_current
207    }
208
209    /// Returns the peak retained table-entry page payload bytes.
210    pub fn table_entry_cache_peak_bytes(&self) -> u64 {
211        self.table_entry_cache_peak
212    }
213}
214
215#[derive(Debug, Default)]
216pub(crate) struct ReaderStatisticsCollector {
217    enabled: bool,
218    cursors_created: AtomicU64,
219    segment_parses: AtomicU64,
220    segment_handle_opens: AtomicU64,
221    segment_handle_reopens: AtomicU64,
222    table_checksum_bytes: AtomicU64,
223    table_checksum_nanos: AtomicU64,
224    chunk_cache_hits: AtomicU64,
225    chunk_cache_misses: AtomicU64,
226    chunk_cache_coalesced: AtomicU64,
227    table_page_cache_hits: AtomicU64,
228    table_page_cache_misses: AtomicU64,
229    segment_read_bytes: AtomicU64,
230    segment_read_nanos: AtomicU64,
231    segment_pool_wait_nanos: AtomicU64,
232    encoded_bytes_read: AtomicU64,
233    decoded_bytes: AtomicU64,
234    decompression_nanos: AtomicU64,
235}
236
237impl ReaderStatisticsCollector {
238    pub(crate) fn new(enabled: bool) -> Self {
239        Self {
240            enabled,
241            ..Self::default()
242        }
243    }
244
245    pub(crate) fn enabled(&self) -> bool {
246        self.enabled
247    }
248
249    pub(crate) fn snapshot(&self) -> Option<ReaderStatistics> {
250        self.enabled.then(|| ReaderStatistics {
251            cursors_created: self.cursors_created.load(Ordering::Relaxed),
252            segment_parses: self.segment_parses.load(Ordering::Relaxed),
253            segment_handle_opens: self.segment_handle_opens.load(Ordering::Relaxed),
254            segment_handle_reopens: self.segment_handle_reopens.load(Ordering::Relaxed),
255            table_checksum_bytes: self.table_checksum_bytes.load(Ordering::Relaxed),
256            table_checksum_nanos: self.table_checksum_nanos.load(Ordering::Relaxed),
257            chunk_cache_hits: self.chunk_cache_hits.load(Ordering::Relaxed),
258            chunk_cache_misses: self.chunk_cache_misses.load(Ordering::Relaxed),
259            chunk_cache_coalesced: self.chunk_cache_coalesced.load(Ordering::Relaxed),
260            table_page_cache_hits: self.table_page_cache_hits.load(Ordering::Relaxed),
261            table_page_cache_misses: self.table_page_cache_misses.load(Ordering::Relaxed),
262            segment_read_bytes: self.segment_read_bytes.load(Ordering::Relaxed),
263            segment_read_nanos: self.segment_read_nanos.load(Ordering::Relaxed),
264            segment_pool_wait_nanos: self.segment_pool_wait_nanos.load(Ordering::Relaxed),
265            encoded_bytes_read: self.encoded_bytes_read.load(Ordering::Relaxed),
266            decoded_bytes: self.decoded_bytes.load(Ordering::Relaxed),
267            decompression_nanos: self.decompression_nanos.load(Ordering::Relaxed),
268        })
269    }
270
271    pub(crate) fn record_cursor_created(&self) {
272        self.add(&self.cursors_created, 1);
273    }
274
275    pub(crate) fn record_segment_parse(&self) {
276        self.add(&self.segment_parses, 1);
277    }
278
279    pub(crate) fn record_segment_handle_open(&self, count: usize) {
280        self.add(&self.segment_handle_opens, usize_to_u64(count));
281    }
282
283    pub(crate) fn record_segment_handle_reopen(&self) {
284        self.add(&self.segment_handle_reopens, 1);
285    }
286
287    pub(crate) fn record_table_checksum(&self, bytes: u64, elapsed: Duration) {
288        self.add(&self.table_checksum_bytes, bytes);
289        self.add(&self.table_checksum_nanos, duration_nanos(elapsed));
290    }
291
292    pub(crate) fn record_chunk_cache_access(&self, hit: bool) {
293        if hit {
294            self.add(&self.chunk_cache_hits, 1);
295        } else {
296            self.add(&self.chunk_cache_misses, 1);
297        }
298    }
299
300    pub(crate) fn record_chunk_cache_coalesced(&self) {
301        self.add(&self.chunk_cache_coalesced, 1);
302    }
303
304    pub(crate) fn record_segment_read(&self, bytes: usize, elapsed: Duration) {
305        self.add(&self.segment_read_bytes, usize_to_u64(bytes));
306        self.add(&self.segment_read_nanos, duration_nanos(elapsed));
307    }
308
309    pub(crate) fn record_segment_pool_wait(&self, elapsed: Duration) {
310        self.add(&self.segment_pool_wait_nanos, duration_nanos(elapsed));
311    }
312
313    pub(crate) fn record_table_page_cache_access(&self, hit: bool) {
314        if hit {
315            self.add(&self.table_page_cache_hits, 1);
316        } else {
317            self.add(&self.table_page_cache_misses, 1);
318        }
319    }
320
321    pub(crate) fn record_encoded_bytes_read(&self, bytes: u64) {
322        self.add(&self.encoded_bytes_read, bytes);
323    }
324
325    pub(crate) fn record_decoded_bytes(&self, bytes: usize) {
326        self.add(&self.decoded_bytes, usize_to_u64(bytes));
327    }
328
329    pub(crate) fn record_decompression(&self, elapsed: Duration) {
330        self.add(&self.decompression_nanos, duration_nanos(elapsed));
331    }
332
333    fn add(&self, counter: &AtomicU64, value: u64) {
334        if !self.enabled || value == 0 {
335            return;
336        }
337        let _ = counter.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
338            Some(current.saturating_add(value))
339        });
340    }
341}
342
343fn duration_nanos(duration: Duration) -> u64 {
344    u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
345}
346
347fn usize_to_u64(value: usize) -> u64 {
348    u64::try_from(value).unwrap_or(u64::MAX)
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn counter_addition_saturates() {
357        let statistics = ReaderStatisticsCollector::new(true);
358        let counter = AtomicU64::new(u64::MAX - 1);
359        statistics.add(&counter, 2);
360        statistics.add(&counter, 1);
361        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
362    }
363
364    #[test]
365    fn disabled_and_zero_additions_preserve_counter() {
366        let counter = AtomicU64::new(42);
367        ReaderStatisticsCollector::new(false).add(&counter, 5);
368        ReaderStatisticsCollector::new(true).add(&counter, 0);
369        assert_eq!(counter.load(Ordering::Relaxed), 42);
370    }
371
372    #[test]
373    fn concurrent_additions_are_not_lost() {
374        let statistics = ReaderStatisticsCollector::new(true);
375        let counter = AtomicU64::new(0);
376        std::thread::scope(|scope| {
377            for _ in 0..8 {
378                scope.spawn(|| {
379                    for _ in 0..1_000 {
380                        statistics.add(&counter, 1);
381                    }
382                });
383            }
384        });
385        assert_eq!(counter.load(Ordering::Relaxed), 8_000);
386    }
387}