1use std::{
2 collections::BTreeMap,
3 sync::{
4 Arc, Mutex, OnceLock,
5 atomic::{AtomicU64, Ordering},
6 },
7};
8
9use crate::{
10 budget::MemoryBudget,
11 discovery::{MemoryProbe, SystemMemoryProbe, discover_host, discover_process_memory},
12 error::{MemoryError, MemoryResult},
13 pool::{MemoryPool, MemoryPoolInner, ReservationAccount, ResourceLedger},
14 report::{MemoryReport, MemoryResourceReport, ProcessMemoryReport},
15 resource::{DeviceIdentity, MemoryResource, MemoryResourceKind},
16};
17
18#[derive(Clone, Debug)]
20pub struct MemoryState {
21 inner: Arc<MemoryStateInner>,
22}
23
24#[derive(Debug)]
25pub(crate) struct MemoryStateInner {
26 pub(crate) resources: Mutex<BTreeMap<String, ResourceLedger>>,
27 process_high_water: AtomicU64,
28}
29
30impl MemoryState {
31 pub fn discover() -> Self {
33 let host = discover_host();
34 let mut resources = BTreeMap::new();
35 resources.insert(host.id.clone(), ResourceLedger::new(host));
36 Self {
37 inner: Arc::new(MemoryStateInner {
38 resources: Mutex::new(resources),
39 process_high_water: AtomicU64::new(0),
40 }),
41 }
42 }
43
44 pub fn current() -> Self {
46 static CURRENT: OnceLock<MemoryState> = OnceLock::new();
47 CURRENT.get_or_init(Self::discover).clone()
48 }
49
50 pub fn refresh(&self) {
52 self.refresh_inner();
53 }
54 fn refresh_inner(&self) -> Option<ProcessMemoryReport> {
55 self.refresh_with_probe(&SystemMemoryProbe)
56 }
57
58 fn refresh_with_probe(&self, probe: &dyn MemoryProbe) -> Option<ProcessMemoryReport> {
59 let host = discover_host();
60 let process = self.sample_process_memory();
61 let targets = {
62 let mut resources = self
63 .inner
64 .resources
65 .lock()
66 .unwrap_or_else(|e| e.into_inner());
67 let ledger = resources
68 .entry(host.id.clone())
69 .or_insert_with(|| ResourceLedger::new(host.clone()));
70 ledger.update_snapshot(host);
71 resources
72 .values()
73 .filter(|ledger| ledger.snapshot.is_refreshable())
74 .map(|ledger| ledger.snapshot.clone())
75 .collect::<Vec<_>>()
76 };
77 let outcomes = targets
78 .into_iter()
79 .map(|target| {
80 let outcome = probe.probe_device(&target);
81 (target, outcome)
82 })
83 .collect::<Vec<_>>();
84 let mut resources = self
85 .inner
86 .resources
87 .lock()
88 .unwrap_or_else(|e| e.into_inner());
89 for (target, outcome) in outcomes {
90 if let Ok(snapshot) = outcome
91 && let Some(ledger) = resources.get_mut(&target.id)
92 {
93 ledger.apply_telemetry(&target, snapshot);
94 }
95 }
96 process
97 }
98
99 pub fn host(&self) -> MemoryResource {
101 self.resource("host").unwrap_or_else(discover_host)
102 }
103
104 pub fn register_discovered_device(
110 &self,
111 id: impl Into<String>,
112 name: impl Into<String>,
113 identity: DeviceIdentity,
114 fallback_bytes: u64,
115 ) {
116 self.insert_device_snapshot(MemoryResource::discover_device(
117 id,
118 name,
119 identity,
120 fallback_bytes,
121 ));
122 }
123
124 pub fn override_device_capacity(
126 &self,
127 id: impl Into<String>,
128 name: impl Into<String>,
129 total_bytes: u64,
130 available_bytes: Option<u64>,
131 ) {
132 self.insert_device_snapshot(MemoryResource::user_device(
133 id,
134 name,
135 total_bytes,
136 available_bytes,
137 ));
138 }
139
140 pub(crate) fn insert_device_snapshot(&self, resource: MemoryResource) {
141 let mut resources = self
142 .inner
143 .resources
144 .lock()
145 .unwrap_or_else(|e| e.into_inner());
146 if let Some(ledger) = resources.get_mut(&resource.id) {
147 ledger.update_snapshot(resource);
148 } else {
149 resources.insert(resource.id.clone(), ResourceLedger::new(resource));
150 }
151 }
152
153 pub fn resource(&self, id: &str) -> Option<MemoryResource> {
155 self.inner
156 .resources
157 .lock()
158 .unwrap_or_else(|e| e.into_inner())
159 .get(id)
160 .map(|ledger| ledger.snapshot.clone())
161 }
162
163 pub fn devices(&self) -> Vec<MemoryResource> {
165 self.inner
166 .resources
167 .lock()
168 .unwrap_or_else(|e| e.into_inner())
169 .values()
170 .filter(|ledger| ledger.snapshot.kind == MemoryResourceKind::Device)
171 .map(|ledger| ledger.snapshot.clone())
172 .collect()
173 }
174
175 pub fn pool(&self, resource_id: &str, budget: MemoryBudget) -> MemoryResult<MemoryPool> {
181 let resource = self.resource(resource_id).ok_or_else(|| {
182 MemoryError::InvalidBudget(format!("unknown memory resource {resource_id:?}"))
183 })?;
184 resource.validate().map_err(|error| {
185 MemoryError::InvalidBudget(format!(
186 "invalid memory resource {resource_id:?}: {error:?}"
187 ))
188 })?;
189 let capacity = budget.resolve(&resource)?;
190 Ok(MemoryPool {
191 inner: Arc::new(MemoryPoolInner {
192 requested: budget,
193 accounting: Arc::new(ReservationAccount::new(
194 Arc::downgrade(&self.inner),
195 resource_id.to_owned(),
196 capacity,
197 )),
198 }),
199 })
200 }
201
202 pub fn report(&self) -> MemoryReport {
204 let process = self.refresh_inner();
205 let resources = self
206 .inner
207 .resources
208 .lock()
209 .unwrap_or_else(|e| e.into_inner());
210 MemoryReport {
211 process,
212 resources: resources
213 .values()
214 .map(|ledger| MemoryResourceReport {
215 resource: ledger.snapshot.clone(),
216 laddu_reserved_bytes: ledger.reserved,
217 laddu_high_water_bytes: ledger.high_water,
218 })
219 .collect(),
220 }
221 }
222
223 fn sample_process_memory(&self) -> Option<ProcessMemoryReport> {
224 let snapshot = discover_process_memory().ok()?;
225 self.inner
226 .process_high_water
227 .fetch_max(snapshot.resident_bytes, Ordering::AcqRel);
228 Some(ProcessMemoryReport {
229 resident_bytes: snapshot.resident_bytes,
230 virtual_bytes: snapshot.virtual_bytes,
231 sampled_high_water_bytes: self.inner.process_high_water.load(Ordering::Acquire),
232 })
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::{
240 CapacitySource, DeviceIdentity, MemoryResourceKind,
241 discovery::{CapacitySnapshot, ProbeFailure, ProbeOutcome},
242 };
243 use std::{sync::mpsc, thread, time::Duration};
244
245 fn resource() -> MemoryResource {
246 MemoryResource {
247 id: "test".into(),
248 name: "Test".into(),
249 kind: MemoryResourceKind::Device,
250 total_bytes: Some(1_000),
251 available_bytes: Some(500),
252 capacity_source: CapacitySource::User,
253 device_identity: None,
254 }
255 }
256 fn refreshable(identity: usize) -> MemoryResource {
257 MemoryResource {
258 capacity_source: CapacitySource::Adaptive,
259 device_identity: Some(DeviceIdentity {
260 adapter_index: identity,
261 vendor_id: 1,
262 device_id: 2,
263 pci_bus_id: format!("bus-{identity}"),
264 }),
265 ..resource()
266 }
267 }
268 fn snapshot(total_bytes: u64, available_bytes: u64) -> CapacitySnapshot {
269 CapacitySnapshot {
270 total_bytes,
271 available_bytes,
272 source: CapacitySource::Drm,
273 }
274 }
275
276 #[derive(Clone, Copy)]
277 struct FixedProbe(ProbeOutcome);
278 impl MemoryProbe for FixedProbe {
279 fn probe_device(&self, _: &MemoryResource) -> ProbeOutcome {
280 self.0
281 }
282 }
283 struct BlockingProbe {
284 entered: mpsc::SyncSender<()>,
285 release: Mutex<mpsc::Receiver<()>>,
286 outcome: ProbeOutcome,
287 }
288 impl MemoryProbe for BlockingProbe {
289 fn probe_device(&self, _: &MemoryResource) -> ProbeOutcome {
290 self.entered.send(()).unwrap();
291 self.release.lock().unwrap().recv().unwrap();
292 self.outcome
293 }
294 }
295 struct ReplacingProbe {
296 state: MemoryState,
297 replacement: MemoryResource,
298 outcome: ProbeOutcome,
299 }
300 impl MemoryProbe for ReplacingProbe {
301 fn probe_device(&self, _: &MemoryResource) -> ProbeOutcome {
302 self.state.insert_device_snapshot(self.replacement.clone());
303 self.outcome
304 }
305 }
306
307 #[test]
308 fn ledgers_clamp_capacity_and_keep_user_overrides_sticky() {
309 let state = MemoryState::discover();
310 let discovered = MemoryResource {
311 total_bytes: Some(1_000),
312 available_bytes: Some(1_100),
313 ..refreshable(0)
314 };
315 state.insert_device_snapshot(discovered);
316 assert_eq!(state.resource("test").unwrap().available_bytes, Some(1_000));
317 let user = MemoryResource::user_device("test", "Test", 700, Some(650));
318 state.override_device_capacity("test", "Test", 700, Some(650));
319 state.insert_device_snapshot(MemoryResource {
320 total_bytes: Some(900),
321 available_bytes: Some(800),
322 ..refreshable(0)
323 });
324 assert_eq!(state.resource("test"), Some(user));
325
326 let replacement = MemoryResource::user_device("test", "Test", 600, Some(550));
327 state.override_device_capacity("test", "Test", 600, Some(550));
328 assert_eq!(state.resource("test"), Some(replacement));
329 assert_eq!(
330 state
331 .devices()
332 .into_iter()
333 .filter(|resource| resource.id == "test")
334 .count(),
335 1
336 );
337 }
338
339 #[test]
340 fn refresh_probes_without_holding_the_resource_lock() {
341 let state = MemoryState::discover();
342 state.insert_device_snapshot(refreshable(0));
343 let (entered_tx, entered_rx) = mpsc::sync_channel(0);
344 let (release_tx, release_rx) = mpsc::sync_channel(0);
345 let probe = BlockingProbe {
346 entered: entered_tx,
347 release: Mutex::new(release_rx),
348 outcome: Ok(snapshot(900, 700)),
349 };
350 let refresh_state = state.clone();
351 let refresh = thread::spawn(move || refresh_state.refresh_with_probe(&probe));
352 entered_rx.recv_timeout(Duration::from_secs(2)).unwrap();
353 let access_state = state.clone();
354 let (done_tx, done_rx) = mpsc::sync_channel(0);
355 let access = thread::spawn(move || {
356 assert!(access_state.resource("test").is_some());
357 done_tx.send(()).unwrap();
358 });
359 let result = done_rx.recv_timeout(Duration::from_secs(2));
360 release_tx.send(()).unwrap();
361 refresh.join().unwrap();
362 access.join().unwrap();
363 assert!(result.is_ok(), "resource access blocked during telemetry");
364 }
365
366 #[test]
367 fn refresh_retains_stale_snapshot_on_failure() {
368 let state = MemoryState::discover();
369 state.insert_device_snapshot(refreshable(0));
370 state.refresh_with_probe(&FixedProbe(Ok(snapshot(800, 600))));
371 let refreshed = state.resource("test").unwrap();
372 state.refresh_with_probe(&FixedProbe(Err(ProbeFailure::Unavailable)));
373 assert_eq!(state.resource("test"), Some(refreshed));
374 }
375
376 #[test]
377 fn refresh_does_not_overwrite_user_override_or_replaced_device() {
378 let state = MemoryState::discover();
379 state.insert_device_snapshot(refreshable(0));
380 let user = MemoryResource::user_device("test", "Test", 700, Some(650));
381 state.refresh_with_probe(&ReplacingProbe {
382 state: state.clone(),
383 replacement: user.clone(),
384 outcome: Ok(snapshot(900, 800)),
385 });
386 assert_eq!(state.resource("test"), Some(user));
387
388 let state = MemoryState::discover();
389 state.insert_device_snapshot(refreshable(0));
390 let replacement = MemoryResource {
391 total_bytes: Some(400),
392 available_bytes: Some(300),
393 ..refreshable(1)
394 };
395 state.refresh_with_probe(&ReplacingProbe {
396 state: state.clone(),
397 replacement: replacement.clone(),
398 outcome: Ok(snapshot(900, 800)),
399 });
400 assert_eq!(state.resource("test"), Some(replacement));
401 }
402
403 #[test]
404 #[cfg(target_os = "linux")]
405 fn reports_current_process_memory() {
406 let state = MemoryState::discover();
407 let first = state.report().process.unwrap();
408 let second = state.report().process.unwrap();
409 assert!(first.resident_bytes > 0);
410 assert!(first.virtual_bytes >= first.resident_bytes);
411 assert!(second.sampled_high_water_bytes >= second.resident_bytes);
412 }
413}