Path: blob/main/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs
9367 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::mpsc::{self, Receiver, Sender};7778use alloc::{79format,80string::{String, ToString},81sync::Arc,82};83use atomic_waker::AtomicWaker;84use bevy_app::{App, First, Startup, Update};85use bevy_ecs::resource::Resource;86use bevy_ecs::{prelude::ResMut, system::Commands};87use bevy_platform::{cell::SyncCell, time::Instant};88use bevy_tasks::{AsyncComputeTaskPool, Task};89use log::info;90use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};9192use crate::{Diagnostic, Diagnostics, DiagnosticsStore};9394use super::{SystemInfo, SystemInformationDiagnosticsPlugin};9596const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;9798/// Sets up the system information diagnostics plugin.99///100/// The plugin spawns a single background task in the async task pool that always reschedules.101/// The [`wake_diagnostic_task`] system wakes this task once per frame during the [`First`]102/// schedule. If enough time has passed since the last refresh, it sends [`SysinfoRefreshData`]103/// through a channel. The [`read_diagnostic_task`] system receives this data during the104/// [`Update`] schedule and adds it as diagnostic measurements.105pub(super) fn setup_plugin(app: &mut App) {106app.add_systems(Startup, setup_system)107.add_systems(First, wake_diagnostic_task)108.add_systems(Update, read_diagnostic_task);109}110111fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>, mut commands: Commands) {112let (tx, rx) = mpsc::channel();113let diagnostic_task = DiagnosticTask::new(tx);114let waker = Arc::clone(&diagnostic_task.waker);115let task = AsyncComputeTaskPool::get().spawn(diagnostic_task);116commands.insert_resource(SysinfoTask {117_task: task,118receiver: SyncCell::new(rx),119waker,120});121122diagnostics.add(123Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"),124);125diagnostics.add(126Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE).with_suffix("%"),127);128diagnostics.add(129Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE).with_suffix("%"),130);131diagnostics.add(132Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE)133.with_suffix("GiB"),134);135}136137struct SysinfoRefreshData {138system_cpu_usage: f64,139system_mem_usage: f64,140process_cpu_usage: f64,141process_mem_usage: f64,142}143144impl SysinfoRefreshData {145fn new(system: &mut System) -> Self {146let pid = sysinfo::get_current_pid().expect("Failed to get current process ID");147system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);148149system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());150system.refresh_memory();151152let system_cpu_usage = system.global_cpu_usage().into();153let total_mem = system.total_memory() as f64;154let used_mem = system.used_memory() as f64;155let system_mem_usage = used_mem / total_mem * 100.0;156157let process_mem_usage = system158.process(pid)159.map(|p| p.memory() as f64 * BYTES_TO_GIB)160.unwrap_or(0.0);161162let process_cpu_usage = system163.process(pid)164.map(|p| p.cpu_usage() as f64 / system.cpus().len() as f64)165.unwrap_or(0.0);166167Self {168system_cpu_usage,169system_mem_usage,170process_cpu_usage,171process_mem_usage,172}173}174}175176#[derive(Resource)]177struct SysinfoTask {178_task: Task<()>,179receiver: SyncCell<Receiver<SysinfoRefreshData>>,180waker: Arc<AtomicWaker>,181}182183struct DiagnosticTask {184system: System,185last_refresh: Instant,186sender: Sender<SysinfoRefreshData>,187waker: Arc<AtomicWaker>,188}189190impl DiagnosticTask {191fn new(sender: Sender<SysinfoRefreshData>) -> Self {192Self {193system: System::new_with_specifics(194RefreshKind::nothing()195.with_cpu(CpuRefreshKind::nothing().with_cpu_usage())196.with_memory(MemoryRefreshKind::everything()),197),198// Avoids initial delay on first refresh199last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL,200sender,201waker: Arc::default(),202}203}204}205206impl Future for DiagnosticTask {207type Output = ();208209fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {210self.waker.register(cx.waker());211212if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL {213self.last_refresh = Instant::now();214215let sysinfo_refresh_data = SysinfoRefreshData::new(&mut self.system);216self.sender.send(sysinfo_refresh_data).unwrap();217}218219// Always reschedules220Poll::Pending221}222}223224fn wake_diagnostic_task(task: ResMut<SysinfoTask>) {225task.waker.wake();226}227228fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut<SysinfoTask>) {229while let Ok(data) = task.receiver.get().try_recv() {230diagnostics.add_measurement(231&SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,232|| data.system_cpu_usage,233);234diagnostics.add_measurement(235&SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,236|| data.system_mem_usage,237);238diagnostics.add_measurement(239&SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,240|| data.process_cpu_usage,241);242diagnostics.add_measurement(243&SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,244|| data.process_mem_usage,245);246}247}248249impl Default for SystemInfo {250fn default() -> Self {251let sys = System::new_with_specifics(252RefreshKind::nothing()253.with_cpu(CpuRefreshKind::nothing())254.with_memory(MemoryRefreshKind::nothing().with_ram()),255);256257let system_info = SystemInfo {258os: System::long_os_version().unwrap_or_else(|| String::from("not available")),259kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")),260cpu: sys261.cpus()262.first()263.map(|cpu| cpu.brand().trim().to_string())264.unwrap_or_else(|| String::from("not available")),265core_count: System::physical_core_count()266.map(|x| x.to_string())267.unwrap_or_else(|| String::from("not available")),268// Convert from Bytes to GibiBytes since it's probably what people expect most of the time269memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB),270};271272info!("{system_info:?}");273system_info274}275}276}277278#[cfg(not(all(279any(280target_os = "linux",281target_os = "windows",282target_os = "android",283target_os = "macos"284),285not(feature = "dynamic_linking"),286feature = "std",287)))]288mod internal {289use alloc::string::ToString;290use bevy_app::{App, Startup};291292pub(super) fn setup_plugin(app: &mut App) {293app.add_systems(Startup, setup_system);294}295296fn setup_system() {297log::warn!("This platform and/or configuration is not supported!");298}299300impl Default for super::SystemInfo {301fn default() -> Self {302let unknown = "Unknown".to_string();303Self {304os: unknown.clone(),305kernel: unknown.clone(),306cpu: unknown.clone(),307core_count: unknown.clone(),308memory: unknown.clone(),309}310}311}312}313314315