Path: blob/main/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs
6595 views
use crate::DiagnosticPath;1use alloc::string::String;2use bevy_app::prelude::*;3use bevy_ecs::resource::Resource;45/// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %)6///7/// Note that gathering system information is a time intensive task and therefore can't be done on every frame.8/// Any system diagnostics gathered by this plugin may not be current when you access them.9///10/// Supported targets:11/// * linux12/// * windows13/// * android14/// * macOS15///16/// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets.17///18/// # See also19///20/// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console.21#[derive(Default)]22pub struct SystemInformationDiagnosticsPlugin;23impl Plugin for SystemInformationDiagnosticsPlugin {24fn build(&self, app: &mut App) {25internal::setup_plugin(app);26}27}2829impl SystemInformationDiagnosticsPlugin {30/// Total system cpu usage in %31pub const SYSTEM_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage");32/// Total system memory usage in %33pub const SYSTEM_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage");34/// Process cpu usage in %35pub const PROCESS_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/cpu_usage");36/// Process memory usage in %37pub const PROCESS_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/mem_usage");38}3940/// A resource that stores diagnostic information about the system.41/// This information can be useful for debugging and profiling purposes.42///43/// # See also44///45/// [`SystemInformationDiagnosticsPlugin`] for more information.46#[derive(Debug, Resource)]47pub struct SystemInfo {48/// OS name and version.49pub os: String,50/// System kernel version.51pub kernel: String,52/// CPU model name.53pub cpu: String,54/// Physical core count.55pub core_count: String,56/// System RAM.57pub memory: String,58}5960// NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on Wasm61#[cfg(all(62any(63target_os = "linux",64target_os = "windows",65target_os = "android",66target_os = "macos"67),68not(feature = "dynamic_linking"),69feature = "std",70))]71mod internal {72use core::{73pin::Pin,74task::{Context, Poll},75};76use std::sync::{77mpsc::{self, Receiver, Sender},78Arc,79};8081use alloc::{82format,83string::{String, ToString},84};85use atomic_waker::AtomicWaker;86use bevy_app::{App, First, Startup, Update};87use bevy_ecs::resource::Resource;88use bevy_ecs::{prelude::ResMut, system::Commands};89use bevy_platform::{cell::SyncCell, time::Instant};90use bevy_tasks::{AsyncComputeTaskPool, Task};91use log::info;92use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};9394use crate::{Diagnostic, Diagnostics, DiagnosticsStore};9596use super::{SystemInfo, SystemInformationDiagnosticsPlugin};9798const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;99100/// Sets up the system information diagnostics plugin.101///102/// The plugin spawns a single background task in the async task pool that always reschedules.103/// The [`wake_diagnostic_task`] system wakes this task once per frame during the [`First`]104/// schedule. If enough time has passed since the last refresh, it sends [`SysinfoRefreshData`]105/// through a channel. The [`read_diagnostic_task`] system receives this data during the106/// [`Update`] schedule and adds it as diagnostic measurements.107pub(super) fn setup_plugin(app: &mut App) {108app.add_systems(Startup, setup_system)109.add_systems(First, wake_diagnostic_task)110.add_systems(Update, read_diagnostic_task);111}112113fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>, mut commands: Commands) {114let (tx, rx) = mpsc::channel();115let diagnostic_task = DiagnosticTask::new(tx);116let waker = Arc::clone(&diagnostic_task.waker);117let task = AsyncComputeTaskPool::get().spawn(diagnostic_task);118commands.insert_resource(SysinfoTask {119_task: task,120receiver: SyncCell::new(rx),121waker,122});123124diagnostics.add(125Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"),126);127diagnostics.add(128Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE).with_suffix("%"),129);130diagnostics.add(131Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE).with_suffix("%"),132);133diagnostics.add(134Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE)135.with_suffix("GiB"),136);137}138139struct SysinfoRefreshData {140system_cpu_usage: f64,141system_mem_usage: f64,142process_cpu_usage: f64,143process_mem_usage: f64,144}145146impl SysinfoRefreshData {147fn new(system: &mut System) -> Self {148let pid = sysinfo::get_current_pid().expect("Failed to get current process ID");149system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);150151system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());152system.refresh_memory();153154let system_cpu_usage = system.global_cpu_usage().into();155let total_mem = system.total_memory() as f64;156let used_mem = system.used_memory() as f64;157let system_mem_usage = used_mem / total_mem * 100.0;158159let process_mem_usage = system160.process(pid)161.map(|p| p.memory() as f64 * BYTES_TO_GIB)162.unwrap_or(0.0);163164let process_cpu_usage = system165.process(pid)166.map(|p| p.cpu_usage() as f64 / system.cpus().len() as f64)167.unwrap_or(0.0);168169Self {170system_cpu_usage,171system_mem_usage,172process_cpu_usage,173process_mem_usage,174}175}176}177178#[derive(Resource)]179struct SysinfoTask {180_task: Task<()>,181receiver: SyncCell<Receiver<SysinfoRefreshData>>,182waker: Arc<AtomicWaker>,183}184185struct DiagnosticTask {186system: System,187last_refresh: Instant,188sender: Sender<SysinfoRefreshData>,189waker: Arc<AtomicWaker>,190}191192impl DiagnosticTask {193fn new(sender: Sender<SysinfoRefreshData>) -> Self {194Self {195system: System::new_with_specifics(196RefreshKind::nothing()197.with_cpu(CpuRefreshKind::nothing().with_cpu_usage())198.with_memory(MemoryRefreshKind::everything()),199),200// Avoids initial delay on first refresh201last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL,202sender,203waker: Arc::default(),204}205}206}207208impl Future for DiagnosticTask {209type Output = ();210211fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {212self.waker.register(cx.waker());213214if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL {215self.last_refresh = Instant::now();216217let sysinfo_refresh_data = SysinfoRefreshData::new(&mut self.system);218self.sender.send(sysinfo_refresh_data).unwrap();219}220221// Always reschedules222Poll::Pending223}224}225226fn wake_diagnostic_task(task: ResMut<SysinfoTask>) {227task.waker.wake();228}229230fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut<SysinfoTask>) {231while let Ok(data) = task.receiver.get().try_recv() {232diagnostics.add_measurement(233&SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,234|| data.system_cpu_usage,235);236diagnostics.add_measurement(237&SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,238|| data.system_mem_usage,239);240diagnostics.add_measurement(241&SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,242|| data.process_cpu_usage,243);244diagnostics.add_measurement(245&SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,246|| data.process_mem_usage,247);248}249}250251impl Default for SystemInfo {252fn default() -> Self {253let sys = System::new_with_specifics(254RefreshKind::nothing()255.with_cpu(CpuRefreshKind::nothing())256.with_memory(MemoryRefreshKind::nothing().with_ram()),257);258259let system_info = SystemInfo {260os: System::long_os_version().unwrap_or_else(|| String::from("not available")),261kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")),262cpu: sys263.cpus()264.first()265.map(|cpu| cpu.brand().trim().to_string())266.unwrap_or_else(|| String::from("not available")),267core_count: System::physical_core_count()268.map(|x| x.to_string())269.unwrap_or_else(|| String::from("not available")),270// Convert from Bytes to GibiBytes since it's probably what people expect most of the time271memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB),272};273274info!("{system_info:?}");275system_info276}277}278}279280#[cfg(not(all(281any(282target_os = "linux",283target_os = "windows",284target_os = "android",285target_os = "macos"286),287not(feature = "dynamic_linking"),288feature = "std",289)))]290mod internal {291use alloc::string::ToString;292use bevy_app::{App, Startup};293294pub(super) fn setup_plugin(app: &mut App) {295app.add_systems(Startup, setup_system);296}297298fn setup_system() {299log::warn!("This platform and/or configuration is not supported!");300}301302impl Default for super::SystemInfo {303fn default() -> Self {304let unknown = "Unknown".to_string();305Self {306os: unknown.clone(),307kernel: unknown.clone(),308cpu: unknown.clone(),309core_count: unknown.clone(),310memory: unknown.clone(),311}312}313}314}315316317