Skip to main content

cranpose_services/
memory_pressure.rs

1//! Memory pressure the platform reports for this process.
2//!
3//! Android delivers it through `onTrimMemory`; other hosts publish their own
4//! signal. There is no backlog: pressure describes a moment, so an observer
5//! that registers later waits for the next report. Applications collect the
6//! stream and give back what they can rebuild — caches, warm model sessions,
7//! pools.
8
9use std::sync::{Arc, Mutex, OnceLock, atomic::Ordering};
10
11use cranpose_core::{EventStream, rememberEventStream};
12use portable_atomic::AtomicU64;
13
14/// How hard the platform asks for memory back.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum MemoryPressure {
17    /// The UI left the screen. Anything held only to draw the next frame fast
18    /// can go.
19    UiHidden,
20    /// The process should give back what it can rebuild.
21    Low,
22    /// The system reclaims by force next. Free everything that can go.
23    Critical,
24}
25
26impl MemoryPressure {
27    /// Maps an Android `ComponentCallbacks2` trim level.
28    pub fn from_android_trim_level(level: i32) -> Self {
29        match level {
30            20 => Self::UiHidden,
31            level if level >= 60 || level == 15 => Self::Critical,
32            _ => Self::Low,
33        }
34    }
35}
36
37type Observer = Arc<dyn Fn(MemoryPressure) + Send + Sync>;
38
39struct Registry {
40    observers: Vec<(u64, Observer)>,
41}
42
43impl Registry {
44    fn new() -> Self {
45        Self {
46            observers: Vec::new(),
47        }
48    }
49
50    fn observe(&mut self, id: u64, observer: Observer) {
51        self.observers.push((id, observer));
52    }
53
54    fn publish(&self) -> Vec<Observer> {
55        self.observers
56            .iter()
57            .map(|(_, observer)| Arc::clone(observer))
58            .collect()
59    }
60
61    fn remove_observer(&mut self, id: u64) {
62        self.observers.retain(|(existing, _)| *existing != id);
63    }
64}
65
66fn registry() -> &'static Mutex<Registry> {
67    static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
68    REGISTRY.get_or_init(|| Mutex::new(Registry::new()))
69}
70
71static NEXT_ID: AtomicU64 = AtomicU64::new(1);
72
73/// Keeps an observer registered until it is dropped.
74pub struct MemoryPressureObserver {
75    id: u64,
76}
77
78impl Drop for MemoryPressureObserver {
79    fn drop(&mut self) {
80        if let Ok(mut registry) = registry().lock() {
81            registry.remove_observer(self.id);
82        }
83    }
84}
85
86/// Registers `observer` for pressure reports.
87///
88/// Applications collect the stream from [`rememberMemoryPressure`] instead of
89/// calling this.
90pub fn observe_memory_pressure(
91    observer: impl Fn(MemoryPressure) + Send + Sync + 'static,
92) -> MemoryPressureObserver {
93    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
94    if let Ok(mut registry) = registry().lock() {
95        registry.observe(id, Arc::new(observer));
96    }
97    MemoryPressureObserver { id }
98}
99
100/// Publishes a pressure report. Callable from any thread; the framework moves
101/// each report onto the UI thread before a composition sees it.
102pub fn publish_memory_pressure(pressure: MemoryPressure) {
103    let observers = {
104        let Ok(registry) = registry().lock() else {
105            return;
106        };
107        registry.publish()
108    };
109    for observer in observers {
110        observer(pressure);
111    }
112}
113
114/// Collects pressure reports for as long as this call stays in the
115/// composition.
116///
117/// ```rust,no_run
118/// use cranpose_macros::composable;
119/// use cranpose_services::rememberMemoryPressure;
120///
121/// #[composable]
122/// fn Caches() {
123///     let pressure = rememberMemoryPressure();
124///     cranpose_core::CollectEvents(pressure, (), |report| {
125///         log::info!("memory pressure: {report:?}");
126///     });
127/// }
128/// ```
129#[expect(non_snake_case)]
130#[track_caller]
131pub fn rememberMemoryPressure() -> EventStream<MemoryPressure> {
132    rememberEventStream((), |sender| {
133        observe_memory_pressure(move |pressure| sender.send(pressure))
134    })
135}
136
137#[cfg(test)]
138#[path = "tests/memory_pressure_tests.rs"]
139mod tests;