| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::collections::HashMap; |
| use std::convert::TryFrom; |
| use std::path::Path; |
| use std::path::PathBuf; |
| use std::sync::atomic::AtomicI32; |
| use std::sync::atomic::AtomicUsize; |
| use std::sync::atomic::Ordering; |
| use std::sync::Arc; |
| use std::time::Duration; |
| use std::time::Instant; |
| |
| use anyhow::bail; |
| use anyhow::Context; |
| use anyhow::Result; |
| use dbus::channel::MatchingReceiver; |
| use dbus::channel::Sender; |
| use dbus::message::MatchRule; |
| use dbus::message::Message; |
| use dbus::nonblock::Proxy; |
| use dbus::nonblock::SyncConnection; |
| use dbus_crossroads::Crossroads; |
| use dbus_crossroads::IfaceBuilder; |
| use dbus_crossroads::IfaceToken; |
| use dbus_crossroads::MethodErr; |
| use dbus_tokio::connection; |
| use log::error; |
| use log::info; |
| use log::LevelFilter; |
| use system_api::battery_saver::BatterySaverModeState; |
| use tokio::sync::Mutex; |
| use tokio::sync::Notify; |
| |
| use crate::arch; |
| use crate::common; |
| use crate::common::read_from_file; |
| use crate::config::ConfigProvider; |
| use crate::feature; |
| use crate::feature::is_feature_enabled; |
| use crate::feature::register_feature; |
| use crate::memory; |
| use crate::memory::MemInfo; |
| use crate::memory::PsiMemoryHandler; |
| use crate::memory::PsiPolicyResult; |
| use crate::memory::MAX_PSI_ERROR_TYPE; |
| use crate::memory::UMA_NAME_PSI_POLICY_ERROR; |
| use crate::metrics; |
| use crate::power; |
| use crate::proc::load_euid; |
| use crate::process_stats; |
| use crate::psi; |
| use crate::qos; |
| use crate::qos::set_process_state; |
| use crate::qos::set_thread_state; |
| use crate::qos::SchedQosContext; |
| use crate::vm_memory_management_client::VmMemoryManagementClient; |
| |
| const SERVICE_NAME: &str = "org.chromium.ResourceManager"; |
| const PATH_NAME: &str = "/org/chromium/ResourceManager"; |
| const INTERFACE_NAME: &str = SERVICE_NAME; |
| |
| const VMCONCIEGE_INTERFACE_NAME: &str = "org.chromium.VmConcierge"; |
| const POWERD_INTERFACE_NAME: &str = "org.chromium.PowerManager"; |
| const POWERD_PATH_NAME: &str = "/org/chromium/PowerManager"; |
| |
| pub const DEFAULT_DBUS_TIMEOUT: Duration = Duration::from_secs(5); |
| |
| // The timeout in second for VM boot mode. Currently this is |
| // 60seconds which is long enough for booting a VM on low-end DUTs. |
| const DEFAULT_VM_BOOT_TIMEOUT: Duration = Duration::from_secs(60); |
| |
| type PowerPreferencesManager = |
| power::DirectoryPowerPreferencesManager<power::DirectoryPowerSourceProvider>; |
| |
| // Context data for the D-Bus service. |
| #[derive(Clone)] |
| struct DbusContext { |
| power_preferences_manager: Arc<PowerPreferencesManager>, |
| |
| // Timer ids for skipping out-of-dated timer events. |
| reset_game_mode_timer_id: Arc<AtomicUsize>, |
| reset_fullscreen_video_timer_id: Arc<AtomicUsize>, |
| reset_vm_boot_mode_timer_id: Arc<AtomicUsize>, |
| |
| scheduler_context: Option<Arc<Mutex<SchedQosContext>>>, |
| } |
| |
| fn send_pressure_signal( |
| conn: &SyncConnection, |
| signal_name: &str, |
| level: u8, |
| reclaim_target_kb: u64, |
| ) { |
| let signal_origin_timestamp_ms = match get_monotonic_timestamp_ms() { |
| Ok(timestamp) => timestamp, |
| Err(_) => { |
| error!("Failed to get current time."); |
| -1 |
| } |
| }; |
| |
| let msg = Message::signal( |
| &PATH_NAME.into(), |
| &INTERFACE_NAME.into(), |
| &signal_name.into(), |
| ) |
| // Since the origin timestamp originates from CLOCK_MONOTONIC, |
| // it should not be used from any guest context. It can only be compared within the host. |
| .append3(level, reclaim_target_kb, signal_origin_timestamp_ms); |
| if conn.send(msg).is_err() { |
| error!("Send Chrome pressure signal failed."); |
| } |
| } |
| |
| // Call swap_management SwapSetSwappiness when set_game_mode returns TuneSwappiness. |
| fn set_game_mode_and_tune_swappiness( |
| power_preferences_manager: &dyn power::PowerPreferencesManager, |
| mode: common::GameMode, |
| conn: Arc<SyncConnection>, |
| ) -> Result<()> { |
| if let Some(common::TuneSwappiness { swappiness }) = |
| common::set_game_mode(power_preferences_manager, mode, PathBuf::from("/")) |
| { |
| const SWAPPINESS_PATH: &str = "/proc/sys/vm/swappiness"; |
| if swappiness != read_from_file(&SWAPPINESS_PATH)? { |
| tokio::spawn(async move { |
| let swap_management_proxy = Proxy::new( |
| "org.chromium.SwapManagement", |
| "/org/chromium/SwapManagement", |
| DEFAULT_DBUS_TIMEOUT, |
| conn, |
| ); |
| match swap_management_proxy |
| .method_call( |
| "org.chromium.SwapManagement", |
| "SwapSetSwappiness", |
| (swappiness,), |
| ) |
| .await |
| { |
| Ok(()) => (), // For type inference. |
| Err(e) => error!("Calling SwapSetSwappiness failed: {:#}", e), |
| } |
| }); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn get_sender_euid(conn: Arc<SyncConnection>, bus_name: Option<String>) -> Result<u32> { |
| let Some(bus_name) = bus_name else { |
| bail!("sender bus name is empty"); |
| }; |
| |
| let (sender_pid,): (u32,) = Proxy::new( |
| "org.freedesktop.DBus", |
| "/org/freedesktop/DBus", |
| DEFAULT_DBUS_TIMEOUT, |
| conn, |
| ) |
| .method_call( |
| "org.freedesktop.DBus", |
| "GetConnectionUnixProcessID", |
| (bus_name,), |
| ) |
| .await |
| .context("get sender pid")?; |
| |
| load_euid(sender_pid).context("load euid") |
| } |
| |
| fn register_interface(cr: &mut Crossroads, conn: Arc<SyncConnection>) -> IfaceToken<DbusContext> { |
| cr.register(INTERFACE_NAME, |b: &mut IfaceBuilder<DbusContext>| { |
| b.method( |
| "GetAvailableMemoryKB", |
| (), |
| ("available",), |
| move |_, _, ()| { |
| let game_mode = common::get_game_mode(); |
| match MemInfo::load() { |
| Ok(meminfo) => Ok((memory::get_background_available_memory_kb( |
| &meminfo, game_mode, |
| ),)), |
| Err(_) => Err(MethodErr::failed("Couldn't get available memory")), |
| } |
| }, |
| ); |
| b.method( |
| "GetForegroundAvailableMemoryKB", |
| (), |
| ("available",), |
| move |_, _, ()| match MemInfo::load() { |
| Ok(meminfo) => Ok((memory::get_foreground_available_memory_kb(&meminfo),)), |
| Err(_) => Err(MethodErr::failed( |
| "Couldn't get foreground available memory", |
| )), |
| }, |
| ); |
| b.method( |
| "GetMemoryMarginsKB", |
| (), |
| ("critical", "moderate"), |
| move |_, _, ()| { |
| let margins = memory::get_memory_margins_kb(); |
| Ok((margins.0, margins.1)) |
| }, |
| ); |
| b.method( |
| "GetComponentMemoryMarginsKB", |
| (), |
| ("margins",), |
| move |_, _, ()| { |
| let margins = memory::get_component_margins_kb(); |
| let result = HashMap::from([ |
| ("ChromeCritical", margins.chrome_critical), |
| ("ChromeModerate", margins.chrome_moderate), |
| ("ArcvmForeground", margins.arcvm.foreground), |
| ("ArcvmPerceptible", margins.arcvm.perceptible), |
| ("ArcvmCached", margins.arcvm.cached), |
| ("ArcContainerForeground", margins.arc_container.foreground), |
| ("ArcContainerPerceptible", margins.arc_container.perceptible), |
| ("ArcContainerCached", margins.arc_container.cached), |
| ]); |
| Ok((result,)) |
| }, |
| ); |
| b.method( |
| "SetMemoryMarginsBps", |
| ("critical_bps", "moderate_bps"), |
| ("critical", "moderate"), |
| move |_, _, (critical_bps, moderate_bps): (u32, u32)| { |
| memory::set_memory_margins_bps(critical_bps, moderate_bps); |
| let margins = memory::get_memory_margins_kb(); |
| Ok((margins.0, margins.1)) |
| }, |
| ); |
| b.method("GetGameMode", (), ("game_mode",), move |_, _, ()| { |
| Ok((common::get_game_mode() as u8,)) |
| }); |
| let conn_clone = conn.clone(); |
| b.method( |
| "SetGameMode", |
| ("game_mode",), |
| (), |
| move |_, context, (mode_raw,): (u8,)| { |
| let mode = common::GameMode::try_from(mode_raw) |
| .map_err(|_| MethodErr::failed("Unsupported game mode value"))?; |
| |
| set_game_mode_and_tune_swappiness( |
| context.power_preferences_manager.as_ref(), |
| mode, |
| conn_clone.clone(), |
| ) |
| .map_err(|e| { |
| error!("set_game_mode failed: {:#}", e); |
| |
| MethodErr::failed("Failed to set game mode") |
| })?; |
| context |
| .reset_game_mode_timer_id |
| .fetch_add(1, Ordering::Relaxed); |
| Ok(()) |
| }, |
| ); |
| let conn_clone2 = conn.clone(); |
| b.method( |
| "SetGameModeWithTimeout", |
| ("game_mode", "timeout_sec"), |
| ("origin_game_mode",), |
| move |_, context, (mode_raw, timeout_raw): (u8, u32)| { |
| let old_game_mode = common::get_game_mode(); |
| let mode = common::GameMode::try_from(mode_raw) |
| .map_err(|_| MethodErr::failed("Unsupported game mode value"))?; |
| let timeout = Duration::from_secs(timeout_raw.into()); |
| set_game_mode_and_tune_swappiness( |
| context.power_preferences_manager.as_ref(), |
| mode, |
| conn_clone2.clone(), |
| ) |
| .map_err(|e| { |
| error!("set_game_mode failed: {:#}", e); |
| |
| MethodErr::failed("Failed to set game mode") |
| })?; |
| |
| // Increase timer id to cancel previous timer events. |
| context |
| .reset_game_mode_timer_id |
| .fetch_add(1, Ordering::Relaxed); |
| let timer_id = context.reset_game_mode_timer_id.load(Ordering::Relaxed); |
| let power_preferences_manager = context.power_preferences_manager.clone(); |
| let reset_game_mode_timer_id = context.reset_game_mode_timer_id.clone(); |
| |
| // Reset game mode after timeout. |
| let conn_clone = conn_clone2.clone(); |
| tokio::spawn(async move { |
| tokio::time::sleep(timeout).await; |
| // If the timer id is changed, this event is canceled. |
| if timer_id == reset_game_mode_timer_id.load(Ordering::Relaxed) |
| && set_game_mode_and_tune_swappiness( |
| power_preferences_manager.as_ref(), |
| common::GameMode::Off, |
| conn_clone, |
| ) |
| .is_err() |
| { |
| error!("Reset game mode failed."); |
| } |
| }); |
| |
| Ok((old_game_mode as u8,)) |
| }, |
| ); |
| b.method("GetRTCAudioActive", (), ("mode",), move |_, _, ()| { |
| Ok((common::get_rtc_audio_active() as u8,)) |
| }); |
| b.method( |
| "SetRTCAudioActive", |
| ("mode",), |
| (), |
| move |_, context, (active_raw,): (u8,)| { |
| let active = common::RTCAudioActive::try_from(active_raw) |
| .map_err(|_| MethodErr::failed("Unsupported RTC audio active value"))?; |
| match common::set_rtc_audio_active( |
| context.power_preferences_manager.as_ref(), |
| active, |
| ) { |
| Ok(()) => Ok(()), |
| Err(e) => { |
| error!("set_rtc_audio_active failed: {:#}", e); |
| Err(MethodErr::failed("Failed to set RTC audio activity")) |
| } |
| } |
| }, |
| ); |
| b.method("GetFullscreenVideo", (), ("mode",), move |_, _, ()| { |
| Ok((common::get_fullscreen_video() as u8,)) |
| }); |
| b.method( |
| "SetFullscreenVideoWithTimeout", |
| ("mode", "timeout_sec"), |
| (), |
| |_, context, (mode_raw, timeout_raw): (u8, u32)| { |
| let mode = common::FullscreenVideo::try_from(mode_raw) |
| .map_err(|_| MethodErr::failed("Unsupported fullscreen video value"))?; |
| let timeout = Duration::from_secs(timeout_raw.into()); |
| |
| common::set_fullscreen_video(context.power_preferences_manager.as_ref(), mode) |
| .map_err(|e| { |
| error!("set_fullscreen_video failed: {:#}", e); |
| |
| MethodErr::failed("Failed to set full screen video mode") |
| })?; |
| |
| context |
| .reset_fullscreen_video_timer_id |
| .fetch_add(1, Ordering::Relaxed); |
| let timer_id = context |
| .reset_fullscreen_video_timer_id |
| .load(Ordering::Relaxed); |
| let power_preferences_manager = context.power_preferences_manager.clone(); |
| let reset_fullscreen_video_timer_id = |
| context.reset_fullscreen_video_timer_id.clone(); |
| tokio::spawn(async move { |
| tokio::time::sleep(timeout).await; |
| if timer_id == reset_fullscreen_video_timer_id.load(Ordering::Relaxed) |
| && common::set_fullscreen_video( |
| power_preferences_manager.as_ref(), |
| common::FullscreenVideo::Inactive, |
| ) |
| .is_err() |
| { |
| error!("Reset fullscreen video mode failed."); |
| } |
| }); |
| |
| Ok(()) |
| }, |
| ); |
| b.method("PowerSupplyChange", (), (), move |_, context, ()| { |
| match common::update_power_preferences(context.power_preferences_manager.as_ref()) { |
| Ok(()) => Ok(()), |
| Err(e) => { |
| error!("update_power_preferences failed: {:#}", e); |
| Err(MethodErr::failed("Failed to update power preferences")) |
| } |
| } |
| }); |
| b.method( |
| "SetLogLevel", |
| ("level",), |
| (), |
| move |_, _, (level_raw,): (u8,)| { |
| let level = match level_raw { |
| 0 => LevelFilter::Off, |
| 1 => LevelFilter::Error, |
| 2 => LevelFilter::Warn, |
| 3 => LevelFilter::Info, |
| 4 => LevelFilter::Debug, |
| 5 => LevelFilter::Trace, |
| _ => return Err(MethodErr::failed("Unsupported log level value")), |
| }; |
| log::set_max_level(level); |
| Ok(()) |
| }, |
| ); |
| let conn_clone = conn.clone(); |
| b.method_with_cr_async( |
| "SetProcessState", |
| ("ProcessId", "ProcessState"), |
| (), |
| move |mut sender_context, cr, (process_id, process_state): (u32, u8)| { |
| let context: Option<&mut DbusContext> = cr.data_mut(sender_context.path()); |
| let sched_ctx = context.and_then(|ctx| ctx.scheduler_context.clone()); |
| let sender_bus_name = sender_context.message().sender().map(|s| s.to_string()); |
| let sender_euid = get_sender_euid(conn_clone.clone(), sender_bus_name); |
| async move { |
| let Some(sched_ctx) = sched_ctx else { |
| return sender_context.reply(Err(MethodErr::failed("no schedqos context"))); |
| }; |
| |
| let sender_euid = match sender_euid.await { |
| Ok(euid) => euid, |
| Err(e) => { |
| error!("failed to get sender euid: {:#}", e); |
| return sender_context |
| .reply(Err(MethodErr::failed("failed to get sender info"))); |
| } |
| }; |
| |
| match set_process_state(sched_ctx, process_id, process_state, sender_euid).await |
| { |
| Ok(_) => sender_context.reply(Ok(())), |
| Err(e) => { |
| error!("change_process_state failed: {:#}, pid={}", e, process_id); |
| sender_context.reply(Err(e.to_dbus_error())) |
| } |
| } |
| } |
| }, |
| ); |
| let conn_clone = conn.clone(); |
| b.method_with_cr_async( |
| "SetThreadState", |
| ("ProcessId", "ThreadId", "ThreadState"), |
| (), |
| move |mut sender_context, cr, (process_id, thread_id, thread_state): (u32, u32, u8)| { |
| let context: Option<&mut DbusContext> = cr.data_mut(sender_context.path()); |
| let sched_ctx = context.and_then(|ctx| ctx.scheduler_context.clone()); |
| let sender_bus_name = sender_context.message().sender().map(|s| s.to_string()); |
| let sender_euid = get_sender_euid(conn_clone.clone(), sender_bus_name); |
| async move { |
| let Some(sched_ctx) = sched_ctx else { |
| return sender_context.reply(Err(MethodErr::failed("no schedqos context"))); |
| }; |
| |
| let sender_euid = match sender_euid.await { |
| Ok(euid) => euid, |
| Err(e) => { |
| error!("failed to get sender euid: {:#}", e); |
| return sender_context |
| .reply(Err(MethodErr::failed("failed to get sender info"))); |
| } |
| }; |
| |
| match set_thread_state( |
| sched_ctx, |
| process_id, |
| thread_id, |
| thread_state, |
| sender_euid, |
| ) |
| .await |
| { |
| Ok(_) => sender_context.reply(Ok(())), |
| Err(e) => { |
| error!("change_thread_state failed: {:#}, pid={}", e, process_id); |
| sender_context.reply(Err(e.to_dbus_error())) |
| } |
| } |
| } |
| }, |
| ); |
| b.method( |
| "ReportBrowserProcesses", |
| ("raw_bytes",), |
| (), |
| move |_, _, (raw_bytes,): (Vec<u8>,)| { |
| use system_api::resource_manager::BrowserType; |
| |
| let report_browser_processes: system_api::resource_manager::ReportBrowserProcesses = |
| match protobuf::Message::parse_from_bytes(&raw_bytes) { |
| Ok(result) => result, |
| Err(e) => { |
| error!("Failed to parse ReportBrowserProcesses protobuf: {:#}", e); |
| return Err(MethodErr::failed( |
| "Failed to parse ReportBrowserProcesses protobuf", |
| )); |
| } |
| }; |
| let browser_type: memory::BrowserType = |
| match report_browser_processes.browser_type.enum_value() { |
| Ok(BrowserType::ASH) => memory::BrowserType::Ash, |
| Ok(BrowserType::LACROS) => memory::BrowserType::Lacros, |
| Err(enum_raw) => { |
| error!( |
| "ReportBrowserProcesses browser type is unknown, enum_raw: {}", |
| enum_raw |
| ); |
| return Err(MethodErr::failed( |
| "ReportBrowserProcesses browser type is unknown", |
| )); |
| } |
| }; |
| let mut background_tab_processes = Vec::<memory::TabProcess>::new(); |
| let mut protected_tab_processes = Vec::<memory::TabProcess>::new(); |
| |
| // The tab processes' last visible times are sent by Chrome as millisecond values |
| // from CLOCK_MONOTONIC. |
| // Rust doesn't support initializing 'Instant's from external values, so the |
| // raw CLOCK_MONOTONIC value is needed as a reference. |
| let current_timestamp_ms = match get_monotonic_timestamp_ms() { |
| Ok(timestamp) => timestamp, |
| Err(_) => { |
| error!("Failed to get current time."); |
| return Err(MethodErr::failed("Failed to get current time.")); |
| } |
| }; |
| let now = Instant::now(); |
| |
| for tab_process in report_browser_processes.processes { |
| // The focused tab processes are not used yet. |
| if tab_process.focused { |
| continue; |
| } |
| |
| // To obtain the 'Instant' at which the tab was last visible, |
| // subtract the number of milliseconds since the tab was visible from the |
| // current Instant. |
| let last_visible_instant = now |
| - Duration::from_millis( |
| (current_timestamp_ms - tab_process.last_visible_ms) as u64, |
| ); |
| |
| if tab_process.protected { |
| &mut protected_tab_processes |
| } else { |
| &mut background_tab_processes |
| } |
| .push(memory::TabProcess { |
| pid: tab_process.pid, |
| last_visible: last_visible_instant, |
| }); |
| } |
| memory::set_browser_tab_processes( |
| browser_type, |
| background_tab_processes, |
| protected_tab_processes, |
| ); |
| Ok(()) |
| }, |
| ); |
| |
| // Advertise the signals. |
| b.signal::<(u8, u64), _>( |
| "MemoryPressureChrome", |
| ("pressure_level", "reclaim_target_kb"), |
| ); |
| b.signal::<(u8, u64), _>( |
| "MemoryPressureArcvm", |
| ("pressure_level", "reclaim_target_kb"), |
| ); |
| }) |
| } |
| |
| fn set_vm_boot_mode(context: DbusContext, mode: common::VmBootMode) -> Result<()> { |
| if !common::is_vm_boot_mode_enabled() { |
| bail!("VM boot mode is not enabled"); |
| } |
| common::set_vm_boot_mode(context.power_preferences_manager.as_ref(), mode).map_err(|e| { |
| error!("set_vm_boot_mode failed: {:#}", e); |
| MethodErr::failed("Failed to set VM boot mode") |
| })?; |
| |
| context |
| .reset_vm_boot_mode_timer_id |
| .fetch_add(1, Ordering::Relaxed); |
| |
| if mode == common::VmBootMode::Active { |
| let timer_id = context.reset_vm_boot_mode_timer_id.load(Ordering::Relaxed); |
| let power_preferences_manager = context.power_preferences_manager.clone(); |
| tokio::spawn(async move { |
| tokio::time::sleep(DEFAULT_VM_BOOT_TIMEOUT).await; |
| if timer_id == context.reset_vm_boot_mode_timer_id.load(Ordering::Relaxed) |
| && common::set_vm_boot_mode( |
| power_preferences_manager.as_ref(), |
| common::VmBootMode::Inactive, |
| ) |
| .is_err() |
| { |
| error!("Reset VM boot mode failed."); |
| } |
| }); |
| } |
| Ok(()) |
| } |
| |
| fn on_battery_saver_mode_change(context: DbusContext, raw_bytes: Vec<u8>) -> Result<()> { |
| let bsm_state: BatterySaverModeState = protobuf::Message::parse_from_bytes(&raw_bytes)?; |
| |
| let mode = if bsm_state.enabled() { |
| common::BatterySaverMode::Active |
| } else { |
| common::BatterySaverMode::Inactive |
| }; |
| |
| common::on_battery_saver_mode_change(context.power_preferences_manager.as_ref(), mode) |
| .map_err(|e| { |
| error!("on_battery_saver_mode_change failed: {:#}", e); |
| MethodErr::failed("Failed to set battery saver mode") |
| })?; |
| |
| Ok(()) |
| } |
| |
| async fn init_battery_saver_mode(context: DbusContext, conn: Arc<SyncConnection>) -> Result<()> { |
| let powerd_proxy = Proxy::new( |
| POWERD_INTERFACE_NAME, |
| POWERD_PATH_NAME, |
| Duration::from_millis(1000), |
| conn, |
| ); |
| |
| let (powerd_response,): (Vec<u8>,) = powerd_proxy |
| .method_call(POWERD_INTERFACE_NAME, "GetBatterySaverModeState", ()) |
| .await?; |
| |
| on_battery_saver_mode_change(context.clone(), powerd_response) |
| } |
| |
| async fn memory_checker_wait(pressure_result: &Result<memory::PressureStatus>) { |
| // Stop waiting if there is 150 ms stall time in 1000 ms window. |
| const STALL_MS: u64 = 150; |
| const WINDOW_MS: u64 = 1000; |
| |
| // Wait longer when the current memory pressure is low. |
| const MIN_WAITING_MS: u64 = 500; |
| const MAX_WAITING_MS_NO_PRESSURE: u64 = 10000; |
| const MAX_WAITING_MS_MODERATE_PRESSURE: u64 = 5000; |
| const MAX_WAITING_MS_CRITICAL_PRESSURE: u64 = 1000; |
| |
| let max_waiting_ms = match pressure_result { |
| Ok(pressure_status) => match pressure_status.chrome_level { |
| memory::PressureLevelChrome::None => MAX_WAITING_MS_NO_PRESSURE, |
| memory::PressureLevelChrome::Moderate => MAX_WAITING_MS_MODERATE_PRESSURE, |
| memory::PressureLevelChrome::Critical => MAX_WAITING_MS_CRITICAL_PRESSURE, |
| }, |
| Err(e) => { |
| error!("get_memory_pressure_status() failed: {}", e); |
| MAX_WAITING_MS_NO_PRESSURE |
| } |
| }; |
| |
| // Waiting for certain range of duration. Interrupt if PSI memory stall exceeds the |
| // threshold. |
| let wait_result = |
| psi::wait_psi_monitor_memory_event(STALL_MS, WINDOW_MS, MIN_WAITING_MS, max_waiting_ms) |
| .await; |
| if wait_result.is_err() { |
| error!( |
| "wait_psi_monitor_memory_event returns error: {:?}", |
| wait_result |
| ); |
| // Fallback to 1 second waiting. |
| const MEMORY_USAGE_POLL_INTERVAL: u64 = 1000; |
| tokio::time::sleep(Duration::from_millis(MEMORY_USAGE_POLL_INTERVAL)).await; |
| } |
| } |
| |
| fn report_notification_count(notification_count: i32) -> Result<()> { |
| metrics::send_to_uma( |
| "Platform.Resourced.MemoryNotificationCountTenMinutes", // Metric name |
| notification_count, // Sample |
| 0, // Min |
| 1000, // Max |
| 50, // Number of buckets |
| ) |
| } |
| |
| fn report_memory_stats(stats: process_stats::MemStats) -> Result<()> { |
| for (process_kind, stats) in stats.iter().enumerate() { |
| for (mem_kind, usage_bytes) in stats.iter().enumerate() { |
| // Max process allocation size in megabytes, used as an upper bound |
| // for UMA histograms (these are all consumer devices, and 64 GB |
| // should be good for a few more years). |
| const MAX_MEM_SIZE_MIB: i32 = 64 * 1024; |
| metrics::send_to_uma( |
| &process_stats::get_metric_name(process_kind, mem_kind), |
| (usage_bytes / 1024 / 1024) as i32, |
| 1, |
| MAX_MEM_SIZE_MIB, |
| 50, |
| )?; |
| } |
| } |
| Ok(()) |
| } |
| |
| fn get_monotonic_timestamp_ms() -> Result<i64> { |
| let mut ts = libc::timespec { |
| tv_sec: 0, |
| tv_nsec: 0, |
| }; |
| |
| // SAFETY: Safe because the only side-effects of this function are modifications via the |
| // passed pointer, and we pass a pointer of the proper type. |
| let current_timestamp_ms = |
| if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts as *mut libc::timespec) } |
| == 0 |
| { |
| // On 32bit platforms, tv_sec and tv_nsec are i32. Upcast to u64 here for compatibility. |
| (ts.tv_sec as u64) * 1000 + (ts.tv_nsec as u64) / 1_000_000 |
| } else { |
| bail!("Failed to get current time."); |
| }; |
| |
| Ok(current_timestamp_ms as i64) |
| } |
| |
| async fn psi_memory_handler_loop( |
| root: &Path, |
| conn: &Arc<SyncConnection>, |
| vmms_client: &VmMemoryManagementClient, |
| feature_notify: &Arc<Notify>, |
| notification_count: &Arc<AtomicI32>, |
| ) -> PsiPolicyResult<()> { |
| let mut handler = PsiMemoryHandler::new(root)?; |
| loop { |
| if let Some(pressure_status) = handler |
| .monitor_memory_pressure(vmms_client, feature_notify) |
| .await? |
| { |
| send_pressure_signals(conn, &pressure_status); |
| notification_count.fetch_add(1, Ordering::Relaxed); |
| } else { |
| // reclaim_on_memory_pressure() returns `None` if the feature flag or params is changed. |
| if !is_feature_enabled(memory::PSI_MEMORY_POLICY_FEATURE_NAME).unwrap_or(false) { |
| return Ok(()); |
| } |
| handler.reload_feature_params(); |
| } |
| } |
| } |
| |
| async fn margin_memory_handler_loop( |
| conn: &Arc<SyncConnection>, |
| vmms_client: &VmMemoryManagementClient, |
| notification_count: &Arc<AtomicI32>, |
| ) { |
| loop { |
| let pressure_result = memory::get_memory_pressure_status(vmms_client).await; |
| |
| // Send memory pressure notification. |
| if let Ok(pressure_status) = pressure_result { |
| send_pressure_signals(conn, &pressure_status); |
| } |
| |
| notification_count.fetch_add(1, Ordering::Relaxed); |
| |
| memory_checker_wait(&pressure_result).await; |
| |
| // The feature flag change is reflected in 10 seconds since memory_checker_wait() wakes |
| // within 10 seconds at least. |
| if is_feature_enabled(memory::PSI_MEMORY_POLICY_FEATURE_NAME).unwrap_or(false) { |
| return; |
| } |
| } |
| } |
| |
| fn send_pressure_signals(conn: &Arc<SyncConnection>, pressure_status: &memory::PressureStatus) { |
| send_pressure_signal( |
| conn, |
| "MemoryPressureChrome", |
| pressure_status.chrome_level as u8, |
| pressure_status.chrome_reclaim_target_kb, |
| ); |
| if pressure_status.arc_container_level != memory::PressureLevelArcContainer::None { |
| send_pressure_signal( |
| conn, |
| "MemoryPressureArcContainer", |
| pressure_status.arc_container_level as u8, |
| pressure_status.arc_container_reclaim_target_kb, |
| ); |
| } |
| } |
| |
| pub async fn service_main() -> Result<()> { |
| let root = Path::new("/"); |
| let config_provider = ConfigProvider::from_root(root); |
| let scheduler_context = match qos::create_schedqos_context(root, &config_provider) { |
| Ok(ctx) => Some(Arc::new(Mutex::new(ctx))), |
| Err(e) => { |
| error!("failed to initialize schedqos context: {e}"); |
| None |
| } |
| }; |
| let context = DbusContext { |
| power_preferences_manager: Arc::new(power::new_directory_power_preferences_manager( |
| root, |
| config_provider, |
| )), |
| reset_game_mode_timer_id: Arc::new(AtomicUsize::new(0)), |
| reset_fullscreen_video_timer_id: Arc::new(AtomicUsize::new(0)), |
| reset_vm_boot_mode_timer_id: Arc::new(AtomicUsize::new(0)), |
| scheduler_context, |
| }; |
| |
| let (io_resource, conn) = connection::new_system_sync()?; |
| |
| // io_resource must be awaited to start receiving D-Bus message. |
| let _handle = tokio::spawn(async { |
| let err = io_resource.await; |
| panic!("Lost connection to D-Bus: {}", err); |
| }); |
| |
| arch::init(); |
| |
| memory::register_features(); |
| |
| let psi_memory_policy_notify = Arc::new(Notify::new()); |
| let notify_cloned = psi_memory_policy_notify.clone(); |
| register_feature( |
| memory::PSI_MEMORY_POLICY_FEATURE_NAME, |
| false, |
| Some(Box::new(move |_| { |
| notify_cloned.notify_waiters(); |
| })), |
| ); |
| |
| feature::init(conn.as_ref()) |
| .await |
| .context("start feature monitoring")?; |
| |
| let vmms_client = VmMemoryManagementClient::new(conn.clone()) |
| .await |
| .context("create VmMemoryManagementClient")?; |
| |
| conn.request_name(SERVICE_NAME, false, true, false).await?; |
| |
| let mut cr = Crossroads::new(); |
| |
| // Enable asynchronous methods. Incoming method calls are spawned as separate tasks if |
| // necessary. |
| cr.set_async_support(Some(( |
| conn.clone(), |
| Box::new(|x| { |
| tokio::spawn(x); |
| }), |
| ))); |
| |
| let token = register_interface(&mut cr, conn.clone()); |
| cr.insert(PATH_NAME, &[token], context.clone()); |
| |
| #[cfg(feature = "vm_grpc")] |
| { |
| // This block does a 1 time check at resourced startup to see if borealis vm |
| // is already running. The `await` has a blocking timeout of 2 seconds |
| // (2 embedded dbus calls). If there is no vm running (expected case), no grpc server |
| // will be started. If resourced was respawned while a borealis session was |
| // active (corner case), this logic will start a vm_grpc server and re-establish the |
| // grpc link. |
| // Internal failures will be logged and ignored. We don't interfere with resourced |
| // operations if grpc server/client fails. |
| let _ = |
| crate::vm_grpc::vm_grpc_util::handle_startup_borealis_state_async(conn.clone()).await; |
| let _ = crate::vm_grpc::vm_grpc_util::register_dbus_hooks_async(conn.clone()).await; |
| } |
| |
| if common::is_vm_boot_mode_enabled() { |
| // Receive VmStartingUpSignal for VM boot mode |
| let vm_starting_up_rule = |
| MatchRule::new_signal(VMCONCIEGE_INTERFACE_NAME, "VmStartingUpSignal"); |
| conn.add_match_no_cb(&vm_starting_up_rule.match_str()) |
| .await?; |
| let context2 = context.clone(); |
| conn.start_receive( |
| vm_starting_up_rule, |
| Box::new(move |_, _| { |
| if let Err(e) = set_vm_boot_mode(context2.clone(), common::VmBootMode::Active) { |
| error!("Failed to initalize VM boot boosting. {}", e); |
| } |
| true |
| }), |
| ); |
| let vm_complete_boot_rule = |
| MatchRule::new_signal(VMCONCIEGE_INTERFACE_NAME, "VmGuestUserlandReadySignal"); |
| conn.add_match_no_cb(&vm_complete_boot_rule.match_str()) |
| .await?; |
| let cb_context = context.clone(); |
| conn.start_receive( |
| vm_complete_boot_rule, |
| Box::new(move |_, _| { |
| if let Err(e) = set_vm_boot_mode(cb_context.clone(), common::VmBootMode::Inactive) { |
| error!("Failed to stop VM boot boosting. {}", e); |
| } |
| true |
| }), |
| ); |
| } |
| |
| if init_battery_saver_mode(context.clone(), conn.clone()) |
| .await |
| .is_err() |
| { |
| error!("init_battery_saver_mode failed"); |
| } |
| |
| // Registers callbacks for `BatterySaverModeStateChanged` from powerd. |
| const BATTERY_SAVER_MODE_EVENT: &str = "BatterySaverModeStateChanged"; |
| let battery_saver_mode_rule = |
| MatchRule::new_signal(POWERD_INTERFACE_NAME, BATTERY_SAVER_MODE_EVENT); |
| conn.add_match_no_cb(&battery_saver_mode_rule.match_str()) |
| .await?; |
| |
| conn.start_receive( |
| battery_saver_mode_rule, |
| Box::new(move |msg, _| match msg.read1() { |
| Ok(bytes) => { |
| if let Err(e) = on_battery_saver_mode_change(context.clone(), bytes) { |
| error!("error handling Battery Saver Mode change. {}", e); |
| } |
| true |
| } |
| Err(e) => { |
| error!( |
| "error reading D-Bus message {}. {}", |
| BATTERY_SAVER_MODE_EVENT, e |
| ); |
| true |
| } |
| }), |
| ); |
| |
| conn.start_receive( |
| MatchRule::new_method_call(), |
| Box::new(move |msg, conn| match cr.handle_message(msg, conn) { |
| Ok(()) => true, |
| Err(()) => { |
| error!("error handling D-Bus message"); |
| false |
| } |
| }), |
| ); |
| |
| // Reports memory pressure notification count and memory stats every 10 minutes. |
| let notification_count = Arc::new(AtomicI32::new(0)); |
| let notification_count_clone = notification_count.clone(); |
| tokio::spawn(async move { |
| loop { |
| // 10 minutes interval. |
| tokio::time::sleep(Duration::from_secs(10 * 60)).await; |
| |
| let count = notification_count_clone.load(Ordering::Relaxed); |
| |
| if let Err(err) = report_notification_count(count) { |
| error!("Failed to report notification count: {}", err); |
| } |
| notification_count_clone.store(0, Ordering::Relaxed); |
| |
| match process_stats::get_all_memory_stats("/proc", "/run") { |
| Ok(stats) => { |
| if let Err(err) = report_memory_stats(stats) { |
| error!("Failed to report memory stats: {}", err); |
| } |
| } |
| Err(e) => error!("Failed to gather memory stats {:?}", e), |
| } |
| } |
| }); |
| |
| loop { |
| let use_psi_policy = match is_feature_enabled(memory::PSI_MEMORY_POLICY_FEATURE_NAME) { |
| Ok(v) => v, |
| Err(e) => { |
| error!( |
| "Failed to get feature {}: {}", |
| memory::PSI_MEMORY_POLICY_FEATURE_NAME, |
| e |
| ); |
| false |
| } |
| }; |
| if use_psi_policy { |
| info!("Using psi memory policy"); |
| if let Err(e) = psi_memory_handler_loop( |
| root, |
| &conn, |
| &vmms_client, |
| &psi_memory_policy_notify, |
| ¬ification_count, |
| ) |
| .await |
| { |
| error!("Failed to run psi memory checker loop: {}", e); |
| if let Err(e) = metrics::send_enum_to_uma( |
| UMA_NAME_PSI_POLICY_ERROR, |
| e.as_i32(), |
| MAX_PSI_ERROR_TYPE + 1, |
| ) { |
| error!("Failed to send psi memory checker error to UMA: {}", e); |
| } |
| |
| // wait 1 second to retry to avoid logging the error in a busy loop. |
| tokio::time::sleep(Duration::from_secs(1)).await; |
| } |
| } else { |
| info!("Using margin memory policy"); |
| margin_memory_handler_loop(&conn, &vmms_client, ¬ification_count).await; |
| } |
| } |
| } |