| // 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. |
| |
| #[cfg(feature = "vm_grpc")] |
| use crate::vm_grpc::vm_grpc_util::vm_grpc_init; |
| #[cfg(feature = "vm_grpc")] |
| use libchromeos::sys::warn; |
| |
| use std::collections::HashMap; |
| use std::convert::TryFrom; |
| use std::path::Path; |
| use std::sync::atomic::{AtomicUsize, Ordering}; |
| use std::sync::Arc; |
| use std::time::Duration; |
| |
| use anyhow::bail; |
| use anyhow::Result; |
| use dbus::channel::MatchingReceiver; |
| use dbus::channel::Sender; |
| use dbus::message::{MatchRule, Message}; |
| use dbus::nonblock::{Proxy, SyncConnection}; |
| use dbus_crossroads::{Crossroads, IfaceBuilder, IfaceToken, MethodErr}; |
| use dbus_tokio::connection; |
| use libchromeos::sys::error; |
| use log::LevelFilter; |
| |
| #[cfg(target_arch = "x86_64")] |
| use crate::gpu_freq_scaling; |
| |
| use crate::common; |
| use crate::config; |
| use crate::memory; |
| use crate::power; |
| |
| 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 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< |
| config::DirectoryConfigProvider, |
| 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_vc_mode_timer_id: Arc<AtomicUsize>, |
| reset_fullscreen_video_timer_id: Arc<AtomicUsize>, |
| reset_vm_boot_mode_timer_id: Arc<AtomicUsize>, |
| } |
| |
| fn send_pressure_signal( |
| conn: &SyncConnection, |
| signal_name: &str, |
| level: u8, |
| reclaim_target_kb: u64, |
| ) { |
| let msg = Message::signal( |
| &PATH_NAME.into(), |
| &INTERFACE_NAME.into(), |
| &signal_name.into(), |
| ) |
| .append2(level, reclaim_target_kb); |
| if conn.send(msg).is_err() { |
| error!("Send Chrome pressure signal failed."); |
| } |
| } |
| |
| // Call debugd 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)? |
| { |
| const SWAPPINESS_PATH: &str = "/proc/sys/vm/swappiness"; |
| if swappiness != common::read_file_to_u64(SWAPPINESS_PATH)? as u32 { |
| tokio::spawn(async move { |
| let debugd_proxy = Proxy::new( |
| "org.chromium.debugd", |
| "/org/chromium/debugd", |
| DEFAULT_DBUS_TIMEOUT, |
| conn, |
| ); |
| match debugd_proxy |
| .method_call("org.chromium.debugd", "SwapSetSwappiness", (swappiness,)) |
| .await |
| { |
| Ok(()) => (), // For type inference. |
| Err(e) => error!("Calling SwapSetSwappiness failed: {:#}", e), |
| } |
| }); |
| } |
| } |
| Ok(()) |
| } |
| |
| 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 = match common::get_game_mode() { |
| Ok(available) => Ok(available), |
| Err(_) => Err(MethodErr::failed("Couldn't get game mode state")), |
| }?; |
| match memory::get_background_available_memory_kb(game_mode) { |
| Ok(available) => Ok((available,)), |
| Err(_) => Err(MethodErr::failed("Couldn't get available memory")), |
| } |
| }, |
| ); |
| b.method( |
| "GetForegroundAvailableMemoryKB", |
| (), |
| ("available",), |
| move |_, _, ()| match memory::get_foreground_available_memory_kb() { |
| Ok(available) => Ok((available,)), |
| 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), |
| ]); |
| Ok((result,)) |
| }, |
| ); |
| b.method( |
| "SetMemoryMarginsBps", |
| ("critical_bps", "moderate_bps"), |
| ("critical", "moderate"), |
| move |_, _, (critical_bps, moderate_bps): (u32, u32)| { |
| match memory::set_memory_margins_bps(critical_bps, moderate_bps) { |
| Ok(()) => { |
| let margins = memory::get_memory_margins_kb(); |
| Ok((margins.0, margins.1)) |
| } |
| Err(_) => Err(MethodErr::failed("Failed to set memory thresholds")), |
| } |
| }, |
| ); |
| b.method( |
| "SetVCModeWithTimeout", |
| ("timeout_sec",), |
| (), |
| move |_, context, (timeout_sec,): (u32,)| { |
| #[cfg(target_arch = "x86_64")] |
| { |
| gpu_freq_scaling::enable_vc_mode().map_err(|e| { |
| error!("enable_vc_mode failed: {:#}", e); |
| MethodErr::failed("Failed to enable VC mode") |
| })?; |
| |
| let timeout = Duration::from_secs(timeout_sec.into()); |
| |
| context |
| .reset_vc_mode_timer_id |
| .fetch_add(1, Ordering::Relaxed); |
| let timer_id = context.reset_vc_mode_timer_id.load(Ordering::Relaxed); |
| let reset_vc_mode_timer_id = context.reset_vc_mode_timer_id.clone(); |
| |
| tokio::spawn(async move { |
| tokio::time::sleep(timeout).await; |
| // If the timer id is changed, this event is canceled. |
| if timer_id == reset_vc_mode_timer_id.load(Ordering::Relaxed) |
| && gpu_freq_scaling::disable_vc_mode().is_err() |
| { |
| error!("disable_vc_mode failed"); |
| } |
| }); |
| } |
| Ok(()) |
| }, |
| ); |
| b.method( |
| "GetGameMode", |
| (), |
| ("game_mode",), |
| move |_, _, ()| match common::get_game_mode() { |
| Ok(game_mode) => Ok((game_mode as u8,)), |
| Err(_) => Err(MethodErr::failed("Failed to get game mode")), |
| }, |
| ); |
| 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(()) |
| }, |
| ); |
| 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() |
| .map_err(|_| MethodErr::failed("Failed to 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.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.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 |_, _, ()| { |
| match common::get_rtc_audio_active() { |
| Ok(active) => Ok((active as u8,)), |
| Err(_) => Err(MethodErr::failed("Failed to get RTC audio activity")), |
| } |
| }); |
| 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_audeio_active failed: {:#}", e); |
| Err(MethodErr::failed("Failed to set RTC audio activity")) |
| } |
| } |
| }, |
| ); |
| b.method("GetFullscreenVideo", (), ("mode",), move |_, _, ()| { |
| match common::get_fullscreen_video() { |
| Ok(mode) => Ok((mode as u8,)), |
| Err(_) => Err(MethodErr::failed("Failed to get fullscreen video activity")), |
| } |
| }); |
| 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(()) |
| }, |
| ); |
| |
| // 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(()) |
| } |
| |
| pub async fn service_main() -> Result<()> { |
| let root = Path::new("/"); |
| let context = DbusContext { |
| power_preferences_manager: Arc::new(power::new_directory_power_preferences_manager(root)), |
| reset_game_mode_timer_id: Arc::new(AtomicUsize::new(0)), |
| reset_vc_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)), |
| }; |
| |
| 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); |
| }); |
| |
| 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")] |
| { |
| let vm_started_rule = MatchRule::new_signal(VMCONCIEGE_INTERFACE_NAME, "VmStartedSignal"); |
| |
| // Swallow any errors related to grpc dbus messages. Failure to initialize |
| // VM_GRPC sohuld not bring down resourced. |
| match conn.add_match_no_cb(&vm_started_rule.match_str()).await { |
| Ok(_) => (), |
| Err(_) => warn!("Unable to set filtering of VmStarted dbus message."), |
| } |
| |
| conn.start_receive( |
| vm_started_rule, |
| Box::new(|msg, _| { |
| match vm_grpc_init(&msg) { |
| Ok(_) => (), |
| Err(e) => warn!("Failed to initialize GRPC client/server pair. {}", e), |
| } |
| true |
| }), |
| ); |
| } |
| |
| 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 |_, _| { |
| match set_vm_boot_mode(context2.clone(), common::VmBootMode::Active) { |
| Ok(_) => true, |
| Err(e) => { |
| error!("Failed to initalize VM boot boosting. {}", e); |
| false |
| } |
| } |
| }), |
| ); |
| let vm_complete_boot_rule = |
| MatchRule::new_signal(VMCONCIEGE_INTERFACE_NAME, "VmGuestUserlandReadySignal"); |
| conn.add_match_no_cb(&vm_complete_boot_rule.match_str()) |
| .await?; |
| conn.start_receive( |
| vm_complete_boot_rule, |
| Box::new(move |_, _| { |
| match set_vm_boot_mode(context.clone(), common::VmBootMode::Inactive) { |
| Ok(_) => true, |
| Err(e) => { |
| error!("Failed to stop VM boot boosting. {}", e); |
| false |
| } |
| } |
| }), |
| ); |
| } |
| |
| 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 |
| } |
| }), |
| ); |
| |
| // The memory checker loop. |
| loop { |
| const MEMORY_USAGE_POLL_INTERVAL: u64 = 1000; |
| tokio::time::sleep(Duration::from_millis(MEMORY_USAGE_POLL_INTERVAL)).await; |
| |
| match memory::get_memory_pressure_status() { |
| Ok(pressure_status) => { |
| send_pressure_signal( |
| &conn, |
| "MemoryPressureChrome", |
| pressure_status.chrome_level as u8, |
| pressure_status.chrome_reclaim_target_kb, |
| ); |
| if pressure_status.arcvm_level != memory::PressureLevelArcvm::None { |
| send_pressure_signal( |
| &conn, |
| "MemoryPressureArcvm", |
| pressure_status.arcvm_level as u8, |
| pressure_status.arcvm_reclaim_target_kb, |
| ); |
| } |
| } |
| Err(e) => error!("get_memory_pressure_status() failed: {}", e), |
| } |
| } |
| } |