1use std::cell::RefCell;
16use std::collections::HashMap;
17use std::rc::Rc;
18
19use day_spec::Lifecycle;
20
21type Handlers = Vec<Rc<dyn Fn()>>;
23
24day_reactive::tls_slots! {
25 lifecycle;
26 static HANDLERS: RefCell<HashMap<Lifecycle, Handlers>> =
27 RefCell::new(HashMap::new());
28 static WARNED: RefCell<std::collections::HashSet<Lifecycle>> =
30 RefCell::new(std::collections::HashSet::new());
31}
32
33static EXITED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
36static LAUNCHED: std::sync::Mutex<Option<Launched>> = std::sync::Mutex::new(None);
39
40struct Launched {
41 name: String,
42 version: Option<String>,
43 #[cfg(not(target_arch = "wasm32"))]
44 at: std::time::Instant,
45}
46
47pub fn note_launch(name: String, version: Option<String>) {
50 crate::memory_profile::start();
51 if let Ok(mut l) = LAUNCHED.lock() {
52 *l = Some(Launched {
53 name,
54 version,
55 #[cfg(not(target_arch = "wasm32"))]
56 at: std::time::Instant::now(),
57 });
58 }
59}
60
61fn exit_summary() -> String {
66 let (name, version, elapsed) = {
67 let l = LAUNCHED.lock().ok();
68 let l = l.as_deref().and_then(Option::as_ref);
69 let name = l
70 .map(|l| l.name.clone())
71 .filter(|n| !n.is_empty())
72 .unwrap_or_else(|| "the app".to_string());
73 let version = l
74 .and_then(|l| l.version.clone())
75 .or_else(|| std::env::var("DAY_APP_VERSION").ok())
76 .filter(|v| !v.is_empty());
77 #[cfg(not(target_arch = "wasm32"))]
78 let elapsed = l.map(|l| l.at.elapsed().as_secs());
79 #[cfg(target_arch = "wasm32")]
80 let elapsed = crate::frame::uptime_secs();
81 (name, version, elapsed)
82 };
83 let memory = resident_memory();
84 let mut summary = format_exit_summary(
85 &name,
86 version.as_deref(),
87 cfg!(debug_assertions),
88 elapsed,
89 memory,
90 );
91 if let Some(profile) = crate::memory_profile::finish(memory) {
92 summary.push_str(". ");
93 summary.push_str(&profile);
94 }
95 summary
96}
97
98fn format_exit_summary(
100 name: &str,
101 version: Option<&str>,
102 debug: bool,
103 elapsed_secs: Option<u64>,
104 memory_bytes: Option<u64>,
105) -> String {
106 let version = match version {
107 Some(v) => format!(" v{v}"),
108 None => String::new(),
109 };
110 let profile = if debug { "debug" } else { "release" };
111 let elapsed = match elapsed_secs {
112 Some(s) => format!("{:02}:{:02}:{:02}", s / 3600, s / 60 % 60, s % 60),
113 None => "an unknown time".to_string(),
114 };
115 let memory = match memory_bytes {
116 Some(b) => format!("{:.2} MB", b as f64 / 1e6),
117 None => "unknown".to_string(),
118 };
119 format!("Clean exit for {name}{version} ({profile}) after {elapsed}. Memory usage: {memory}")
120}
121
122pub fn resident_memory() -> Option<u64> {
127 #[cfg(any(target_os = "macos", target_os = "ios"))]
128 {
129 #[repr(C)]
131 struct TaskVmInfo {
132 virtual_size: u64,
135 region_count: i32,
136 page_size: i32,
137 resident_size: u64,
138 resident_size_peak: u64,
139 device: u64,
140 device_peak: u64,
141 internal: u64,
142 internal_peak: u64,
143 external: u64,
144 external_peak: u64,
145 reusable: u64,
146 reusable_peak: u64,
147 purgeable_volatile_pmap: u64,
148 purgeable_volatile_resident: u64,
149 purgeable_volatile_virtual: u64,
150 compressed: u64,
151 compressed_peak: u64,
152 compressed_lifetime: u64,
153 phys_footprint: u64,
154 }
155 unsafe extern "C" {
156 fn mach_task_self() -> u32;
157 fn task_info(task: u32, flavor: u32, info: *mut TaskVmInfo, count: *mut u32) -> i32;
158 }
159 const TASK_VM_INFO: u32 = 22;
160 let mut info = std::mem::MaybeUninit::<TaskVmInfo>::zeroed();
161 let mut count = (std::mem::size_of::<TaskVmInfo>() / std::mem::size_of::<u32>()) as u32;
162 let ok = unsafe {
165 task_info(
166 mach_task_self(),
167 TASK_VM_INFO,
168 info.as_mut_ptr(),
169 &mut count,
170 )
171 };
172 if ok != 0 {
173 return None;
174 }
175 let info = unsafe { info.assume_init() };
177 return Some(if info.phys_footprint > 0 {
178 info.phys_footprint
179 } else {
180 info.resident_size
181 });
182 }
183 #[cfg(any(target_os = "linux", target_os = "android", target_env = "ohos"))]
184 {
185 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
187 let resident: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
188 unsafe extern "C" {
189 fn sysconf(name: i32) -> i64;
190 }
191 #[cfg(target_os = "android")]
192 const SC_PAGESIZE: i32 = 40;
193 #[cfg(not(target_os = "android"))]
194 const SC_PAGESIZE: i32 = 30;
195 let page = unsafe { sysconf(SC_PAGESIZE) };
197 return Some(resident * u64::try_from(page).unwrap_or(4096));
198 }
199 #[cfg(windows)]
200 {
201 #[repr(C)]
202 struct ProcessMemoryCounters {
203 cb: u32,
204 page_fault_count: u32,
205 peak_working_set_size: usize,
206 working_set_size: usize,
207 quota_peak_paged_pool_usage: usize,
208 quota_paged_pool_usage: usize,
209 quota_peak_non_paged_pool_usage: usize,
210 quota_non_paged_pool_usage: usize,
211 pagefile_usage: usize,
212 peak_pagefile_usage: usize,
213 }
214 #[link(name = "kernel32")]
215 unsafe extern "system" {
216 fn GetCurrentProcess() -> isize;
217 fn K32GetProcessMemoryInfo(
218 process: isize,
219 counters: *mut ProcessMemoryCounters,
220 cb: u32,
221 ) -> i32;
222 }
223 let mut counters = std::mem::MaybeUninit::<ProcessMemoryCounters>::zeroed();
224 let cb = std::mem::size_of::<ProcessMemoryCounters>() as u32;
225 let ok = unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), counters.as_mut_ptr(), cb) };
228 if ok == 0 {
229 return None;
230 }
231 return Some(unsafe { counters.assume_init() }.working_set_size as u64);
233 }
234 #[cfg(target_arch = "wasm32")]
235 {
236 return Some(core::arch::wasm32::memory_size(0) as u64 * 65536);
238 }
239 #[allow(unreachable_code)]
240 None
241}
242
243pub fn reset_handlers() {
247 HANDLERS.with(|h| h.borrow_mut().clear());
248 WARNED.with(|w| w.borrow_mut().clear());
249}
250
251pub fn on_lifecycle(phase: Lifecycle, f: impl Fn() + 'static) {
260 HANDLERS.with(|h| h.borrow_mut().entry(phase).or_default().push(Rc::new(f)));
261 if crate::tree::has_tree() {
264 warn_if_unsupported(phase);
265 }
266}
267
268pub fn dispatch_lifecycle(phase: Lifecycle) {
275 if phase == Lifecycle::WillTerminate {
278 crate::windows::save_remembered_frames();
279 crate::present::shutdown_tasks();
280 }
281 if phase == Lifecycle::DidExit {
282 if EXITED.swap(true, std::sync::atomic::Ordering::SeqCst) {
283 return;
284 }
285 if HANDLERS.try_with(|_| ()).is_ok() {
288 crate::present::shutdown_tasks();
289 dispatch_handlers(phase);
290 }
291 log::info!("{}", exit_summary());
292 return;
293 }
294 dispatch_handlers(phase);
295}
296
297fn dispatch_handlers(phase: Lifecycle) {
298 crate::frame::lifecycle(phase);
299 let handlers = HANDLERS.with(|h| h.borrow().get(&phase).cloned().unwrap_or_default());
300 if handlers.is_empty() {
301 return;
302 }
303 let mut any_panicked = false;
304 day_reactive::batch(|| {
305 for f in &handlers {
306 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f())).is_err() {
314 any_panicked = true;
315 log::warn!(
316 "an on_lifecycle({}) handler panicked and was contained — the app continues.",
317 phase.name()
318 );
319 crate::notify_contained_panic();
320 }
321 }
322 });
323 if any_panicked {
326 day_reactive::recover_from_panic();
327 }
328}
329
330pub fn lifecycle_supported(phase: Lifecycle) -> bool {
335 if crate::tree::has_tree() {
336 crate::with_tree(|t| t.supports_lifecycle(phase))
337 } else {
338 phase.is_universal()
339 }
340}
341
342fn warn_if_unsupported(phase: Lifecycle) {
343 if lifecycle_supported(phase) {
344 return;
345 }
346 let first = WARNED.with(|w| w.borrow_mut().insert(phase));
347 if first {
348 log::warn!(
349 "an `on_lifecycle({})` handler was registered, but this backend never delivers \
350 that phase, so it will not run. Guard it with `day::lifecycle_supported(..)` or a \
351 `day::require_lifecycle!(..)` compile-time check (docs/lifecycle.md).",
352 phase.name()
353 );
354 }
355}
356
357pub fn warn_unsupported_registrations() {
360 let phases: Vec<Lifecycle> = HANDLERS.with(|h| h.borrow().keys().copied().collect());
361 for p in phases {
362 warn_if_unsupported(p);
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use std::cell::Cell;
370
371 #[test]
372 fn the_exit_line_reads_as_documented() {
373 assert_eq!(
374 format_exit_summary(
375 "Day Rise",
376 Some("0.4.5"),
377 true,
378 Some(3 * 3600 + 7 * 60 + 9),
379 Some(12_345_678)
380 ),
381 "Clean exit for Day Rise v0.4.5 (debug) after 03:07:09. Memory usage: 12.35 MB"
382 );
383 assert_eq!(
384 format_exit_summary("", None, false, None, None),
385 "Clean exit for (release) after an unknown time. Memory usage: unknown"
386 );
387 }
388
389 #[test]
390 fn resident_memory_is_readable_here() {
391 assert!(resident_memory().is_some_and(|b| b > 1 << 20));
393 }
394
395 #[test]
396 fn did_exit_runs_its_handlers_once() {
397 thread_local! {
398 static N: Cell<u32> = const { Cell::new(0) };
399 }
400 let task = crate::task(std::future::pending());
401 on_lifecycle(Lifecycle::DidExit, move || {
402 assert!(task.is_finished());
403 N.with(|c| c.set(c.get() + 1));
404 });
405 dispatch_lifecycle(Lifecycle::DidExit);
406 dispatch_lifecycle(Lifecycle::DidExit);
407 assert_eq!(N.with(Cell::get), 1);
408 EXITED.store(false, std::sync::atomic::Ordering::SeqCst);
409 }
410
411 #[test]
412 fn dispatch_runs_handlers_for_the_phase_only() {
413 thread_local! {
414 static A: Cell<u32> = const { Cell::new(0) };
415 static B: Cell<u32> = const { Cell::new(0) };
416 }
417 on_lifecycle(Lifecycle::DidLaunch, || A.with(|c| c.set(c.get() + 1)));
418 on_lifecycle(Lifecycle::DidLaunch, || A.with(|c| c.set(c.get() + 1)));
419 on_lifecycle(Lifecycle::WillTerminate, || B.with(|c| c.set(c.get() + 1)));
420
421 dispatch_lifecycle(Lifecycle::DidLaunch);
422 assert_eq!(A.with(Cell::get), 2, "both DidLaunch handlers ran");
423 assert_eq!(B.with(Cell::get), 0, "WillTerminate handler did not run");
424
425 dispatch_lifecycle(Lifecycle::WillTerminate);
426 assert_eq!(B.with(Cell::get), 1);
427
428 dispatch_lifecycle(Lifecycle::DidReceiveMemoryWarning);
430 }
431
432 #[test]
433 fn panicking_handler_is_contained_and_siblings_still_run() {
434 thread_local! {
435 static RAN: Cell<u32> = const { Cell::new(0) };
436 }
437 on_lifecycle(Lifecycle::WillResignActive, || {
440 panic!("boom in a lifecycle handler")
441 });
442 on_lifecycle(Lifecycle::WillResignActive, || {
443 RAN.with(|c| c.set(c.get() + 1))
444 });
445
446 dispatch_lifecycle(Lifecycle::WillResignActive);
448 assert_eq!(
449 RAN.with(Cell::get),
450 1,
451 "the non-panicking sibling handler still ran"
452 );
453 }
454}