| // Copyright 2020 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| use boot_options::BootOptions; |
| use core::cell::UnsafeCell; |
| use core::ffi::c_void; |
| use core::mem::MaybeUninit; |
| use core::pin::Pin; |
| use core::ptr::NonNull; |
| use core::sync::atomic::{AtomicBool, Ordering}; |
| use kprint::{kprint, kprintln}; |
| use object_constants_rs as object_constants; |
| use pin_init::{InPlaceWrite, PinInit, Wrapper, pin_data, pin_init}; |
| use zx_status::Status; |
| |
| use super::event_dispatcher::EventDispatcher; |
| use super::executor::Executor; |
| use crate::counters::define_kcounter; |
| use crate::debuglog_rs::dlog_shutdown; |
| use crate::kernel; |
| use crate::kernel::deadline::{Deadline, InstantUnknown, TimerSlack}; |
| use crate::kernel::event::AutounsignalEvent; |
| use crate::kernel::relaxed_atomic::RelaxedAtomicU8; |
| use crate::kernel::thread::ScopedMemoryAllocationDisabled; |
| use crate::kernel::timer::Timer; |
| use crate::platform_rs::HaltToken; |
| use crate::platform_rs::power::{PlatformHaltAction, ZirconCrashReason, platform_halt}; |
| use crate::platform_rs::timer::{DurationMono, InstantMono, current_mono_time}; |
| use crate::stall::StallAggregator; |
| use crate::vm::vm::oom_ktrace_duration; |
| use crate::vm::{evictor, pmm}; |
| |
| const PAGE_SIZE: u64 = page::SIZE as u64; |
| const MB: u64 = 1024 * 1024; |
| |
| define_kcounter!(PRESSURE_LEVEL_OOM, "memory_watchdog.pressure.oom", Sum); |
| define_kcounter!(PRESSURE_LEVEL_IMMINENT_OOM, "memory_watchdog.pressure.imminent_oom", Sum); |
| define_kcounter!(PRESSURE_LEVEL_CRITICAL, "memory_watchdog.pressure.critical", Sum); |
| define_kcounter!(PRESSURE_LEVEL_WARNING, "memory_watchdog.pressure.warning", Sum); |
| define_kcounter!(PRESSURE_LEVEL_NORMAL, "memory_watchdog.pressure.normal", Sum); |
| |
| define_kcounter!(EVICTION_TRIGGERED, "memory_watchdog.eviction.triggered", Sum); |
| |
| #[repr(u8)] |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| pub enum PressureLevel { |
| OutOfMemory = 0, |
| ImminentOutOfMemory = 1, |
| Critical = 2, |
| Warning = 3, |
| Normal = 4, |
| } |
| |
| pub const NUM_LEVELS: usize = 5; |
| const NUM_WATERMARKS: usize = NUM_LEVELS - 1; |
| |
| impl TryFrom<u8> for PressureLevel { |
| type Error = (); |
| fn try_from(val: u8) -> Result<Self, Self::Error> { |
| match val { |
| 0 => Ok(Self::OutOfMemory), |
| 1 => Ok(Self::ImminentOutOfMemory), |
| 2 => Ok(Self::Critical), |
| 3 => Ok(Self::Warning), |
| 4 => Ok(Self::Normal), |
| _ => Err(()), |
| } |
| } |
| } |
| |
| fn pressure_level_to_string(level: PressureLevel) -> &'static str { |
| match level { |
| PressureLevel::OutOfMemory => "OutOfMemory", |
| PressureLevel::ImminentOutOfMemory => "ImminentOutOfMemory", |
| PressureLevel::Critical => "Critical", |
| PressureLevel::Warning => "Warning", |
| PressureLevel::Normal => "Normal", |
| } |
| } |
| |
| /// OneShot eviction strategy only triggers eviction events at memory pressure state transitions. |
| /// Continuous eviction strategy enables continuous background eviction as long the system remains |
| /// under memory pressure (i.e. at a memory pressure level that is eligible for eviction). |
| #[repr(u8)] |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| enum EvictionStrategy { |
| OneShot = 0, |
| Continuous = 1, |
| } |
| |
| #[repr(transparent)] |
| struct RelaxedAtomicPressureLevel(RelaxedAtomicU8); |
| |
| impl RelaxedAtomicPressureLevel { |
| const fn new(level: PressureLevel) -> Self { |
| Self(RelaxedAtomicU8::new(level as u8)) |
| } |
| |
| fn load(&self) -> PressureLevel { |
| // SAFETY: PressureLevel is a u8, and we only store valid PressureLevel values. |
| unsafe { core::mem::transmute(self.0.load()) } |
| } |
| |
| fn store(&self, level: PressureLevel) { |
| self.0.store(level as u8); |
| } |
| } |
| |
| fn count_pressure_event(level: PressureLevel) { |
| match level { |
| PressureLevel::OutOfMemory => PRESSURE_LEVEL_OOM.add(1), |
| PressureLevel::ImminentOutOfMemory => PRESSURE_LEVEL_IMMINENT_OOM.add(1), |
| PressureLevel::Critical => PRESSURE_LEVEL_CRITICAL.add(1), |
| PressureLevel::Warning => PRESSURE_LEVEL_WARNING.add(1), |
| PressureLevel::Normal => PRESSURE_LEVEL_NORMAL.add(1), |
| } |
| } |
| |
| fn handle_on_oom_reboot() { |
| // Notify the pmm that although we are out of memory, we would like to never wait for memory. |
| // This ensures that if userspace needs to allocate to do a graceful shutdown it is able to. |
| pmm::node().stop_returning_should_wait(); |
| |
| if !HaltToken::get().take() { |
| // We failed to acquire the token. Someone else must have it. That's OK. We'll rely on |
| // them to halt/reboot. Nothing left for us to do but wait. |
| kprintln!("memory-pressure: halt/reboot already in progress; sleeping forever"); |
| let _ = kernel::thread::sleep(InstantMono::INFINITE); |
| } |
| // We now have the halt token so we're committed. To ensure we record the true cause of the |
| // reboot, we must ensure nothing (aside from a panic) prevents us from halting with reason OOM. |
| |
| // We are out of or nearly out of memory so future attempts to allocate may fail. From this |
| // point on, avoid performing any allocation. Establish a "no allocation allowed" scope to |
| // detect (assert) if we attempt to allocate. |
| let _allocation_disabled = ScopedMemoryAllocationDisabled::new(); |
| |
| kprintln!( |
| "memory-pressure: pausing for {}ms after OOM mem signal", |
| BootOptions::get().oom_timeout_ms |
| ); |
| let status = HaltToken::get().wait_for_ack(&Deadline::after_mono( |
| DurationMono::from_millis(BootOptions::get().oom_timeout_ms as i64), |
| TimerSlack::none(), |
| )); |
| |
| match status { |
| Ok(()) => { |
| kprintln!("memory-pressure: rebooting due to OOM. received user-mode acknowledgement."); |
| } |
| Err(Status::TIMED_OUT) => { |
| // User mode code should have acked by now, since it hasn't, reboot the system. |
| kprintln!( |
| "memory-pressure: rebooting due to OOM. timed out after waiting {}ms for \ |
| user-mode ack.", |
| BootOptions::get().oom_timeout_ms |
| ); |
| } |
| Err(status) => { |
| kprintln!( |
| "memory-pressure: rebooting due to OOM. unexpected error while waiting for \ |
| user-mode acknowledgement (status {}).", |
| status.into_raw() |
| ); |
| } |
| } |
| |
| // Tell the oom_tests host test that we are about to generate an OOM |
| // crashlog to keep it happy. Without these messages present in a |
| // specific order in the log, the test will fail. |
| kprint!("memory-pressure: stowing crashlog\nZIRCON REBOOT REASON (OOM)\n"); |
| |
| // The debuglog could contain diagnostic messages that would assist in debugging the cause of |
| // the OOM. Shutdown debuglog before rebooting in order to flush any queued messages. |
| // |
| // It is important that we don't hang during this process so set a deadline for the debuglog |
| // to shutdown. |
| // |
| // How long should we wait? Shutting down the debuglog includes flushing any buffered |
| // messages to the serial port (if present). Writing to a serial port can be slow. Assuming |
| // we have a full debuglog buffer of 128KB, at 115200 bps, with 8-N-1, it will take roughly |
| // 11.4 seconds to drain the buffer. The timeout should be long enough to allow a full DLOG |
| // buffer to be drained. |
| let deadline = current_mono_time() + DurationMono::from_seconds(20); |
| let status = dlog_shutdown(deadline); |
| if let Err(status) = status { |
| // If `dlog_shutdown` failed, there's not much we can do besides print an error (which |
| // probably won't make it out anyway since we've already called `dlog_shutdown`) and |
| // continue on to `platform_halt`. |
| kprintln!("ERROR: dlog_shutdown failed: {}", status.into_raw()); |
| } |
| platform_halt(PlatformHaltAction::Reboot, ZirconCrashReason::Oom); |
| } |
| |
| /// The callback provided to the `eviction_trigger` timer. |
| /// |
| /// # Safety |
| /// |
| /// `arg` must be a valid pointer to an initialized `MemoryWatchdogState`. |
| unsafe extern "C" fn eviction_trigger_callback( |
| _timer: *mut kernel::timer::Timer, |
| _now: i64, |
| arg: *mut c_void, |
| ) { |
| // SAFETY: `arg` points to a live `MemoryWatchdogState` registered when setting the timer. |
| let watchdog = unsafe { &*arg.cast::<MemoryWatchdogState>() }; |
| watchdog.eviction_trigger(); |
| } |
| |
| zr::static_assert_size_and_align!( |
| MemoryWatchdogState, |
| object_constants::kMemoryWatchdogStateSize, |
| object_constants::kMemoryWatchdogStateAlign, |
| ); |
| |
| /// Monitors system-wide memory pressure levels, signals userspace events, and triggers eviction |
| /// or out-of-memory actions. |
| /// |
| /// Object is thread safe. |
| #[pin_data] |
| #[repr(C)] |
| pub struct MemoryWatchdogState { |
| /// Kernel-owned events used to signal userspace at different levels of memory pressure. |
| mem_pressure_events: UnsafeCell<[Option<fbl::RefPtr<EventDispatcher>>; NUM_LEVELS]>, |
| |
| /// Event used for communicating memory related state changes between other parts of the system |
| /// and the `worker_thread`. |
| #[pin] |
| mem_state_signal: AutounsignalEvent, |
| |
| /// Relaxed atomic so that debug methods can safely read it. |
| mem_event_idx: RelaxedAtomicPressureLevel, |
| prev_mem_event_idx: UnsafeCell<PressureLevel>, |
| |
| /// Watermark information is not modified after `init` and so is safe for multiple threads to |
| /// access. |
| mem_watermarks: UnsafeCell<[u64; NUM_WATERMARKS]>, |
| watermark_debounce: UnsafeCell<u64>, |
| |
| /// Used to delay signaling memory level transitions in the case of rapid changes. |
| hysteresis_seconds: UnsafeCell<DurationMono>, |
| |
| /// Used to delay eviction when going from a pressure level that does not require eviction to |
| /// one that does. |
| eviction_delay_ms: UnsafeCell<DurationMono>, |
| |
| /// Tracks last time the memory state was evaluated (and signaled if required). |
| prev_mem_state_eval_time: UnsafeCell<InstantMono>, |
| |
| /// The highest pressure level we trigger eviction at, OOM being the lowest pressure level (0). |
| max_eviction_level: UnsafeCell<PressureLevel>, |
| |
| /// The free memory target to aim for when we trigger eviction. |
| free_mem_target: UnsafeCell<u64>, |
| |
| /// Current minimum amount of memory we want the triggered eviction to reclaim. |
| min_free_target: UnsafeCell<u64>, |
| |
| /// A timer is used to trigger eviction so that user space is given a chance to act upon a |
| /// memory pressure signal first. |
| #[pin] |
| eviction_trigger: UnsafeCell<Timer>, |
| |
| /// Tracks whether or not continuous eviction is presently happening or not. This is an atomic |
| /// so that it can be set by the `eviction_trigger` after the timeout occurs, and this |
| /// happens in a different thread. This value is only allowed to be set to true by the |
| /// `eviction_trigger` callback, and is only allowed to be set to false by the main |
| /// `worker_thread`, preventing any races in transitioning its state. Races in reading the |
| /// state are fine, and the correctness of these is justified at each usage site. |
| continuous_eviction_active: AtomicBool, |
| |
| /// OneShot eviction strategy only triggers eviction events at memory pressure state |
| /// transitions. Continuous eviction strategy enables continuous background eviction as long |
| /// the system remains under memory pressure (i.e. at a memory pressure level that is |
| /// eligible for eviction). |
| eviction_strategy: UnsafeCell<EvictionStrategy>, |
| |
| /// Record of the thread running `worker_thread` created during `init`. |
| worker_thread: UnsafeCell<Option<kernel::thread::ThreadPtr>>, |
| |
| executor: UnsafeCell<Option<NonNull<Executor>>>, |
| } |
| |
| /// Type alias for [`MemoryWatchdogState`]. |
| pub type MemoryWatchdog = MemoryWatchdogState; |
| |
| unsafe extern "C" { |
| #[allow(improper_ctypes)] |
| fn cpp_get_memory_watchdog() -> *const MemoryWatchdogState; |
| } |
| |
| /// Returns a reference to the global [`MemoryWatchdog`]. |
| #[inline] |
| pub fn get_memory_watchdog() -> &'static MemoryWatchdog { |
| // SAFETY: `cpp_get_memory_watchdog` returns a pointer to the static `gExecutor`'s |
| // `MemoryWatchdog`, which is layout-compatible with `MemoryWatchdogState` (verified by |
| // `static_assert` in `memory_watchdog.cc`) and valid for `'static`. |
| unsafe { &*cpp_get_memory_watchdog() } |
| } |
| |
| // SAFETY: `MemoryWatchdogState` fields are either initialized once in `init` before any concurrent |
| // access, accessed exclusively by the worker thread, or synchronized via atomics (`mem_event_idx`, |
| // `continuous_eviction_active`), `AutounsignalEvent`, and `Timer::cancel` ordering before target |
| // updates. |
| unsafe impl Sync for MemoryWatchdogState {} |
| // SAFETY: `MemoryWatchdogState` can be safely transferred across threads under the same invariants |
| // as `Sync`. |
| unsafe impl Send for MemoryWatchdogState {} |
| |
| impl MemoryWatchdogState { |
| /// Creates an in-place initializer for a new `MemoryWatchdogState`. |
| pub fn new() -> impl PinInit<Self, core::convert::Infallible> { |
| pin_init!(Self { |
| mem_pressure_events: UnsafeCell::new([None, None, None, None, None]), |
| mem_state_signal <- AutounsignalEvent::init(false), |
| mem_event_idx: RelaxedAtomicPressureLevel::new(PressureLevel::Normal), |
| prev_mem_event_idx: UnsafeCell::new(PressureLevel::Normal), |
| mem_watermarks: UnsafeCell::new([0; NUM_WATERMARKS]), |
| watermark_debounce: UnsafeCell::new(0), |
| hysteresis_seconds: UnsafeCell::new(DurationMono::from_seconds(10)), |
| eviction_delay_ms: UnsafeCell::new(DurationMono::from_millis(5000)), |
| prev_mem_state_eval_time: UnsafeCell::new(InstantMono::INFINITE_PAST), |
| max_eviction_level: UnsafeCell::new(PressureLevel::Critical), |
| free_mem_target: UnsafeCell::new(0), |
| min_free_target: UnsafeCell::new(0), |
| eviction_trigger <- UnsafeCell::pin_init(Timer::init(kernel::timer::ZX_CLOCK_MONOTONIC)), |
| continuous_eviction_active: AtomicBool::new(false), |
| eviction_strategy: UnsafeCell::new(EvictionStrategy::OneShot), |
| worker_thread: UnsafeCell::new(None), |
| executor: UnsafeCell::new(None), |
| }) |
| } |
| |
| fn is_imminent_oom_enabled(&self) -> bool { |
| // SAFETY: `mem_watermarks` is not modified after `init`. |
| unsafe { |
| (*self.mem_watermarks.get())[PressureLevel::ImminentOutOfMemory as usize] |
| != (*self.mem_watermarks.get())[PressureLevel::OutOfMemory as usize] |
| } |
| } |
| |
| fn free_mem_bounds_for_level(&self, level: PressureLevel) -> (u64, u64) { |
| // If ImminentOOM is disabled, we will never enter that level, and so we should never be |
| // asked to compute its bounds. |
| debug_assert!( |
| self.is_imminent_oom_enabled() || level != PressureLevel::ImminentOutOfMemory |
| ); |
| // Calculate the range, including debounce, for the current memory level. |
| let mut lower = 0; |
| let mut upper = u64::MAX; |
| // SAFETY: `mem_watermarks` and `watermark_debounce` are not modified after `init`. |
| unsafe { |
| if (level as usize) > (PressureLevel::OutOfMemory as usize) { |
| lower = (*self.mem_watermarks.get())[level as usize - 1] |
| .saturating_sub(*self.watermark_debounce.get()); |
| } |
| if (level as usize) < NUM_WATERMARKS { |
| upper = (*self.mem_watermarks.get())[level as usize] |
| .saturating_add(*self.watermark_debounce.get()); |
| } |
| } |
| (lower, upper) |
| } |
| |
| fn calculate_pressure_level(&self) -> PressureLevel { |
| // Get the bounds for the current level, this is inclusive of the debounce. |
| let current = self.mem_event_idx.load(); |
| let (lower, upper) = self.free_mem_bounds_for_level(current); |
| |
| // Retrieve current free memory. |
| // SAFETY: Queries PMM free page count and reads `mem_watermarks` (not modified after |
| // `init`). |
| let (free_mem, watermarks) = |
| unsafe { (pmm::node().count_free_pages() * PAGE_SIZE, &*self.mem_watermarks.get()) }; |
| |
| // Check if still inside the bounds. |
| if free_mem >= lower && free_mem <= upper { |
| // No change. |
| return current; |
| } |
| |
| // Determine the new level using a simple O(N). |
| let mut new_level = PressureLevel::OutOfMemory as usize; |
| while new_level < NUM_WATERMARKS && free_mem > watermarks[new_level] { |
| new_level += 1; |
| } |
| let new_level = PressureLevel::try_from(new_level as u8).unwrap(); |
| debug_assert_ne!(new_level, current); |
| // If ImminentOOM is disabled, we should never compute it as the current level. |
| debug_assert!( |
| self.is_imminent_oom_enabled() || new_level != PressureLevel::ImminentOutOfMemory |
| ); |
| new_level |
| } |
| |
| // Called by the `worker_thread` to determine if a kernel event needs to be signaled |
| // corresponding to pressure change to level `idx`. |
| #[inline] |
| fn is_signal_due(&self, idx: PressureLevel, time_now: InstantMono) -> bool { |
| // We signal a memory state change immediately if any of these conditions are met: |
| // 1) The current index is lower than the previous one signaled (i.e. available memory is |
| // lower now), so that clients can act on the signal quickly. |
| // 2) `hysteresis_seconds` have elapsed since the last time we examined the state. |
| // SAFETY: Called exclusively on `worker_thread`; `hysteresis_seconds` is constant after |
| // `init`. |
| unsafe { |
| idx < *self.prev_mem_event_idx.get() |
| || time_now - *self.prev_mem_state_eval_time.get() >= *self.hysteresis_seconds.get() |
| } |
| } |
| |
| // Called by the `worker_thread` to determine if kernel eviction (asynchronous) needs to be |
| // triggered in response to pressure change to level `idx`. |
| #[inline] |
| fn is_eviction_required(&self, idx: PressureLevel) -> bool { |
| // Trigger asynchronous eviction if: |
| // 1) the memory availability state is more critical than the previous one |
| // AND |
| // 2) we're configured to evict at that level. |
| // |
| // Do not trigger asynchronous eviction at the OOM level, as we have already performed |
| // synchronous eviction to attempt a quick recovery before reaching here. At this |
| // point we are about to signal filesystems to shut down on OOM, after which |
| // eviction will be a no-op anyway, since there will no longer be any pager-backed |
| // memory to evict. |
| // |
| //SAFETY: Called exclusively on `worker_thread`; |
| // `max_eviction_level` is constant after `init`. |
| unsafe { |
| idx < *self.prev_mem_event_idx.get() |
| && idx <= *self.max_eviction_level.get() |
| && idx != PressureLevel::OutOfMemory |
| } |
| } |
| |
| fn eviction_trigger(&self) { |
| // SAFETY: `eviction_strategy` and `free_mem_target` are constant after `init`. |
| // `min_free_target` is only modified by `worker_thread` after canceling `eviction_trigger`. |
| unsafe { |
| // This runs from a timer interrupt context, as such we do not want to be performing |
| // synchronous eviction and blocking some random thread. Therefore we use |
| // the asynchronous eviction trigger that will cause the eviction thread to |
| // perform the actual eviction work. |
| if *self.eviction_strategy.get() == EvictionStrategy::Continuous { |
| // Under a continuous eviction strategy we must set `continuous_eviction_active` to |
| // true so that the `worker_thread` knows it should bump the evictor |
| // when memory states change. |
| self.continuous_eviction_active.store(true, Ordering::SeqCst); |
| } |
| EVICTION_TRIGGERED.add(1); |
| pmm::node().evictor().evict_asynchronous( |
| *self.min_free_target.get(), |
| *self.free_mem_target.get(), |
| evictor::EvictionLevel::OnlyOldest, |
| evictor::Output::Print, |
| ); |
| } |
| } |
| |
| // Helper called by the memory pressure thread when OOM state is entered. |
| fn on_oom(&self) { |
| match BootOptions::get().oom_behavior { |
| boot_options::OomBehavior::JobKill => { |
| // SAFETY: `executor` is initialized in `init` before the worker thread starts. |
| let executor = unsafe { (*self.executor.get()).unwrap().as_ref() }; |
| if !executor.get_root_job_dispatcher().kill_job_with_kill_on_oom() { |
| kprintln!("memory-pressure: no alive job has a kill bit"); |
| } |
| |
| // Since killing is asynchronous, sleep for a short period for the system to |
| // quiesce. This prevents us from rapidly killing more jobs than |
| // necessary. And if we don't find a killable job, don't just spin |
| // since the next iteration probably won't find a one either. |
| let _ = kernel::thread::sleep_relative(DurationMono::from_millis(500)); |
| } |
| boot_options::OomBehavior::Reboot => { |
| handle_on_oom_reboot(); |
| } |
| } |
| } |
| |
| fn wait_for_mem_change(&self, deadline: &Deadline) { |
| let prev = self.mem_event_idx.load(); |
| // Coming into this method we must not be in the `OutOfMemory` state, as if this were |
| // possible allocations would be stalled and we would be waiting for a memory state |
| // change *before* triggering the evictor, which would cause a deadlock. |
| debug_assert_ne!(prev, PressureLevel::OutOfMemory); |
| let mut status = Ok(()); |
| // Count how many times in a row the setting of the free memory signal fails. This should |
| // only fail in the case of an unlikely race, and failing repeatedly could indicate |
| // a bug and also means the free_pages_evt_ in the pmm is not getting signaled. |
| let mut set_free_memory_failed_iterations: u32 = 0; |
| // This loop can iterate many times as it is woken up by both pmm state changes and, if |
| // continuous eviction is enabled, page queues state changes. |
| // SAFETY: `mem_watermarks`, `watermark_debounce`, and `free_mem_target` are not modified |
| // after `init`, and `mem_state_signal` is valid for the lifetime of `self`. |
| unsafe { |
| loop { |
| let cur_level = self.mem_event_idx.load(); |
| // We cannot enter this method in the `OutOfMemory` state and since we would exit |
| // this loop if the state were to change we can never be configuring |
| // the free memory signal in this state. |
| debug_assert_ne!(cur_level, PressureLevel::OutOfMemory); |
| let (lower, upper) = self.free_mem_bounds_for_level(cur_level); |
| let delay_alloc_level = ((*self.mem_watermarks.get()) |
| [PressureLevel::OutOfMemory as usize] |
| .saturating_sub(*self.watermark_debounce.get())) |
| / PAGE_SIZE; |
| if pmm::node().set_free_memory_signal( |
| lower / PAGE_SIZE, |
| upper / PAGE_SIZE, |
| delay_alloc_level, |
| self.mem_state_signal.as_raw().cast(), |
| ) { |
| // After having successfully set the event check again for any allocation |
| // failures. This is to ensure that if an allocation failure |
| // happened while we did not have an event set that it is |
| // not missed. |
| set_free_memory_failed_iterations = 0; |
| if BootOptions::get().oom_trigger_on_alloc_failure |
| && pmm::node().has_alloc_failed_no_mem() |
| { |
| return; |
| } |
| status = self.mem_state_signal.wait(deadline); |
| } else { |
| set_free_memory_failed_iterations += 1; |
| // Setting failed, must've raced. Fall through and compute the new pressure |
| // level. |
| if set_free_memory_failed_iterations > 5 |
| && set_free_memory_failed_iterations.is_power_of_two() |
| { |
| kprintln!( |
| "memory-pressure: WARNING pmm_set_free_memory_signal has failed \ |
| {set_free_memory_failed_iterations} times in a row" |
| ); |
| } |
| } |
| self.mem_event_idx.store(self.calculate_pressure_level()); |
| // If continuous eviction is currently active then let the evictor know that it may |
| // have some work to do. This is done by requesting an asynchronous |
| // eviction to the free memory target. As we are running in the |
| // context of the `worker_thread`, there is no race where active could |
| // transition from true->false, so we will not trigger eviction unnecessarily. |
| // Should there be a false->true race (due to the `eviction_trigger` |
| // callback running) then this is fine, since that callback will set |
| // the eviction target. See the documentation on |
| // EvictOneShotAsynchronous for how eviction requests combine, and why |
| // we can repeatedly perform this request correctly. |
| if self.continuous_eviction_active.load(Ordering::SeqCst) { |
| pmm::node().evictor().evict_asynchronous( |
| 0, |
| *self.free_mem_target.get(), |
| evictor::EvictionLevel::OnlyOldest, |
| evictor::Output::NoPrint, |
| ); |
| } |
| // In the case where we raced with additional pmm actions keep looping unless the |
| // deadline was reached. |
| if self.mem_event_idx.load() != prev || status.is_err() { |
| break; |
| } |
| } |
| } |
| } |
| |
| fn worker_thread(&self) -> ! { |
| // Tracks whether we have logged entering the OOM state. Used to rate limit logs when |
| // bouncing in and out of OOM rapidly. Hysteresis delays confirming the transition |
| // to a lower pressure state, so we can re-enter OOM multiple times before that |
| // happens. |
| let mut oom_entry_logged = false; |
| // Accumulates the total number of pages evicted during a contiguous OOM period (which may |
| // involve multiple bounces in and out of OOM level). |
| let mut total_oom_evicted_pages: u64 = 0; |
| |
| // SAFETY: `worker_thread` is the single thread that mutates `prev_mem_event_idx`, |
| // `prev_mem_state_eval_time`, `min_free_target`, and `eviction_trigger` (after `init`), |
| // and reads configuration fields that are immutable after `init`. |
| unsafe { |
| loop { |
| // If we've hit OOM level perform some immediate synchronous eviction to attempt to |
| // avoid OOM. |
| if self.mem_event_idx.load() == PressureLevel::OutOfMemory { |
| oom_ktrace_duration!(1, "MemoryWatchdog::OutOfMemory"); |
| |
| // Log only the first time we enter OOM in this period to avoid spam. |
| if !oom_entry_logged { |
| kprintln!( |
| "memory-pressure: beginning reclamation to avoid OOM. Allocations are \ |
| now disabled" |
| ); |
| oom_entry_logged = true; |
| } |
| |
| count_pressure_event(self.mem_event_idx.load()); |
| // Keep trying to perform eviction for as long as we are evicting non-zero pages |
| // and we remain in the out of memory state. |
| while self.mem_event_idx.load() == PressureLevel::OutOfMemory { |
| let evicted_pages = pmm::node() |
| .evictor() |
| .evict_synchronous( |
| MB * BootOptions::get().oom_eviction_delta_at_oom_mb, |
| 0, |
| evictor::EvictionLevel::IncludeNewest, |
| evictor::Output::NoPrint, |
| evictor::TriggerReason::OOM, |
| ) |
| .counts |
| .non_loaned_total(); |
| if evicted_pages == 0 { |
| kprintln!("memory-pressure: found no pages to evict"); |
| break; |
| } |
| // Accumulate total pages evicted across all bounces in this OOM period. |
| total_oom_evicted_pages = |
| total_oom_evicted_pages.saturating_add(evicted_pages); |
| self.mem_event_idx.store(self.calculate_pressure_level()); |
| } |
| } |
| |
| // Check to see if the PMM has failed any allocations. If the PMM has ever failed |
| // to allocate because it was out of memory, then escalate the |
| // pressure level to trigger an OOM response immediately. The idea |
| // here is that usermode processes may not be able to handle allocation |
| // failure and therefore could have become wedged in some way. |
| if BootOptions::get().oom_trigger_on_alloc_failure |
| && pmm::node().has_alloc_failed_no_mem() |
| { |
| let first_failure = pmm::node().get_first_alloc_failure(); |
| // This log message is load-bearing server-side as it's used to identify the |
| // culprit of the OOM. |
| // Please notify //src/developer/forensics/OWNERS upon changing. |
| kprintln!( |
| "memory-pressure: failed one or more allocations (first reported type: \ |
| {:s}, size: {}, free memory: {}MB), escalating to oom...", |
| first_failure.type_str(), |
| first_failure.size, |
| (first_failure.free_count * PAGE_SIZE) / MB |
| ); |
| self.mem_event_idx.store(PressureLevel::OutOfMemory); |
| } |
| |
| let time_now = current_mono_time(); |
| |
| if self.is_signal_due(self.mem_event_idx.load(), time_now) { |
| count_pressure_event(self.mem_event_idx.load()); |
| |
| // Log the total pages reclaimed during this OOM period (if any) and reset the |
| // OOM logging flag. |
| if oom_entry_logged { |
| if total_oom_evicted_pages > 0 { |
| let mut buf = [0u8; pretty::MAX_FORMAT_SIZE_LEN]; |
| kprintln!( |
| "memory-pressure: reclaimed {:s} to avoid OOM", |
| pretty::format_size_rs( |
| &mut buf, |
| (total_oom_evicted_pages * PAGE_SIZE) as usize |
| ) |
| ); |
| total_oom_evicted_pages = 0; |
| } |
| oom_entry_logged = false; |
| } |
| |
| kprintln!( |
| "memory-pressure: memory availability state - {:s}", |
| pressure_level_to_string(self.mem_event_idx.load()) |
| ); |
| |
| if self.is_eviction_required(self.mem_event_idx.load()) { |
| pmm::page_queues().dump(); |
| // Clear any previous eviction trigger. Once Cancel completes we know that |
| // we will not race with the callback and are free |
| // to update the targets. Cancel will return true if the |
| // timer was canceled before it was scheduled on a cpu, i.e. an eviction was |
| // outstanding. |
| let eviction_was_outstanding = |
| Pin::new_unchecked(&mut *self.eviction_trigger.get()).cancel(); |
| |
| if BootOptions::get().oom_evict_with_min_target { |
| let free_mem = pmm::node().count_free_pages() * PAGE_SIZE; |
| // Set the minimum amount to free as half the amount required to reach |
| // our desired free memory level. This |
| // minimum ensures that even if the user reduces memory in reaction to |
| // this signal we will always attempt to free a bit. |
| *self.min_free_target.get() = if free_mem < *self.free_mem_target.get() |
| { |
| (*self.free_mem_target.get() - free_mem) / 2 |
| } else { |
| 0 |
| }; |
| } else { |
| *self.min_free_target.get() = 0; |
| } |
| |
| // If eviction was outstanding when we canceled the eviction trigger, |
| // trigger eviction immediately without any delay. |
| // We are here because of a rapid allocation spike which |
| // caused the memory pressure to become more critical in a very short |
| // interval, so it might be better to evict pages as |
| // soon as possible to try and counter the allocation spike. |
| // Otherwise if eviction was not outstanding, trigger the eviction for |
| // slightly in the future. Half the hysteresis time |
| // here is a balance between giving user space time to |
| // release memory and the eviction running before the end of the hysteresis |
| // period. |
| if eviction_was_outstanding |
| || *self.eviction_delay_ms.get() == DurationMono::ZERO |
| { |
| self.eviction_trigger(); |
| } else { |
| Pin::new_unchecked(&mut *self.eviction_trigger.get()).set_oneshot( |
| (time_now + *self.eviction_delay_ms.get()).into_nanos(), |
| eviction_trigger_callback, |
| self as *const Self as *mut c_void, |
| ); |
| } |
| } else if *self.eviction_strategy.get() == EvictionStrategy::Continuous |
| && self.mem_event_idx.load() > *self.max_eviction_level.get() |
| { |
| // If we're out of the max configured eviction-eligible memory pressure |
| // level, disable continuous eviction. |
| |
| // Cancel any outstanding eviction trigger, so that eviction is not |
| // accidentally enabled *after* we disable it here. |
| Pin::new_unchecked(&mut *self.eviction_trigger.get()).cancel(); |
| self.continuous_eviction_active.store(false, Ordering::SeqCst); |
| } |
| |
| // Unsignal the last event that was signaled. |
| if let Err(status) = (*self.mem_pressure_events.get()) |
| [*self.prev_mem_event_idx.get() as usize] |
| .as_ref() |
| .unwrap() |
| .user_signal_self(zx_types::ZX_EVENT_SIGNALED, 0) |
| { |
| panic!( |
| "memory-pressure: unsignal memory event {} failed: {}\n", |
| pressure_level_to_string(*self.prev_mem_event_idx.get()), |
| status.into_raw() |
| ); |
| } |
| |
| // Signal event corresponding to the new memory state. |
| if let Err(status) = (*self.mem_pressure_events.get()) |
| [self.mem_event_idx.load() as usize] |
| .as_ref() |
| .unwrap() |
| .user_signal_self(0, zx_types::ZX_EVENT_SIGNALED) |
| { |
| panic!( |
| "memory-pressure: signal memory event {} failed: {}\n", |
| pressure_level_to_string(self.mem_event_idx.load()), |
| status.into_raw() |
| ); |
| } |
| *self.prev_mem_event_idx.get() = self.mem_event_idx.load(); |
| *self.prev_mem_state_eval_time.get() = time_now; |
| |
| // If we're below the out-of-memory watermark, trigger OOM behavior. |
| if self.mem_event_idx.load() == PressureLevel::OutOfMemory { |
| pmm::page_queues().dump(); |
| self.on_oom(); |
| } |
| |
| // Wait for the memory state to change again. |
| self.wait_for_mem_change(&Deadline::infinite()); |
| } else { |
| *self.prev_mem_state_eval_time.get() = time_now; |
| |
| // We are ignoring this memory state transition. Wait for only |
| // `hysteresis_seconds`, and then re-evaluate the memory |
| // state. Otherwise we could remain stuck at the lower memory state if |
| // `mem_state_signal` is not signaled. |
| self.wait_for_mem_change(&Deadline::no_slack(InstantUnknown::from( |
| time_now + *self.hysteresis_seconds.get(), |
| ))); |
| } |
| } |
| } |
| } |
| |
| /// `init` must be called before any other methods. |
| /// |
| /// # Safety |
| /// |
| /// The caller must guarantee that `executor` is safe to dereference. |
| pub unsafe fn init(&self, executor: NonNull<Executor>) { |
| // SAFETY: `init` is called once during boot before any other methods or threads access |
| // `self`. |
| unsafe { |
| debug_assert!((*self.executor.get()).is_none()); |
| |
| *self.executor.get() = Some(executor); |
| |
| let events = &mut *self.mem_pressure_events.get(); |
| for (i, event) in events.iter_mut().enumerate().take(NUM_LEVELS) { |
| let level = PressureLevel::try_from(i as u8).unwrap(); |
| let handle = match EventDispatcher::create(0) { |
| Ok((h, _rights)) => h, |
| Err(status) => { |
| panic!( |
| "memory-pressure: create memory event {} failed: {}\n", |
| pressure_level_to_string(level), |
| status.into_raw() |
| ); |
| } |
| }; |
| *event = Some(handle.release()); |
| } |
| |
| let boot_options = BootOptions::get(); |
| if boot_options.oom_enabled { |
| { |
| let watermarks = &mut *self.mem_watermarks.get(); |
| // TODO(rashaeqbal): The watermarks chosen below are arbitrary. Tune them based |
| // on memory usage patterns. Consider moving to percentages |
| // of total memory instead of absolute numbers - will |
| // be easier to maintain across platforms. |
| watermarks[PressureLevel::OutOfMemory as usize] = |
| boot_options.oom_out_of_memory_threshold_mb * MB; |
| watermarks[PressureLevel::ImminentOutOfMemory as usize] = watermarks |
| [PressureLevel::OutOfMemory as usize] |
| + boot_options.oom_imminent_oom_delta_mb * MB; |
| watermarks[PressureLevel::Critical as usize] = |
| boot_options.oom_critical_threshold_mb * MB; |
| watermarks[PressureLevel::Warning as usize] = |
| boot_options.oom_warning_threshold_mb * MB; |
| } |
| let watermarks = &*self.mem_watermarks.get(); |
| |
| *self.watermark_debounce.get() = boot_options.oom_debounce_mb * MB; |
| if boot_options.oom_evict_at_warning { |
| *self.max_eviction_level.get() = PressureLevel::Warning; |
| } |
| |
| // Validate our watermarks and debounce settings makes sense. |
| for j in 0..NUM_WATERMARKS { |
| let prev = if j == 0 { 0 } else { watermarks[j - 1] }; |
| let next = if j == NUM_WATERMARKS - 1 { u64::MAX } else { watermarks[j + 1] }; |
| let curr = watermarks[j]; |
| // The watermarks should be in increasing order, with a minimum of |
| // `watermark_debounce` difference between consecutive |
| // levels. The only exception is if the ImminentOOM level is |
| // disabled (by setting oom_imminent_oom_delta_mb to 0), in which case the OOM |
| // and ImminentOOM watermarks will be the same, and the |
| // ImminentOOM level will never be entered; we will |
| // either be in OOM or Critical. |
| assert!( |
| curr > prev |
| || (j == PressureLevel::ImminentOutOfMemory as usize && curr == prev) |
| ); |
| assert!( |
| curr < next || (j == PressureLevel::OutOfMemory as usize && curr == next) |
| ); |
| assert!( |
| curr.saturating_sub(prev) > *self.watermark_debounce.get() |
| || (j == PressureLevel::ImminentOutOfMemory as usize && curr == prev) |
| ); |
| assert!( |
| next.saturating_sub(curr) > *self.watermark_debounce.get() |
| || (j == PressureLevel::OutOfMemory as usize && curr == next) |
| ); |
| } |
| |
| // Set our eviction target to be such that we try to get completely out of the max |
| // eviction level, taking into account the debounce. |
| *self.free_mem_target.get() = watermarks[*self.max_eviction_level.get() as usize] |
| + *self.watermark_debounce.get(); |
| |
| *self.hysteresis_seconds.get() = |
| DurationMono::from_seconds(boot_options.oom_hysteresis_seconds as i64); |
| *self.eviction_delay_ms.get() = |
| DurationMono::from_millis(boot_options.oom_eviction_delay_ms as i64); |
| |
| kprintln!( |
| "memory-pressure: memory watermarks - OutOfMemory: {}MB, Critical: {}MB, \ |
| Warning: {}MB, Debounce: {}MB", |
| watermarks[PressureLevel::OutOfMemory as usize] / MB, |
| watermarks[PressureLevel::Critical as usize] / MB, |
| watermarks[PressureLevel::Warning as usize] / MB, |
| *self.watermark_debounce.get() / MB |
| ); |
| |
| kprintln!( |
| "memory-pressure: hysteresis interval - {} seconds", |
| (*self.hysteresis_seconds.get()).into_seconds() |
| ); |
| |
| if boot_options.oom_evict_continuous { |
| *self.eviction_strategy.get() = EvictionStrategy::Continuous; |
| pmm::page_queues() |
| .set_aging_event(Some(NonNull::from(self.mem_state_signal.as_event()))); |
| } else { |
| *self.eviction_strategy.get() = EvictionStrategy::OneShot; |
| } |
| |
| kprintln!( |
| "memory-pressure: eviction: level - {:s}, strategy - {:s}, delay - {} ms", |
| pressure_level_to_string(*self.max_eviction_level.get()), |
| if boot_options.oom_evict_continuous { "continuous" } else { "one-shot" }, |
| (*self.eviction_delay_ms.get()).into_millis() |
| ); |
| |
| if self.is_imminent_oom_enabled() { |
| kprintln!( |
| "memory-pressure: ImminentOutOfMemory watermark - {}MB", |
| watermarks[PressureLevel::ImminentOutOfMemory as usize] / MB |
| ); |
| } |
| |
| extern "C" fn memory_worker_thread(arg: *mut c_void) -> i32 { |
| // SAFETY: `arg` is a valid pointer to `MemoryWatchdogState` passed to |
| // `create_with_priority`. |
| let watchdog = unsafe { &*arg.cast::<MemoryWatchdogState>() }; |
| watchdog.worker_thread() |
| } |
| |
| *self.worker_thread.get() = kernel::thread::create_with_priority( |
| c"memory-pressure-thread".as_ptr(), |
| memory_worker_thread, |
| self as *const Self as *mut c_void, |
| kernel::scheduler_state::HIGHEST_PRIORITY, |
| ) |
| .ok(); |
| debug_assert!((*self.worker_thread.get()).is_some()); |
| (*self.worker_thread.get()).unwrap().resume(); |
| } |
| } |
| } |
| |
| /// Returns the memory pressure event dispatcher corresponding to `kind`. |
| pub fn get_mem_pressure_event(&self, kind: u32) -> Option<fbl::RefPtr<EventDispatcher>> { |
| // SAFETY: `mem_pressure_events` is initialized in `init` and not modified thereafter. |
| let events = unsafe { &*self.mem_pressure_events.get() }; |
| match kind { |
| zx_types::ZX_SYSTEM_EVENT_OUT_OF_MEMORY => { |
| events[PressureLevel::OutOfMemory as usize].clone() |
| } |
| zx_types::ZX_SYSTEM_EVENT_IMMINENT_OUT_OF_MEMORY => { |
| events[PressureLevel::ImminentOutOfMemory as usize].clone() |
| } |
| zx_types::ZX_SYSTEM_EVENT_MEMORY_PRESSURE_CRITICAL => { |
| events[PressureLevel::Critical as usize].clone() |
| } |
| zx_types::ZX_SYSTEM_EVENT_MEMORY_PRESSURE_WARNING => { |
| events[PressureLevel::Warning as usize].clone() |
| } |
| zx_types::ZX_SYSTEM_EVENT_MEMORY_PRESSURE_NORMAL => { |
| events[PressureLevel::Normal as usize].clone() |
| } |
| _ => None, |
| } |
| } |
| |
| /// Returns the number of bytes of memory that must be allocated to reach `level`. |
| pub fn debug_num_bytes_till_pressure_level(&self, level: PressureLevel) -> u64 { |
| // SAFETY: `executor`, `mem_watermarks`, and `watermark_debounce` are not modified after |
| // `init`. |
| unsafe { |
| // Check we have been initialized. |
| debug_assert!((*self.executor.get()).is_some()); |
| if self.mem_event_idx.load() <= level { |
| // Already in level, or in a state with less available memory than level |
| return 0; |
| } |
| // We need to either get free_pages below mem_watermarks[level] or, if we are |
| // in state (level + 1), we also need to clear the debounce amount. For simplicity we |
| // just always allocate the debounce amount as well. |
| let trigger = (*self.mem_watermarks.get())[level as usize] |
| .saturating_sub(*self.watermark_debounce.get()); |
| let free_count = pmm::node().count_free_pages() * PAGE_SIZE; |
| // Handle races in the current pressure level. |
| if free_count < trigger { |
| return 0; |
| } |
| free_count - trigger |
| } |
| } |
| |
| /// Dumps the current memory watchdog state to the kernel log. |
| pub fn dump(&self) { |
| // SAFETY: `mem_watermarks` and `watermark_debounce` are not modified after `init`. |
| unsafe { |
| let mut buf1 = [0u8; pretty::MAX_FORMAT_SIZE_LEN]; |
| let mut buf2 = [0u8; pretty::MAX_FORMAT_SIZE_LEN]; |
| kprint!("watermarks: ["); |
| for i in 0..NUM_WATERMARKS { |
| let level = PressureLevel::try_from(i as u8).unwrap(); |
| kprint!( |
| "{:s}: {:s}{:s}", |
| pressure_level_to_string(level), |
| pretty::format_size_rs(&mut buf1, (*self.mem_watermarks.get())[i] as usize), |
| if i + 1 == NUM_WATERMARKS { "]\n" } else { ", " } |
| ); |
| } |
| let current = self.mem_event_idx.load(); |
| let (lower, upper) = self.free_mem_bounds_for_level(current); |
| kprintln!( |
| "debounce: {:s}", |
| pretty::format_size_rs(&mut buf1, *self.watermark_debounce.get() as usize) |
| ); |
| kprintln!("current state: {} [{:s}]", current as u8, pressure_level_to_string(current)); |
| kprintln!( |
| "current bounds: [{:s}, {:s}]", |
| pretty::format_size_rs(&mut buf1, lower as usize), |
| pretty::format_size_rs(&mut buf2, upper as usize) |
| ); |
| kprintln!( |
| "free memory: {:s}", |
| pretty::format_size_rs( |
| &mut buf1, |
| (pmm::node().count_free_pages() * PAGE_SIZE) as usize |
| ) |
| ); |
| let stats = StallAggregator::get().read_stats(); |
| kprintln!( |
| "memory stall time: some {}, full {}", |
| stats.stalled_time_some.into_nanos(), |
| stats.stalled_time_full.into_nanos() |
| ); |
| } |
| } |
| |
| /// Debug method to retrieve any current worker thread. Only to be used for testing / debugging |
| /// purposes. It is up to the caller to know if this objects is alive or not. |
| pub fn debug_get_worker_thread(&self) -> Option<kernel::thread::ThreadPtr> { |
| // SAFETY: `worker_thread` is written once in `init` before any debug callers run. |
| unsafe { *self.worker_thread.get() } |
| } |
| } |
| |
| /// Initializes a `MemoryWatchdogState` in-place. |
| /// |
| /// # Safety |
| /// |
| /// `storage` must point to valid, properly aligned, uninitialized memory for `MemoryWatchdogState`. |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_memory_watchdog_construct( |
| storage: *mut MaybeUninit<MemoryWatchdogState>, |
| ) { |
| // SAFETY: `storage` is aligned and sized for `MemoryWatchdogState`. |
| let _ = unsafe { storage.as_mut_unchecked().write_pin_init(MemoryWatchdogState::new()) }; |
| } |
| |
| /// Drops a `MemoryWatchdogState` in-place. |
| /// |
| /// # Safety |
| /// |
| /// `storage` must point to a valid, initialized `MemoryWatchdogState`. |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_memory_watchdog_destroy(storage: *mut MemoryWatchdogState) { |
| // SAFETY: `storage` points to an initialized `MemoryWatchdogState`. |
| unsafe { |
| core::ptr::drop_in_place(storage); |
| } |
| } |
| |
| /// Retrieves the memory pressure event for `kind`. |
| /// |
| /// # Safety |
| /// |
| /// `storage` must point to a valid, initialized `MemoryWatchdogState`, and `out_event` must point |
| /// to valid uninitialized memory for `Option<fbl::RefPtr<EventDispatcher>>`. |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_memory_watchdog_get_mem_pressure_event( |
| storage: *const MemoryWatchdogState, |
| kind: u32, |
| out_event: *mut MaybeUninit<Option<fbl::RefPtr<EventDispatcher>>>, |
| ) { |
| // SAFETY: `storage` and `out_event` are valid pointers per the caller contract. |
| unsafe { |
| let state = &*storage; |
| (*out_event).write(state.get_mem_pressure_event(kind)); |
| } |
| } |
| |
| /// Returns the number of bytes of memory to allocate to reach `level`. |
| /// |
| /// # Safety |
| /// |
| /// `storage` must point to a valid, initialized `MemoryWatchdogState`. |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_memory_watchdog_debug_num_bytes_till_pressure_level( |
| storage: *const MemoryWatchdogState, |
| level: u8, |
| ) -> u64 { |
| // SAFETY: `storage` points to a valid, initialized `MemoryWatchdogState`. |
| let state = unsafe { &*storage }; |
| let Ok(level) = PressureLevel::try_from(level) else { |
| return 0; |
| }; |
| state.debug_num_bytes_till_pressure_level(level) |
| } |
| |
| /// Dumps the current memory watchdog state to the kernel log. |
| /// |
| /// # Safety |
| /// |
| /// `storage` must point to a valid, initialized `MemoryWatchdogState`. |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_memory_watchdog_dump(storage: *const MemoryWatchdogState) { |
| // SAFETY: `storage` points to a valid, initialized `MemoryWatchdogState`. |
| let state = unsafe { &*storage }; |
| state.dump(); |
| } |