From 5a88ad61240eb2306f29ee217e2a53db27c75abd Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Wed, 3 Sep 2025 20:58:42 +0300 Subject: [PATCH 1/6] Refactor system diagnostics to use a single task System information diagnostics now uses a single task that wakes up every time the `First` schedule runs. The task checks if enough time has passed since the last refresh. If enough time has passed, it refreshes the system information and sends it to a channel. The `read_diagonstic_task` system then reads the system information from the channel to add diagnostic data. --- crates/bevy_diagnostic/src/diagnostic.rs | 8 +- .../system_information_diagnostics_plugin.rs | 185 +++++++++++------- 2 files changed, 113 insertions(+), 80 deletions(-) diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index dd1e3576431d6..937da5b258502 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -39,18 +39,18 @@ impl DiagnosticPath { pub fn new(path: impl Into>) -> DiagnosticPath { let path = path.into(); - debug_assert!(!path.is_empty(), "diagnostic path can't be empty"); + debug_assert!(!path.is_empty(), "diagnostic path should not be empty"); debug_assert!( !path.starts_with('/'), - "diagnostic path can't be start with `/`" + "diagnostic path should not start with `/`" ); debug_assert!( !path.ends_with('/'), - "diagnostic path can't be end with `/`" + "diagnostic path should not end with `/`" ); debug_assert!( !path.contains("//"), - "diagnostic path can't contain empty components" + "diagnostic path should not contain empty components" ); DiagnosticPath { diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 83d3663895ca5..cb06a6724ffc8 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -70,19 +70,25 @@ pub struct SystemInfo { feature = "std", ))] pub mod internal { + use core::{ + pin::Pin, + task::{Context, Poll, Waker}, + }; + use std::sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Mutex, + }; + use alloc::{ format, string::{String, ToString}, - sync::Arc, - vec::Vec, }; use bevy_app::{App, First, Startup, Update}; use bevy_ecs::resource::Resource; - use bevy_ecs::{prelude::ResMut, system::Local}; + use bevy_ecs::{prelude::ResMut, system::Commands}; use bevy_platform::time::Instant; - use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task}; + use bevy_tasks::AsyncComputeTaskPool; use log::info; - use std::sync::Mutex; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; use crate::{Diagnostic, Diagnostics, DiagnosticsStore}; @@ -93,12 +99,20 @@ pub mod internal { 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::(); + .add_systems(First, wake_diagnostic_task) + .add_systems(Update, read_diagnostic_task); } - fn setup_system(mut diagnostics: ResMut) { + fn setup_system(mut diagnostics: ResMut, mut commands: Commands) { + let (tx, rx) = mpsc::channel(); + let diagnostic_task = DiagnosticTask::new(tx); + let waker = Arc::clone(&diagnostic_task.waker); + AsyncComputeTaskPool::get().spawn(diagnostic_task).detach(); + commands.insert_resource(SysinfoTask { + receiver: Mutex::new(rx), + waker, + }); + diagnostics.add( Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"), ); @@ -121,78 +135,98 @@ pub mod internal { process_mem_usage: f64, } - #[derive(Resource, Default)] - struct SysinfoTasks { - tasks: Vec>, + impl SysinfoRefreshData { + fn new(system: &mut System) -> Self { + let pid = sysinfo::get_current_pid().expect("Failed to get current process ID"); + system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true); + + system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage()); + system.refresh_memory(); + + let system_cpu_usage = system.global_cpu_usage().into(); + let total_mem = system.total_memory() as f64; + let used_mem = system.used_memory() as f64; + let system_mem_usage = used_mem / total_mem * 100.0; + + let process_mem_usage = system + .process(pid) + .map(|p| p.memory() as f64 * BYTES_TO_GIB) + .unwrap_or(0.0); + + let process_cpu_usage = system + .process(pid) + .map(|p| p.cpu_usage() as f64 / system.cpus().len() as f64) + .unwrap_or(0.0); + + Self { + system_cpu_usage, + system_mem_usage, + process_cpu_usage, + process_mem_usage, + } + } + } + + #[derive(Resource)] + struct SysinfoTask { + receiver: Mutex>, + waker: Arc>>, } - fn launch_diagnostic_tasks( - mut tasks: ResMut, - // TODO: Consider a fair mutex - mut sysinfo: Local>>>, - // TODO: FromWorld for Instant? - mut last_refresh: Local>, - ) { - let sysinfo = sysinfo.get_or_insert_with(|| { - Arc::new(Mutex::new(System::new_with_specifics( - RefreshKind::nothing() - .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) - .with_memory(MemoryRefreshKind::everything()), - ))) - }); + struct DiagnosticTask { + system: System, + last_refresh: Instant, + sender: Sender, + waker: Arc>>, + } - 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 - // Queuing 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(); - let pid = sysinfo::get_current_pid().expect("Failed to get current process ID"); - sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true); - - sys.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage()); - sys.refresh_memory(); - let system_cpu_usage = sys.global_cpu_usage().into(); - let total_mem = sys.total_memory() as f64; - let used_mem = sys.used_memory() as f64; - let system_mem_usage = used_mem / total_mem * 100.0; - - let process_mem_usage = sys - .process(pid) - .map(|p| p.memory() as f64 * BYTES_TO_GIB) - .unwrap_or(0.0); - - let process_cpu_usage = sys - .process(pid) - .map(|p| p.cpu_usage() as f64 / sys.cpus().len() as f64) - .unwrap_or(0.0); - - SysinfoRefreshData { - system_cpu_usage, - system_mem_usage, - process_cpu_usage, - process_mem_usage, - } - }); - tasks.tasks.push(task); - *last_refresh = Instant::now(); + impl DiagnosticTask { + fn new(sender: Sender) -> Self { + Self { + system: System::new_with_specifics( + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) + .with_memory(MemoryRefreshKind::everything()), + ), + last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL, + sender, + waker: Arc::default(), + } + } + + fn update_waker(&mut self, cx: &mut Context<'_>) { + let mut waker = self.waker.lock().unwrap(); + *waker = Some(cx.waker().clone()); } } - 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; - }; + impl Future for DiagnosticTask { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.update_waker(cx); + + if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL { + self.last_refresh = Instant::now(); + + let sysinfo_refresh_data = SysinfoRefreshData::new(&mut self.system); + self.sender.send(sysinfo_refresh_data).unwrap(); + } + Poll::Pending + } + } + + fn wake_diagnostic_task(task: ResMut) { + let mut waker = task.waker.lock().unwrap(); + if let Some(waker) = waker.take() { + waker.wake_by_ref(); + } + } + + fn read_diagnostic_task(mut diagnostics: Diagnostics, task: ResMut) { + let receiver = task.receiver.lock().unwrap(); + while let Ok(data) = receiver.try_recv() { diagnostics.add_measurement( &SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE, || data.system_cpu_usage, @@ -209,8 +243,7 @@ pub mod internal { &SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE, || data.process_mem_usage, ); - false - }); + } } impl Default for SystemInfo { From e857a0cd3b0ed7cba3daf9e4eb76f7884b0578a4 Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Thu, 4 Sep 2025 13:55:33 +0300 Subject: [PATCH 2/6] Use AtomicWaker in SysinfoTask --- crates/bevy_diagnostic/Cargo.toml | 1 + .../system_information_diagnostics_plugin.rs | 19 +++++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index de04e5ec9946b..b803136931b41 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -60,6 +60,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # other +atomic-waker = { version = "1", default-features = false } const-fnv1a-hash = "1.1.0" serde = { version = "1.0", default-features = false, features = [ "alloc", diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index cb06a6724ffc8..1c9512418ce18 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -72,7 +72,7 @@ pub struct SystemInfo { pub mod internal { use core::{ pin::Pin, - task::{Context, Poll, Waker}, + task::{Context, Poll}, }; use std::sync::{ mpsc::{self, Receiver, Sender}, @@ -83,6 +83,7 @@ pub mod internal { format, string::{String, ToString}, }; + use atomic_waker::AtomicWaker; use bevy_app::{App, First, Startup, Update}; use bevy_ecs::resource::Resource; use bevy_ecs::{prelude::ResMut, system::Commands}; @@ -170,14 +171,14 @@ pub mod internal { #[derive(Resource)] struct SysinfoTask { receiver: Mutex>, - waker: Arc>>, + waker: Arc, } struct DiagnosticTask { system: System, last_refresh: Instant, sender: Sender, - waker: Arc>>, + waker: Arc, } impl DiagnosticTask { @@ -193,18 +194,13 @@ pub mod internal { waker: Arc::default(), } } - - fn update_waker(&mut self, cx: &mut Context<'_>) { - let mut waker = self.waker.lock().unwrap(); - *waker = Some(cx.waker().clone()); - } } impl Future for DiagnosticTask { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.update_waker(cx); + self.waker.register(cx.waker()); if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL { self.last_refresh = Instant::now(); @@ -218,9 +214,8 @@ pub mod internal { } fn wake_diagnostic_task(task: ResMut) { - let mut waker = task.waker.lock().unwrap(); - if let Some(waker) = waker.take() { - waker.wake_by_ref(); + if let Some(waker) = task.waker.take() { + waker.wake(); } } From 5dbfde961443c061473639e42b730cea1c90b148 Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Thu, 4 Sep 2025 14:18:19 +0300 Subject: [PATCH 3/6] Use SyncCell in SysinfoTask --- .../src/system_information_diagnostics_plugin.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 1c9512418ce18..847c293a6ad80 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -76,7 +76,7 @@ pub mod internal { }; use std::sync::{ mpsc::{self, Receiver, Sender}, - Arc, Mutex, + Arc, }; use alloc::{ @@ -87,7 +87,7 @@ pub mod internal { use bevy_app::{App, First, Startup, Update}; use bevy_ecs::resource::Resource; use bevy_ecs::{prelude::ResMut, system::Commands}; - use bevy_platform::time::Instant; + use bevy_platform::{cell::SyncCell, time::Instant}; use bevy_tasks::AsyncComputeTaskPool; use log::info; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; @@ -110,7 +110,7 @@ pub mod internal { let waker = Arc::clone(&diagnostic_task.waker); AsyncComputeTaskPool::get().spawn(diagnostic_task).detach(); commands.insert_resource(SysinfoTask { - receiver: Mutex::new(rx), + receiver: SyncCell::new(rx), waker, }); @@ -170,7 +170,7 @@ pub mod internal { #[derive(Resource)] struct SysinfoTask { - receiver: Mutex>, + receiver: SyncCell>, waker: Arc, } @@ -219,9 +219,8 @@ pub mod internal { } } - fn read_diagnostic_task(mut diagnostics: Diagnostics, task: ResMut) { - let receiver = task.receiver.lock().unwrap(); - while let Ok(data) = receiver.try_recv() { + fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut) { + while let Ok(data) = task.receiver.get().try_recv() { diagnostics.add_measurement( &SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE, || data.system_cpu_usage, From a970972924bb97b037be31bc0451e43161709ad3 Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Fri, 5 Sep 2025 18:25:33 +0300 Subject: [PATCH 4/6] Use wake directly instead of take and wake --- .../src/system_information_diagnostics_plugin.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 847c293a6ad80..ac39fe07719b5 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -69,7 +69,7 @@ pub struct SystemInfo { not(feature = "dynamic_linking"), feature = "std", ))] -pub mod internal { +mod internal { use core::{ pin::Pin, task::{Context, Poll}, @@ -214,9 +214,7 @@ pub mod internal { } fn wake_diagnostic_task(task: ResMut) { - if let Some(waker) = task.waker.take() { - waker.wake(); - } + task.waker.wake(); } fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut) { @@ -279,7 +277,7 @@ pub mod internal { not(feature = "dynamic_linking"), feature = "std", )))] -pub mod internal { +mod internal { use alloc::string::ToString; use bevy_app::{App, Startup}; From f1157b7f1c7775bc4379a6e9ebebdede52b67379 Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Fri, 5 Sep 2025 20:17:15 +0300 Subject: [PATCH 5/6] Cancel the DiagnosticTask when SysinfoTask drops --- .../src/system_information_diagnostics_plugin.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index ac39fe07719b5..40c014b09237a 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -88,7 +88,7 @@ mod internal { use bevy_ecs::resource::Resource; use bevy_ecs::{prelude::ResMut, system::Commands}; use bevy_platform::{cell::SyncCell, time::Instant}; - use bevy_tasks::AsyncComputeTaskPool; + use bevy_tasks::{AsyncComputeTaskPool, Task}; use log::info; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; @@ -108,8 +108,9 @@ mod internal { let (tx, rx) = mpsc::channel(); let diagnostic_task = DiagnosticTask::new(tx); let waker = Arc::clone(&diagnostic_task.waker); - AsyncComputeTaskPool::get().spawn(diagnostic_task).detach(); + let task = AsyncComputeTaskPool::get().spawn(diagnostic_task); commands.insert_resource(SysinfoTask { + _task: task, receiver: SyncCell::new(rx), waker, }); @@ -170,6 +171,7 @@ mod internal { #[derive(Resource)] struct SysinfoTask { + _task: Task<()>, receiver: SyncCell>, waker: Arc, } From eb3bb55d464b77b32ef317f4f93fecce8f0a1ebf Mon Sep 17 00:00:00 2001 From: Dimitrios Loukadakis Date: Sat, 6 Sep 2025 06:07:22 +0300 Subject: [PATCH 6/6] Add doc to system information diagnostics --- .../system_information_diagnostics_plugin.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 40c014b09237a..0f61d88720160 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -9,12 +9,12 @@ use bevy_ecs::resource::Resource; /// Any system diagnostics gathered by this plugin may not be current when you access them. /// /// Supported targets: -/// * linux, -/// * windows, -/// * android, +/// * linux +/// * windows +/// * android /// * macOS /// -/// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets +/// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets. /// /// # See also /// @@ -98,6 +98,13 @@ mod internal { const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0; + /// Sets up the system information diagnostics plugin. + /// + /// The plugin spawns a single background task in the async task pool that always reschedules. + /// The [`wake_diagnostic_task`] system wakes this task once per frame during the [`First`] + /// schedule. If enough time has passed since the last refresh, it sends [`SysinfoRefreshData`] + /// through a channel. The [`read_diagnostic_task`] system receives this data during the + /// [`Update`] schedule and adds it as diagnostic measurements. pub(super) fn setup_plugin(app: &mut App) { app.add_systems(Startup, setup_system) .add_systems(First, wake_diagnostic_task) @@ -191,6 +198,7 @@ mod internal { .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) .with_memory(MemoryRefreshKind::everything()), ), + // Avoids initial delay on first refresh last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL, sender, waker: Arc::default(), @@ -211,6 +219,7 @@ mod internal { self.sender.send(sysinfo_refresh_data).unwrap(); } + // Always reschedules Poll::Pending } }