use crate::DiagnosticPath;
use alloc::string::String;
use bevy_app::prelude::*;
use bevy_ecs::resource::Resource;
#[derive(Default)]
pub struct SystemInformationDiagnosticsPlugin;
impl Plugin for SystemInformationDiagnosticsPlugin {
fn build(&self, app: &mut App) {
internal::setup_plugin(app);
}
}
impl SystemInformationDiagnosticsPlugin {
pub const SYSTEM_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage");
pub const SYSTEM_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage");
pub const PROCESS_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/cpu_usage");
pub const PROCESS_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/mem_usage");
}
#[derive(Debug, Resource)]
pub struct SystemInfo {
pub os: String,
pub kernel: String,
pub cpu: String,
pub core_count: String,
pub memory: String,
}
#[cfg(all(
any(
target_os = "linux",
target_os = "windows",
target_os = "android",
target_os = "macos"
),
not(feature = "dynamic_linking"),
feature = "std",
))]
mod internal {
use core::{
pin::Pin,
task::{Context, Poll},
};
use std::sync::mpsc::{self, Receiver, Sender};
use alloc::{
format,
string::{String, ToString},
sync::Arc,
};
use atomic_waker::AtomicWaker;
use bevy_app::{App, First, Startup, Update};
use bevy_ecs::resource::Resource;
use bevy_ecs::{prelude::ResMut, system::Commands};
use bevy_platform::{cell::SyncCell, time::Instant};
use bevy_tasks::{AsyncComputeTaskPool, Task};
use log::info;
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
use crate::{Diagnostic, Diagnostics, DiagnosticsStore};
use super::{SystemInfo, SystemInformationDiagnosticsPlugin};
const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;
pub(super) fn setup_plugin(app: &mut App) {
app.add_systems(Startup, setup_system)
.add_systems(First, wake_diagnostic_task)
.add_systems(Update, read_diagnostic_task);
}
fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>, mut commands: Commands) {
let (tx, rx) = mpsc::channel();
let diagnostic_task = DiagnosticTask::new(tx);
let waker = Arc::clone(&diagnostic_task.waker);
let task = AsyncComputeTaskPool::get().spawn(diagnostic_task);
commands.insert_resource(SysinfoTask {
_task: task,
receiver: SyncCell::new(rx),
waker,
});
diagnostics.add(
Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"),
);
diagnostics.add(
Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE).with_suffix("%"),
);
diagnostics.add(
Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE).with_suffix("%"),
);
diagnostics.add(
Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE)
.with_suffix("GiB"),
);
}
struct SysinfoRefreshData {
system_cpu_usage: f64,
system_mem_usage: f64,
process_cpu_usage: f64,
process_mem_usage: f64,
}
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 {
_task: Task<()>,
receiver: SyncCell<Receiver<SysinfoRefreshData>>,
waker: Arc<AtomicWaker>,
}
struct DiagnosticTask {
system: System,
last_refresh: Instant,
sender: Sender<SysinfoRefreshData>,
waker: Arc<AtomicWaker>,
}
impl DiagnosticTask {
fn new(sender: Sender<SysinfoRefreshData>) -> 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(),
}
}
}
impl Future for DiagnosticTask {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.waker.register(cx.waker());
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<SysinfoTask>) {
task.waker.wake();
}
fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut<SysinfoTask>) {
while let Ok(data) = task.receiver.get().try_recv() {
diagnostics.add_measurement(
&SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,
|| data.system_cpu_usage,
);
diagnostics.add_measurement(
&SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,
|| data.system_mem_usage,
);
diagnostics.add_measurement(
&SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,
|| data.process_cpu_usage,
);
diagnostics.add_measurement(
&SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,
|| data.process_mem_usage,
);
}
}
impl Default for SystemInfo {
fn default() -> Self {
let sys = System::new_with_specifics(
RefreshKind::nothing()
.with_cpu(CpuRefreshKind::nothing())
.with_memory(MemoryRefreshKind::nothing().with_ram()),
);
let system_info = SystemInfo {
os: System::long_os_version().unwrap_or_else(|| String::from("not available")),
kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")),
cpu: sys
.cpus()
.first()
.map(|cpu| cpu.brand().trim().to_string())
.unwrap_or_else(|| String::from("not available")),
core_count: System::physical_core_count()
.map(|x| x.to_string())
.unwrap_or_else(|| String::from("not available")),
memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB),
};
info!("{system_info:?}");
system_info
}
}
}
#[cfg(not(all(
any(
target_os = "linux",
target_os = "windows",
target_os = "android",
target_os = "macos"
),
not(feature = "dynamic_linking"),
feature = "std",
)))]
mod internal {
use alloc::string::ToString;
use bevy_app::{App, Startup};
pub(super) fn setup_plugin(app: &mut App) {
app.add_systems(Startup, setup_system);
}
fn setup_system() {
log::warn!("This platform and/or configuration is not supported!");
}
impl Default for super::SystemInfo {
fn default() -> Self {
let unknown = "Unknown".to_string();
Self {
os: unknown.clone(),
kernel: unknown.clone(),
cpu: unknown.clone(),
core_count: unknown.clone(),
memory: unknown.clone(),
}
}
}
}