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    table_page_cache_hits: u64,
20    table_page_cache_misses: u64,
21    encoded_bytes_read: u64,
22    decoded_bytes: u64,
23    decompression_nanos: u64,
24}
25
26impl ReaderStatistics {
27    /// Returns the number of logical-media and single-file cursors created.
28    pub fn cursors_created(&self) -> u64 {
29        self.cursors_created
30    }
31
32    /// Returns the number of segment metadata parses performed while opening.
33    pub fn segment_parses(&self) -> u64 {
34        self.segment_parses
35    }
36
37    /// Returns the number of segment handles opened for the first time.
38    pub fn segment_handle_opens(&self) -> u64 {
39        self.segment_handle_opens
40    }
41
42    /// Returns the number of previously evicted segment handles reopened.
43    pub fn segment_handle_reopens(&self) -> u64 {
44        self.segment_handle_reopens
45    }
46
47    /// Returns the table-entry bytes processed for checksum validation.
48    pub fn table_checksum_bytes(&self) -> u64 {
49        self.table_checksum_bytes
50    }
51
52    /// Returns nanoseconds spent validating table-entry checksums.
53    pub fn table_checksum_nanos(&self) -> u64 {
54        self.table_checksum_nanos
55    }
56
57    /// Returns decoded chunk-cache hits.
58    pub fn chunk_cache_hits(&self) -> u64 {
59        self.chunk_cache_hits
60    }
61
62    /// Returns decoded chunk-cache misses.
63    pub fn chunk_cache_misses(&self) -> u64 {
64        self.chunk_cache_misses
65    }
66
67    /// Returns table-entry page-cache hits.
68    pub fn table_page_cache_hits(&self) -> u64 {
69        self.table_page_cache_hits
70    }
71
72    /// Returns table-entry page-cache misses.
73    pub fn table_page_cache_misses(&self) -> u64 {
74        self.table_page_cache_misses
75    }
76
77    /// Returns encoded chunk bytes read from segment files.
78    pub fn encoded_bytes_read(&self) -> u64 {
79        self.encoded_bytes_read
80    }
81
82    /// Returns decoded logical chunk bytes produced.
83    pub fn decoded_bytes(&self) -> u64 {
84        self.decoded_bytes
85    }
86
87    /// Returns nanoseconds spent decompressing chunks.
88    pub fn decompression_nanos(&self) -> u64 {
89        self.decompression_nanos
90    }
91
92    /// Returns a field-wise saturating delta from an earlier snapshot.
93    #[must_use]
94    pub fn saturating_delta(self, earlier: Self) -> Self {
95        Self {
96            cursors_created: self.cursors_created.saturating_sub(earlier.cursors_created),
97            segment_parses: self.segment_parses.saturating_sub(earlier.segment_parses),
98            segment_handle_opens: self
99                .segment_handle_opens
100                .saturating_sub(earlier.segment_handle_opens),
101            segment_handle_reopens: self
102                .segment_handle_reopens
103                .saturating_sub(earlier.segment_handle_reopens),
104            table_checksum_bytes: self
105                .table_checksum_bytes
106                .saturating_sub(earlier.table_checksum_bytes),
107            table_checksum_nanos: self
108                .table_checksum_nanos
109                .saturating_sub(earlier.table_checksum_nanos),
110            chunk_cache_hits: self
111                .chunk_cache_hits
112                .saturating_sub(earlier.chunk_cache_hits),
113            chunk_cache_misses: self
114                .chunk_cache_misses
115                .saturating_sub(earlier.chunk_cache_misses),
116            table_page_cache_hits: self
117                .table_page_cache_hits
118                .saturating_sub(earlier.table_page_cache_hits),
119            table_page_cache_misses: self
120                .table_page_cache_misses
121                .saturating_sub(earlier.table_page_cache_misses),
122            encoded_bytes_read: self
123                .encoded_bytes_read
124                .saturating_sub(earlier.encoded_bytes_read),
125            decoded_bytes: self.decoded_bytes.saturating_sub(earlier.decoded_bytes),
126            decompression_nanos: self
127                .decompression_nanos
128                .saturating_sub(earlier.decompression_nanos),
129        }
130    }
131}
132
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
134#[non_exhaustive]
135/// Configured and observed payload bytes for one shared EWF reader cache set.
136pub struct ReaderCacheInfo {
137    chunk_cache_capacity: u64,
138    table_entry_cache_capacity: u64,
139    table_entry_cache_current: u64,
140    table_entry_cache_peak: u64,
141}
142
143impl ReaderCacheInfo {
144    pub(crate) fn new(
145        chunk_cache_capacity_bytes: u64,
146        table_entry_cache_capacity_bytes: usize,
147        table_entry_cache_current_bytes: usize,
148        table_entry_cache_peak_bytes: usize,
149    ) -> Self {
150        Self {
151            chunk_cache_capacity: chunk_cache_capacity_bytes,
152            table_entry_cache_capacity: usize_to_u64(table_entry_cache_capacity_bytes),
153            table_entry_cache_current: usize_to_u64(table_entry_cache_current_bytes),
154            table_entry_cache_peak: usize_to_u64(table_entry_cache_peak_bytes),
155        }
156    }
157
158    /// Returns the decoded chunk-cache byte capacity.
159    pub fn chunk_cache_capacity_bytes(&self) -> u64 {
160        self.chunk_cache_capacity
161    }
162
163    /// Returns the configured table-entry cache byte capacity.
164    pub fn table_entry_cache_capacity_bytes(&self) -> u64 {
165        self.table_entry_cache_capacity
166    }
167
168    /// Returns the currently retained table-entry page payload bytes.
169    pub fn table_entry_cache_current_bytes(&self) -> u64 {
170        self.table_entry_cache_current
171    }
172
173    /// Returns the peak retained table-entry page payload bytes.
174    pub fn table_entry_cache_peak_bytes(&self) -> u64 {
175        self.table_entry_cache_peak
176    }
177}
178
179#[derive(Debug, Default)]
180pub(crate) struct ReaderStatisticsCollector {
181    enabled: bool,
182    cursors_created: AtomicU64,
183    segment_parses: AtomicU64,
184    segment_handle_opens: AtomicU64,
185    segment_handle_reopens: AtomicU64,
186    table_checksum_bytes: AtomicU64,
187    table_checksum_nanos: AtomicU64,
188    chunk_cache_hits: AtomicU64,
189    chunk_cache_misses: AtomicU64,
190    table_page_cache_hits: AtomicU64,
191    table_page_cache_misses: AtomicU64,
192    encoded_bytes_read: AtomicU64,
193    decoded_bytes: AtomicU64,
194    decompression_nanos: AtomicU64,
195}
196
197impl ReaderStatisticsCollector {
198    pub(crate) fn new(enabled: bool) -> Self {
199        Self {
200            enabled,
201            ..Self::default()
202        }
203    }
204
205    pub(crate) fn enabled(&self) -> bool {
206        self.enabled
207    }
208
209    pub(crate) fn snapshot(&self) -> Option<ReaderStatistics> {
210        self.enabled.then(|| ReaderStatistics {
211            cursors_created: self.cursors_created.load(Ordering::Relaxed),
212            segment_parses: self.segment_parses.load(Ordering::Relaxed),
213            segment_handle_opens: self.segment_handle_opens.load(Ordering::Relaxed),
214            segment_handle_reopens: self.segment_handle_reopens.load(Ordering::Relaxed),
215            table_checksum_bytes: self.table_checksum_bytes.load(Ordering::Relaxed),
216            table_checksum_nanos: self.table_checksum_nanos.load(Ordering::Relaxed),
217            chunk_cache_hits: self.chunk_cache_hits.load(Ordering::Relaxed),
218            chunk_cache_misses: self.chunk_cache_misses.load(Ordering::Relaxed),
219            table_page_cache_hits: self.table_page_cache_hits.load(Ordering::Relaxed),
220            table_page_cache_misses: self.table_page_cache_misses.load(Ordering::Relaxed),
221            encoded_bytes_read: self.encoded_bytes_read.load(Ordering::Relaxed),
222            decoded_bytes: self.decoded_bytes.load(Ordering::Relaxed),
223            decompression_nanos: self.decompression_nanos.load(Ordering::Relaxed),
224        })
225    }
226
227    pub(crate) fn record_cursor_created(&self) {
228        self.add(&self.cursors_created, 1);
229    }
230
231    pub(crate) fn record_segment_parse(&self) {
232        self.add(&self.segment_parses, 1);
233    }
234
235    pub(crate) fn record_segment_handle_open(&self, count: usize) {
236        self.add(&self.segment_handle_opens, usize_to_u64(count));
237    }
238
239    pub(crate) fn record_segment_handle_reopen(&self) {
240        self.add(&self.segment_handle_reopens, 1);
241    }
242
243    pub(crate) fn record_table_checksum(&self, bytes: u64, elapsed: Duration) {
244        self.add(&self.table_checksum_bytes, bytes);
245        self.add(&self.table_checksum_nanos, duration_nanos(elapsed));
246    }
247
248    pub(crate) fn record_chunk_cache_access(&self, hit: bool) {
249        if hit {
250            self.add(&self.chunk_cache_hits, 1);
251        } else {
252            self.add(&self.chunk_cache_misses, 1);
253        }
254    }
255
256    pub(crate) fn record_table_page_cache_access(&self, hit: bool) {
257        if hit {
258            self.add(&self.table_page_cache_hits, 1);
259        } else {
260            self.add(&self.table_page_cache_misses, 1);
261        }
262    }
263
264    pub(crate) fn record_encoded_bytes_read(&self, bytes: u64) {
265        self.add(&self.encoded_bytes_read, bytes);
266    }
267
268    pub(crate) fn record_decoded_bytes(&self, bytes: usize) {
269        self.add(&self.decoded_bytes, usize_to_u64(bytes));
270    }
271
272    pub(crate) fn record_decompression(&self, elapsed: Duration) {
273        self.add(&self.decompression_nanos, duration_nanos(elapsed));
274    }
275
276    fn add(&self, counter: &AtomicU64, value: u64) {
277        if !self.enabled || value == 0 {
278            return;
279        }
280        let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
281            Some(current.saturating_add(value))
282        });
283    }
284}
285
286fn duration_nanos(duration: Duration) -> u64 {
287    u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
288}
289
290fn usize_to_u64(value: usize) -> u64 {
291    u64::try_from(value).unwrap_or(u64::MAX)
292}