Skip to main content

mnemosyne/stats/
reporting.rs

1//! Human- and machine-readable summaries over the bin statistics.
2
3/// Returns a human-readable one-line summary of the current policy and telemetry.
4///
5/// Format: `policy=<name> mitigations=0x<flags> allocs=<n> live_bytes=<b> int_frag=<x>%`
6#[must_use]
7pub fn policy_summary() -> alloc::string::String {
8    use alloc::format;
9    use mnemosyne_core::policy::AllocPolicy;
10    let stats = mnemosyne_local::summary_line();
11    format!(
12        "policy={} mitigations=0x{:08X} fingerprint=0x{:016X} {stats}",
13        mnemosyne_core::policy::StandardPolicy::POLICY_NAME,
14        mnemosyne_core::policy::StandardPolicy::MITIGATION_FLAGS,
15        mnemosyne_core::policy::StandardPolicy::POLICY_FINGERPRINT,
16    )
17}
18
19/// Returns the `n` hottest size classes by alloc_count, sorted descending.
20///
21/// Flushes TLS stats before sampling. Returns at most `n` entries; fewer
22/// if fewer than `n` classes have been allocated from.
23#[must_use]
24pub fn top_n_classes(n: usize) -> alloc::vec::Vec<mnemosyne_local::BinSnapshot> {
25    let mut snapshots: alloc::vec::Vec<_> = mnemosyne_local::all_bin_snapshots()
26        .into_iter()
27        .filter(|s| s.alloc_count > 0)
28        .collect();
29    snapshots.sort_unstable_by_key(|s| core::cmp::Reverse(s.alloc_count));
30    snapshots.truncate(n);
31    snapshots
32}
33
34// ── Stats window ──────────────────────────────────────────────────────────────
35
36/// A snapshot of bin stats taken at a fixed point in time.
37///
38/// Create a baseline with [`BinStatsWindow::capture`], then call
39/// [`BinStatsWindow::delta`] later to compute per-class deltas over the window.
40/// This is the recommended pattern for profiling a code region:
41///
42/// ```rust
43/// # use mnemosyne::BinStatsWindow;
44/// let baseline = BinStatsWindow::capture();
45/// // ... code under profiling ...
46/// let delta = baseline.delta();
47/// ```
48pub struct BinStatsWindow {
49    bins: [mnemosyne_local::BinSnapshot; mnemosyne_core::NUM_SIZE_CLASSES],
50}
51
52impl BinStatsWindow {
53    /// Captures the current per-class bin stats as a baseline.
54    ///
55    /// Flushes the calling thread's TLS batch first so the snapshot
56    /// reflects all preceding allocations on this thread.
57    #[must_use]
58    pub fn capture() -> Self {
59        Self {
60            bins: mnemosyne_local::all_bin_snapshots(),
61        }
62    }
63
64    /// Computes per-class deltas since the baseline was captured.
65    ///
66    /// Each returned snapshot has its counters set to the difference since
67    /// the baseline. Counters that decreased (or were reset) saturate to zero.
68    #[must_use]
69    pub fn delta(&self) -> [mnemosyne_local::BinSnapshot; mnemosyne_core::NUM_SIZE_CLASSES] {
70        let now = mnemosyne_local::all_bin_snapshots();
71        core::array::from_fn(|class| {
72            let b = &self.bins[class];
73            let n = &now[class];
74            n.saturating_delta(b)
75        })
76    }
77
78    /// Computes the sum of a single field across all size-class delta snapshots.
79    ///
80    /// SSOT for the `self.delta().iter().map(f).fold(0, saturating_add)` pattern
81    /// shared by `total_alloc_count_delta`, `total_live_bytes_delta`, and
82    /// `total_requested_bytes_delta`.
83    #[inline(always)]
84    fn sum_delta_field(&self, f: impl Fn(&mnemosyne_local::BinSnapshot) -> u64) -> u64 {
85        self.delta().iter().map(f).fold(0u64, u64::saturating_add)
86    }
87
88    /// Total allocations during the window across all size classes.
89    #[must_use]
90    pub fn total_alloc_count_delta(&self) -> u64 {
91        self.sum_delta_field(|s| s.alloc_count)
92    }
93
94    /// Total live bytes at the end of the window minus the start.
95    #[must_use]
96    pub fn total_live_bytes_delta(&self) -> u64 {
97        self.sum_delta_field(|s| s.live_bytes())
98    }
99
100    /// Total user-requested bytes during the window across all size classes.
101    ///
102    /// Requires `record_alloc_with_size` to have been used at call sites.
103    #[must_use]
104    pub fn total_requested_bytes_delta(&self) -> u64 {
105        self.sum_delta_field(|s| s.requested_bytes)
106    }
107
108    /// Internal fragmentation ratio over the window:
109    /// `(total_alloc_bytes_delta - total_requested_bytes_delta) / total_alloc_bytes_delta`.
110    ///
111    /// Returns `0.0` when `total_alloc_bytes_delta == 0` or `requested` is zero.
112    #[must_use]
113    pub fn window_internal_fragmentation(&self) -> f64 {
114        let d = self.delta();
115        let alloc: u64 = d.iter().map(|s| s.alloc_bytes).fold(0, u64::saturating_add);
116        let req: u64 = d
117            .iter()
118            .map(|s| s.requested_bytes)
119            .fold(0, u64::saturating_add);
120        if alloc == 0 || req == 0 {
121            return 0.0;
122        }
123        let waste = alloc.saturating_sub(req);
124        (waste as f64 / alloc as f64).min(1.0)
125    }
126}