mnemosyne_local/bin_stats/snapshot.rs
1/// Per-size-class allocation statistics snapshot.
2///
3/// Non-exhaustive: this is telemetry the allocator *produces*, and its field
4/// set grows as new counters are added — `requested_bytes` was the most recent.
5/// Marking it so keeps each addition a non-breaking change instead of a major
6/// one. Construct via [`Default`] and read the fields.
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
8#[non_exhaustive]
9pub struct BinSnapshot {
10 /// Total allocations served from this size class.
11 pub alloc_count: u64,
12 /// Total frees returned to this size class.
13 pub dealloc_count: u64,
14 /// Cumulative bytes allocated (size-class block size × alloc_count).
15 ///
16 /// The product saturates at `u64::MAX` rather than wrapping.
17 pub alloc_bytes: u64,
18 /// Cumulative user-requested bytes for this class.
19 ///
20 /// Populated by `record_alloc_with_size`; zero when the call sites only
21 /// use `record_alloc`. Internal fragmentation =
22 /// `(alloc_bytes - requested_bytes) / alloc_bytes`.
23 pub requested_bytes: u64,
24 /// Block size of this size class in bytes.
25 pub block_size: usize,
26 /// Live allocation estimate: `alloc_count − dealloc_count`.
27 ///
28 /// Under-estimates because the two counters are not snapshotted
29 /// atomically, but never negative from the caller's perspective:
30 /// subtraction uses saturating arithmetic.
31 pub live_estimate: u64,
32}
33
34impl BinSnapshot {
35 /// Counters advanced since `baseline`, saturating at zero where one
36 /// decreased or was reset.
37 ///
38 /// Lives here rather than at the call site because [`BinSnapshot`] is
39 /// `#[non_exhaustive]`: only this crate may build one by literal, so a new
40 /// counter field extends this method instead of breaking every consumer
41 /// that computes a delta.
42 #[must_use]
43 pub fn saturating_delta(&self, baseline: &Self) -> Self {
44 Self {
45 alloc_count: self.alloc_count.saturating_sub(baseline.alloc_count),
46 dealloc_count: self.dealloc_count.saturating_sub(baseline.dealloc_count),
47 alloc_bytes: self.alloc_bytes.saturating_sub(baseline.alloc_bytes),
48 requested_bytes: self
49 .requested_bytes
50 .saturating_sub(baseline.requested_bytes),
51 block_size: self.block_size,
52 live_estimate: self.live_estimate.saturating_sub(baseline.live_estimate),
53 }
54 }
55
56 /// Fragmentation ratio: `live_bytes / alloc_bytes`, in `[0.0, 1.0]`.
57 ///
58 /// Returns `0.0` when nothing has ever been allocated in this class.
59 #[inline]
60 #[must_use]
61 pub fn fragmentation_ratio(&self) -> f64 {
62 if self.alloc_bytes == 0 {
63 return 0.0;
64 }
65 let live_bytes = self.live_estimate.saturating_mul(self.block_size as u64);
66 (live_bytes as f64 / self.alloc_bytes as f64).min(1.0)
67 }
68
69 /// Internal fragmentation: `(alloc_bytes - requested_bytes) / alloc_bytes`.
70 ///
71 /// Returns `0.0` when `requested_bytes` is zero (not tracked) or
72 /// `alloc_bytes` is zero.
73 #[inline]
74 #[must_use]
75 pub fn internal_fragmentation_ratio(&self) -> f64 {
76 if self.alloc_bytes == 0 || self.requested_bytes == 0 {
77 return 0.0;
78 }
79 let waste = self.alloc_bytes.saturating_sub(self.requested_bytes);
80 (waste as f64 / self.alloc_bytes as f64).min(1.0)
81 }
82
83 /// Live bytes in this class: `live_estimate × block_size`.
84 #[inline]
85 #[must_use]
86 pub fn live_bytes(&self) -> u64 {
87 self.live_estimate.saturating_mul(self.block_size as u64)
88 }
89}