From f187d9c5fc771f6da863ed2810bb83c2e225a9c2 Mon Sep 17 00:00:00 2001 From: Brezak Date: Mon, 10 Jun 2024 21:06:22 +0200 Subject: [PATCH] Poll system information in separate tasks (#13693) # Objective Reading system information severely slows down the update loop. Fixes #12848. ## Solution Read system info in a separate thread. ## Testing - Open the scene 3d example - Add `FrameTimeDiagnosticsPlugin`, `SystemInformationDiagnosticsPlugin` and `LogDiagnosticsPlugin` to the app. - Add this system to the update schedule to disable Vsync on the main window ```rust fn change_window_mode(mut windows: Query<&mut Window, Added>) { for mut window in &mut windows { window.present_mode = PresentMode::AutoNoVsync; } } ``` - Read the fps values in the console before and after this PR. On my PC I went from around 50 fps to around 1150 fps. --- ## Changelog ### Changed - The `SystemInformationDiagnosticsPlugin` now reads system data separate of the update cycle. ### Added - The `EXPECTED_SYSTEM_INFORMATION_INTERVAL` constant which defines how often we read system diagnostic data. --------- Co-authored-by: IceSentry --- crates/bevy_diagnostic/Cargo.toml | 1 + .../system_information_diagnostics_plugin.rs | 122 +++++++++++++----- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 78f1d0bb005fb..a9678d0a23ee0 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -20,6 +20,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } bevy_time = { path = "../bevy_time", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } const-fnv1a-hash = "1.1.0" diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 3709a6d05fd18..981912ddc511c 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -4,6 +4,9 @@ use bevy_ecs::system::Resource; /// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %) /// +/// Note that gathering system information is a time intensive task and therefore can't be done on every frame. +/// Any system diagnostics gathered by this plugin may not be current when you access them. +/// /// Supported targets: /// * linux, /// * windows, @@ -19,8 +22,7 @@ use bevy_ecs::system::Resource; pub struct SystemInformationDiagnosticsPlugin; impl Plugin for SystemInformationDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, internal::setup_system) - .add_systems(Update, internal::diagnostic_system); + internal::setup_plugin(app); } } @@ -58,6 +60,14 @@ pub struct SystemInfo { ))] pub mod internal { use bevy_ecs::{prelude::ResMut, system::Local}; + use std::{ + sync::{Arc, Mutex}, + time::Instant, + }; + + use bevy_app::{App, First, Startup, Update}; + use bevy_ecs::system::Resource; + use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task}; use bevy_utils::tracing::info; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; @@ -67,41 +77,91 @@ pub mod internal { const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0; - pub(crate) fn setup_system(mut diagnostics: ResMut) { + pub(super) fn setup_plugin(app: &mut App) { + app.add_systems(Startup, setup_system) + .add_systems(First, launch_diagnostic_tasks) + .add_systems(Update, read_diagnostic_tasks) + .init_resource::(); + } + + fn setup_system(mut diagnostics: ResMut) { diagnostics .add(Diagnostic::new(SystemInformationDiagnosticsPlugin::CPU_USAGE).with_suffix("%")); diagnostics .add(Diagnostic::new(SystemInformationDiagnosticsPlugin::MEM_USAGE).with_suffix("%")); } - pub(crate) fn diagnostic_system( - mut diagnostics: Diagnostics, - mut sysinfo: Local>, + struct SysinfoRefreshData { + current_cpu_usage: f64, + current_used_mem: f64, + } + + #[derive(Resource, Default)] + struct SysinfoTasks { + tasks: Vec>, + } + + fn launch_diagnostic_tasks( + mut tasks: ResMut, + // TODO: Consider a fair mutex + mut sysinfo: Local>>>, + // TODO: FromWorld for Instant? + mut last_refresh: Local>, ) { - if sysinfo.is_none() { - *sysinfo = Some(System::new_with_specifics( + let sysinfo = sysinfo.get_or_insert_with(|| { + Arc::new(Mutex::new(System::new_with_specifics( RefreshKind::new() .with_cpu(CpuRefreshKind::new().with_cpu_usage()) .with_memory(MemoryRefreshKind::everything()), - )); - } - let Some(sys) = sysinfo.as_mut() else { - return; - }; - - sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage()); - sys.refresh_memory(); - let current_cpu_usage = sys.global_cpu_info().cpu_usage(); - // `memory()` fns return a value in bytes - let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB; - let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB; - let current_used_mem = used_mem / total_mem * 100.0; - - diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || { - current_cpu_usage as f64 + ))) }); - diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || { - current_used_mem + + let last_refresh = last_refresh.get_or_insert_with(Instant::now); + + let thread_pool = AsyncComputeTaskPool::get(); + + // Only queue a new system refresh task when necessary + // Queueing earlier than that will not give new data + if last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL + // These tasks don't yield and will take up all of the task pool's + // threads if we don't limit their amount. + && tasks.tasks.len() * 2 < available_parallelism() + { + let sys = Arc::clone(sysinfo); + let task = thread_pool.spawn(async move { + let mut sys = sys.lock().unwrap(); + + sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage()); + sys.refresh_memory(); + let current_cpu_usage = sys.global_cpu_info().cpu_usage().into(); + // `memory()` fns return a value in bytes + let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB; + let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB; + let current_used_mem = used_mem / total_mem * 100.0; + + SysinfoRefreshData { + current_cpu_usage, + current_used_mem, + } + }); + tasks.tasks.push(task); + *last_refresh = Instant::now(); + } + } + + fn read_diagnostic_tasks(mut diagnostics: Diagnostics, mut tasks: ResMut) { + tasks.tasks.retain_mut(|task| { + let Some(data) = block_on(poll_once(task)) else { + return true; + }; + + diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || { + data.current_cpu_usage + }); + diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || { + data.current_used_mem + }); + false }); } @@ -145,12 +205,14 @@ pub mod internal { not(feature = "dynamic_linking") )))] pub mod internal { - pub(crate) fn setup_system() { - bevy_utils::tracing::warn!("This platform and/or configuration is not supported!"); + use bevy_app::{App, Startup}; + + pub(super) fn setup_plugin(app: &mut App) { + app.add_systems(Startup, setup_system); } - pub(crate) fn diagnostic_system() { - // no-op + fn setup_system() { + bevy_utils::tracing::warn!("This platform and/or configuration is not supported!"); } impl Default for super::SystemInfo {