Skip to main content

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}