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