1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Diagnostic snapshots emitted by the budget tick.
use crate::jemalloc::JemallocStats;
use crate::policy::Verdict;
/// Diagnostic snapshot of one budget tick.
#[derive(Clone, Debug)]
pub struct BudgetSnapshot {
/// Configured target RSS in bytes.
pub target_bytes: u64,
/// Target after active-lease subtraction.
pub effective_target_bytes: u64,
/// Effective usage used by the policy: maximum of OS process usage,
/// jemalloc residency and live heap allocation. These overlap and must
/// not be added, nor may a smaller allocator reading hide other memory.
pub rss_bytes: u64,
/// Raw sysinfo RSS reading (`phys_footprint` on macOS, RSS on
/// Linux). Kept for the gap diagnostic — compare against
/// [`BudgetSnapshot::rss_bytes`] to see how far the OS-reported
/// footprint diverges from jemalloc's view. Equals `rss_bytes`
/// when no jemalloc stats are available.
pub rss_sysinfo_bytes: u64,
/// Sum of bytes reserved by live [`LeaseGuard`](crate::LeaseGuard)s.
pub lease_bytes_active: u64,
/// jemalloc introspection at tick time. Splits the non-cache
/// bucket into "live allocations the program holds"
/// ([`JemallocStats::allocated_bytes`]) and "pages jemalloc has
/// not yet returned to the kernel"
/// ([`JemallocStats::retained_bytes`]). Zero on builds that do
/// not install jemalloc.
pub jemalloc: JemallocStats,
/// What the policy decided this tick and why.
pub verdict: Verdict,
/// Number of consecutive ticks (including this one) that have
/// ended in the same [`Verdict`] label. Resets to `1` when the
/// label changes. Useful for spotting wedges in post-mortem
/// logs ("the budget has been at `skip-shrink-lean-caches` for
/// 480 ticks") without arithmetic on timestamps.
pub verdict_streak: u32,
/// When the verdict label changed at this tick, the label of
/// the previous run; `None` otherwise. Lets post-mortem capture
/// the length of the run that just ended via
/// [`BudgetSnapshot::previous_verdict_streak`].
pub previous_verdict_label: Option<&'static str>,
/// Length of the run that just ended (`Some` when
/// [`BudgetSnapshot::previous_verdict_label`] is `Some`).
pub previous_verdict_streak: Option<u32>,
/// One entry per still-live registered cache.
pub caches: Vec<CacheSnapshot>,
/// One entry per still-live registered [`NonCacheReporter`]
/// (e.g. the reasoner working-set gauge). Names slices of the
/// otherwise-opaque `non_cache_live` bucket.
///
/// [`NonCacheReporter`]: crate::NonCacheReporter
pub reporters: Vec<ReporterSnapshot>,
}
impl BudgetSnapshot {
/// Sum of `new_max_bytes` across every cache after the tick.
/// Useful when logging "what did the budget reapportion to".
#[must_use]
pub fn cap_bytes_after(&self) -> u64 {
self.caches.iter().map(|c| c.new_max_bytes).sum()
}
/// Sum of `current_bytes` across every cache at tick time —
/// i.e. live cache footprint, before the new cap is applied.
#[must_use]
pub fn cache_bytes_current(&self) -> u64 {
self.caches.iter().map(|c| c.current_bytes).sum()
}
/// `rss_bytes − cache_bytes_current − lease_bytes_active`,
/// saturating at zero. This is the **non-cache bucket**: heap
/// that the budget knows about but cannot resize — typically
/// transient allocator state (parser context, query result buffers,
/// or other working memory) plus the
/// non-reclaimable parts of jemalloc's retained-decommitted
/// pages.
///
/// Surfaced on every tick emission as `non_cache_bytes` so
/// post-mortem analysis can attribute RSS growth to a specific
/// bucket (cache pressure vs. non-cache spike) instead of
/// inferring it from the cache breakdown. See
/// [`BudgetSnapshot::non_cache_live_bytes`] for the further
/// jemalloc-attributed split.
#[must_use]
pub fn non_cache_bytes(&self) -> u64 {
self.rss_bytes
.saturating_sub(self.cache_bytes_current())
.saturating_sub(self.lease_bytes_active)
}
/// Non-cache bytes attributed to **live application
/// allocations** by jemalloc: `je_allocated_bytes −
/// cache_bytes_current − lease_bytes_active`, saturating at
/// zero.
///
/// Reading this alongside `non_cache_bytes` is the diagnostic
/// that separates real leaks from jemalloc retention. If
/// `non_cache_live_bytes` is flat across a long-running soak but
/// `non_cache_bytes` keeps growing, the growth is jemalloc
/// retained pages (`jemalloc.retained_bytes`) — an allocator
/// tuning issue. If `non_cache_live_bytes` itself grows, the
/// program is holding more memory (a leak somewhere outside
/// the registered caches).
///
/// Zero on builds that do not install jemalloc.
#[must_use]
pub fn non_cache_live_bytes(&self) -> u64 {
self.jemalloc
.allocated_bytes
.saturating_sub(self.cache_bytes_current())
.saturating_sub(self.lease_bytes_active)
}
/// Sum of `current_bytes` across all live non-cache reporters —
/// the **attributed** portion of `non_cache_live_bytes`.
#[must_use]
pub fn reporter_bytes(&self) -> u64 {
self.reporters
.iter()
.map(|r| r.current_bytes)
.fold(0u64, u64::saturating_add)
}
/// `non_cache_live_bytes − reporter_bytes`, saturating at zero —
/// the **unattributed** remainder of the live non-cache bucket
/// (parser context, query buffers, transient allocator state, and
/// any subsystem that hasn't registered a reporter yet).
#[must_use]
pub fn non_cache_other_bytes(&self) -> u64 {
self.non_cache_live_bytes()
.saturating_sub(self.reporter_bytes())
}
}
/// Per-reporter slice of a [`BudgetSnapshot`].
#[derive(Clone, Debug)]
pub struct ReporterSnapshot {
/// Reporter name (`NonCacheReporter::name`).
pub name: String,
/// Bytes the reporter said it was holding when the tick fired.
pub current_bytes: u64,
}
/// Per-cache slice of a [`BudgetSnapshot`].
#[derive(Clone, Debug)]
pub struct CacheSnapshot {
/// Cache name (`Resizable::name`).
pub name: String,
/// Bytes currently held by the cache when the tick fired.
pub current_bytes: u64,
/// Cap before this tick reapportioned.
pub previous_max_bytes: u64,
/// Cap installed by this tick.
pub new_max_bytes: u64,
/// Most recent hit rate; `None` if no lookups yet.
pub hit_rate: Option<f64>,
}
impl CacheSnapshot {
/// Signed cap change this tick (`new_max_bytes − previous_max_bytes`).
/// Negative on shrink, positive on grow, zero on hold or skip.
///
/// Logged as `delta_bytes` on each per-cache emission so
/// post-mortem analysis can grep for nonzero deltas directly
/// without doing arithmetic.
///
/// Returns `i64::MAX` / `i64::MIN` on extreme cases where the
/// difference would not fit in `i64` — practically unreachable
/// (caps are bounded by `MEMORY_BUDGET_TARGET_MIB`, not `u64::MAX`)
/// but the saturation keeps the math total.
#[must_use]
pub fn delta_bytes(&self) -> i64 {
let prev = i64::try_from(self.previous_max_bytes).unwrap_or(i64::MAX);
let new = i64::try_from(self.new_max_bytes).unwrap_or(i64::MAX);
new.saturating_sub(prev)
}
}