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}