memory_budget/budget/memory/
tick.rs1use std::sync::Arc;
4use std::sync::atomic::Ordering;
5
6use super::super::snapshot::{BudgetSnapshot, CacheSnapshot, ReporterSnapshot};
7use super::super::state::LiveViews;
8use super::handle::MemoryBudget;
9use crate::policy::{CacheView, PolicyInput};
10use crate::resizable::Resizable;
11
12impl MemoryBudget {
13 pub fn tick_now(&self) -> BudgetSnapshot {
17 let rss_sysinfo_bytes = self.state.rss.read_rss_bytes();
18 let snapshot = self.run_tick(rss_sysinfo_bytes);
19 self.emit(&snapshot);
20 #[cfg(feature = "heap-profiling")]
25 if let Some(profiler) = self.state.heap_profiler.as_ref() {
26 profiler.maybe_dump(snapshot.rss_bytes);
27 }
28 if let Some(ceiling) = self.state.config.hard_ceiling_bytes()
31 && snapshot.rss_bytes > ceiling
32 {
33 tracing::error!(
34 target: "memory_budget::budget",
35 rss_bytes = snapshot.rss_bytes,
36 rss_sysinfo_bytes = snapshot.rss_sysinfo_bytes,
37 ceiling_bytes = ceiling,
38 "hard ceiling exceeded — aborting process"
39 );
40 std::process::abort();
41 }
42 snapshot
43 }
44
45 pub fn spawn(&self) -> tokio::task::JoinHandle<()> {
49 let state = Arc::clone(&self.state);
50 let interval = state.config.tick();
51 tokio::spawn(async move {
52 let handle = MemoryBudget { state };
53 let mut ticker = tokio::time::interval(interval);
54 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
55 loop {
56 ticker.tick().await;
57 handle.tick_now();
58 }
59 })
60 }
61
62 fn run_tick(&self, rss_sysinfo_bytes: u64) -> BudgetSnapshot {
66 let effective_target = self
67 .state
68 .config
69 .target_bytes()
70 .saturating_sub(self.state.active_lease_bytes.load(Ordering::Relaxed));
71 let jemalloc = self.state.jemalloc.read();
72 let rss_bytes = jemalloc.observed_usage_bytes(rss_sysinfo_bytes);
73 let live = self.live_views();
74 let previous_cache_bytes = self.state.previous_cache_bytes.load(Ordering::Relaxed);
75 let previous_cache_count = self.state.previous_cache_count.load(Ordering::Relaxed) as u32;
76 let previous_rss_bytes = self.state.previous_rss_bytes.load(Ordering::Relaxed);
77 let input = PolicyInput {
78 effective_target_bytes: effective_target,
79 rss_bytes,
80 caches: live.views,
81 previous_cache_bytes,
82 previous_cache_count,
83 previous_rss_bytes,
84 };
85 let output = self.state.policy.reapportion(&input);
86 for (cache, new_max) in live.live_caches.iter().zip(output.new_max_bytes.iter()) {
87 cache.set_max_bytes(*new_max);
88 }
89 let current_cache_bytes: u64 = input.caches.iter().map(|c| c.current_bytes).sum();
95 self.state
96 .previous_cache_bytes
97 .store(current_cache_bytes, Ordering::Relaxed);
98 self.state
99 .previous_cache_count
100 .store(input.caches.len() as u64, Ordering::Relaxed);
101 self.state
102 .previous_rss_bytes
103 .store(rss_bytes, Ordering::Relaxed);
104 let verdict_label = output.verdict.label();
105 let (verdict_streak, previous_verdict_label, previous_verdict_streak) =
106 self.advance_verdict_streak(verdict_label);
107 let reporters = self.live_reporters();
108 BudgetSnapshot {
109 target_bytes: self.state.config.target_bytes(),
110 effective_target_bytes: effective_target,
111 rss_bytes,
112 rss_sysinfo_bytes,
113 lease_bytes_active: self.state.active_lease_bytes.load(Ordering::Relaxed),
114 jemalloc,
115 verdict: output.verdict,
116 verdict_streak,
117 previous_verdict_label,
118 previous_verdict_streak,
119 reporters,
120 caches: input
121 .caches
122 .into_iter()
123 .zip(output.new_max_bytes)
124 .map(|(view, new_max)| CacheSnapshot {
125 name: view.name,
126 current_bytes: view.current_bytes,
127 previous_max_bytes: view.max_bytes,
128 new_max_bytes: new_max,
129 hit_rate: view.stats.hit_rate(),
130 })
131 .collect(),
132 }
133 }
134
135 fn advance_verdict_streak(
140 &self,
141 label: &'static str,
142 ) -> (u32, Option<&'static str>, Option<u32>) {
143 let mut guard = self
144 .state
145 .verdict_streak
146 .lock()
147 .expect("verdict streak lock");
148 if guard.label == label && guard.length > 0 {
149 guard.length = guard.length.saturating_add(1);
150 (guard.length, None, None)
151 } else {
152 let prev_label = if guard.length > 0 {
153 Some(guard.label)
154 } else {
155 None
156 };
157 let prev_streak = if guard.length > 0 {
158 Some(guard.length)
159 } else {
160 None
161 };
162 guard.label = label;
163 guard.length = 1;
164 (1, prev_label, prev_streak)
165 }
166 }
167
168 fn live_views(&self) -> LiveViews {
171 let mut guard = self.state.registrations.lock().expect("registrations lock");
172 let mut views = Vec::with_capacity(guard.len());
173 let mut live_caches: Vec<Arc<dyn Resizable>> = Vec::with_capacity(guard.len());
174 guard.retain(|reg| {
175 let Some(cache) = reg.cache.upgrade() else {
176 return false;
177 };
178 views.push(CacheView {
179 name: cache.name().to_owned(),
180 max_bytes: cache.max_bytes(),
181 preferred_max_bytes: reg.preferred_max_bytes,
182 current_bytes: cache.current_bytes(),
183 stats: cache.stats(),
184 });
185 live_caches.push(cache);
186 true
187 });
188 LiveViews { views, live_caches }
189 }
190
191 fn live_reporters(&self) -> Vec<ReporterSnapshot> {
194 let mut guard = self.state.reporters.lock().expect("reporters lock");
195 let mut out = Vec::with_capacity(guard.len());
196 guard.retain(|weak| {
197 let Some(reporter) = weak.upgrade() else {
198 return false;
199 };
200 out.push(ReporterSnapshot {
201 name: reporter.name().to_owned(),
202 current_bytes: reporter.current_bytes(),
203 });
204 true
205 });
206 out
207 }
208}