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