use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use day_spec::Lifecycle;
type Handlers = Vec<Rc<dyn Fn()>>;
day_reactive::tls_slots! {
lifecycle;
static HANDLERS: RefCell<HashMap<Lifecycle, Handlers>> =
RefCell::new(HashMap::new());
static WARNED: RefCell<std::collections::HashSet<Lifecycle>> =
RefCell::new(std::collections::HashSet::new());
}
static EXITED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static LAUNCHED: std::sync::Mutex<Option<Launched>> = std::sync::Mutex::new(None);
struct Launched {
name: String,
version: Option<String>,
#[cfg(not(target_arch = "wasm32"))]
at: std::time::Instant,
}
pub fn note_launch(name: String, version: Option<String>) {
crate::memory_profile::start();
if let Ok(mut l) = LAUNCHED.lock() {
*l = Some(Launched {
name,
version,
#[cfg(not(target_arch = "wasm32"))]
at: std::time::Instant::now(),
});
}
}
fn exit_summary() -> String {
let (name, version, elapsed) = {
let l = LAUNCHED.lock().ok();
let l = l.as_deref().and_then(Option::as_ref);
let name = l
.map(|l| l.name.clone())
.filter(|n| !n.is_empty())
.unwrap_or_else(|| "the app".to_string());
let version = l
.and_then(|l| l.version.clone())
.or_else(|| std::env::var("DAY_APP_VERSION").ok())
.filter(|v| !v.is_empty());
#[cfg(not(target_arch = "wasm32"))]
let elapsed = l.map(|l| l.at.elapsed().as_secs());
#[cfg(target_arch = "wasm32")]
let elapsed = crate::frame::uptime_secs();
(name, version, elapsed)
};
let memory = resident_memory();
let mut summary = format_exit_summary(
&name,
version.as_deref(),
cfg!(debug_assertions),
elapsed,
memory,
);
if let Some(profile) = crate::memory_profile::finish(memory) {
summary.push_str(". ");
summary.push_str(&profile);
}
summary
}
fn format_exit_summary(
name: &str,
version: Option<&str>,
debug: bool,
elapsed_secs: Option<u64>,
memory_bytes: Option<u64>,
) -> String {
let version = match version {
Some(v) => format!(" v{v}"),
None => String::new(),
};
let profile = if debug { "debug" } else { "release" };
let elapsed = match elapsed_secs {
Some(s) => format!("{:02}:{:02}:{:02}", s / 3600, s / 60 % 60, s % 60),
None => "an unknown time".to_string(),
};
let memory = match memory_bytes {
Some(b) => format!("{:.2} MB", b as f64 / 1e6),
None => "unknown".to_string(),
};
format!("Clean exit for {name}{version} ({profile}) after {elapsed}. Memory usage: {memory}")
}
pub fn resident_memory() -> Option<u64> {
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
#[repr(C)]
struct TaskVmInfo {
virtual_size: u64,
region_count: i32,
page_size: i32,
resident_size: u64,
resident_size_peak: u64,
device: u64,
device_peak: u64,
internal: u64,
internal_peak: u64,
external: u64,
external_peak: u64,
reusable: u64,
reusable_peak: u64,
purgeable_volatile_pmap: u64,
purgeable_volatile_resident: u64,
purgeable_volatile_virtual: u64,
compressed: u64,
compressed_peak: u64,
compressed_lifetime: u64,
phys_footprint: u64,
}
unsafe extern "C" {
fn mach_task_self() -> u32;
fn task_info(task: u32, flavor: u32, info: *mut TaskVmInfo, count: *mut u32) -> i32;
}
const TASK_VM_INFO: u32 = 22;
let mut info = std::mem::MaybeUninit::<TaskVmInfo>::zeroed();
let mut count = (std::mem::size_of::<TaskVmInfo>() / std::mem::size_of::<u32>()) as u32;
let ok = unsafe {
task_info(
mach_task_self(),
TASK_VM_INFO,
info.as_mut_ptr(),
&mut count,
)
};
if ok != 0 {
return None;
}
let info = unsafe { info.assume_init() };
return Some(if info.phys_footprint > 0 {
info.phys_footprint
} else {
info.resident_size
});
}
#[cfg(any(target_os = "linux", target_os = "android", target_env = "ohos"))]
{
let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
let resident: u64 = statm.split_whitespace().nth(1)?.parse().ok()?;
unsafe extern "C" {
fn sysconf(name: i32) -> i64;
}
#[cfg(target_os = "android")]
const SC_PAGESIZE: i32 = 40;
#[cfg(not(target_os = "android"))]
const SC_PAGESIZE: i32 = 30;
let page = unsafe { sysconf(SC_PAGESIZE) };
return Some(resident * u64::try_from(page).unwrap_or(4096));
}
#[cfg(windows)]
{
#[repr(C)]
struct ProcessMemoryCounters {
cb: u32,
page_fault_count: u32,
peak_working_set_size: usize,
working_set_size: usize,
quota_peak_paged_pool_usage: usize,
quota_paged_pool_usage: usize,
quota_peak_non_paged_pool_usage: usize,
quota_non_paged_pool_usage: usize,
pagefile_usage: usize,
peak_pagefile_usage: usize,
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetCurrentProcess() -> isize;
fn K32GetProcessMemoryInfo(
process: isize,
counters: *mut ProcessMemoryCounters,
cb: u32,
) -> i32;
}
let mut counters = std::mem::MaybeUninit::<ProcessMemoryCounters>::zeroed();
let cb = std::mem::size_of::<ProcessMemoryCounters>() as u32;
let ok = unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), counters.as_mut_ptr(), cb) };
if ok == 0 {
return None;
}
return Some(unsafe { counters.assume_init() }.working_set_size as u64);
}
#[cfg(target_arch = "wasm32")]
{
return Some(core::arch::wasm32::memory_size(0) as u64 * 65536);
}
#[allow(unreachable_code)]
None
}
pub fn reset_handlers() {
HANDLERS.with(|h| h.borrow_mut().clear());
WARNED.with(|w| w.borrow_mut().clear());
}
pub fn on_lifecycle(phase: Lifecycle, f: impl Fn() + 'static) {
HANDLERS.with(|h| h.borrow_mut().entry(phase).or_default().push(Rc::new(f)));
if crate::tree::has_tree() {
warn_if_unsupported(phase);
}
}
pub fn dispatch_lifecycle(phase: Lifecycle) {
if phase == Lifecycle::WillTerminate {
crate::windows::save_remembered_frames();
crate::present::shutdown_tasks();
}
if phase == Lifecycle::DidExit {
if EXITED.swap(true, std::sync::atomic::Ordering::SeqCst) {
return;
}
if HANDLERS.try_with(|_| ()).is_ok() {
crate::present::shutdown_tasks();
dispatch_handlers(phase);
}
log::info!("{}", exit_summary());
return;
}
dispatch_handlers(phase);
}
fn dispatch_handlers(phase: Lifecycle) {
crate::frame::lifecycle(phase);
let handlers = HANDLERS.with(|h| h.borrow().get(&phase).cloned().unwrap_or_default());
if handlers.is_empty() {
return;
}
let mut any_panicked = false;
day_reactive::batch(|| {
for f in &handlers {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f())).is_err() {
any_panicked = true;
log::warn!(
"an on_lifecycle({}) handler panicked and was contained — the app continues.",
phase.name()
);
crate::notify_contained_panic();
}
}
});
if any_panicked {
day_reactive::recover_from_panic();
}
}
pub fn lifecycle_supported(phase: Lifecycle) -> bool {
if crate::tree::has_tree() {
crate::with_tree(|t| t.supports_lifecycle(phase))
} else {
phase.is_universal()
}
}
fn warn_if_unsupported(phase: Lifecycle) {
if lifecycle_supported(phase) {
return;
}
let first = WARNED.with(|w| w.borrow_mut().insert(phase));
if first {
log::warn!(
"an `on_lifecycle({})` handler was registered, but this backend never delivers \
that phase, so it will not run. Guard it with `day::lifecycle_supported(..)` or a \
`day::require_lifecycle!(..)` compile-time check (docs/lifecycle.md).",
phase.name()
);
}
}
pub fn warn_unsupported_registrations() {
let phases: Vec<Lifecycle> = HANDLERS.with(|h| h.borrow().keys().copied().collect());
for p in phases {
warn_if_unsupported(p);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn the_exit_line_reads_as_documented() {
assert_eq!(
format_exit_summary(
"Day Rise",
Some("0.4.5"),
true,
Some(3 * 3600 + 7 * 60 + 9),
Some(12_345_678)
),
"Clean exit for Day Rise v0.4.5 (debug) after 03:07:09. Memory usage: 12.35 MB"
);
assert_eq!(
format_exit_summary("", None, false, None, None),
"Clean exit for (release) after an unknown time. Memory usage: unknown"
);
}
#[test]
fn resident_memory_is_readable_here() {
assert!(resident_memory().is_some_and(|b| b > 1 << 20));
}
#[test]
fn did_exit_runs_its_handlers_once() {
thread_local! {
static N: Cell<u32> = const { Cell::new(0) };
}
let task = crate::task(std::future::pending());
on_lifecycle(Lifecycle::DidExit, move || {
assert!(task.is_finished());
N.with(|c| c.set(c.get() + 1));
});
dispatch_lifecycle(Lifecycle::DidExit);
dispatch_lifecycle(Lifecycle::DidExit);
assert_eq!(N.with(Cell::get), 1);
EXITED.store(false, std::sync::atomic::Ordering::SeqCst);
}
#[test]
fn dispatch_runs_handlers_for_the_phase_only() {
thread_local! {
static A: Cell<u32> = const { Cell::new(0) };
static B: Cell<u32> = const { Cell::new(0) };
}
on_lifecycle(Lifecycle::DidLaunch, || A.with(|c| c.set(c.get() + 1)));
on_lifecycle(Lifecycle::DidLaunch, || A.with(|c| c.set(c.get() + 1)));
on_lifecycle(Lifecycle::WillTerminate, || B.with(|c| c.set(c.get() + 1)));
dispatch_lifecycle(Lifecycle::DidLaunch);
assert_eq!(A.with(Cell::get), 2, "both DidLaunch handlers ran");
assert_eq!(B.with(Cell::get), 0, "WillTerminate handler did not run");
dispatch_lifecycle(Lifecycle::WillTerminate);
assert_eq!(B.with(Cell::get), 1);
dispatch_lifecycle(Lifecycle::DidReceiveMemoryWarning);
}
#[test]
fn panicking_handler_is_contained_and_siblings_still_run() {
thread_local! {
static RAN: Cell<u32> = const { Cell::new(0) };
}
on_lifecycle(Lifecycle::WillResignActive, || {
panic!("boom in a lifecycle handler")
});
on_lifecycle(Lifecycle::WillResignActive, || {
RAN.with(|c| c.set(c.get() + 1))
});
dispatch_lifecycle(Lifecycle::WillResignActive);
assert_eq!(
RAN.with(Cell::get),
1,
"the non-panicking sibling handler still ran"
);
}
}