1use crate::DiagnosticPath;
2use alloc::string::String;
3use bevy_app::prelude::*;
4use bevy_ecs::resource::Resource;
5
6#[derive(Default)]
23pub struct SystemInformationDiagnosticsPlugin;
24impl Plugin for SystemInformationDiagnosticsPlugin {
25 fn build(&self, app: &mut App) {
26 internal::setup_plugin(app);
27 }
28}
29
30impl SystemInformationDiagnosticsPlugin {
31 pub const SYSTEM_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage");
33 pub const SYSTEM_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage");
35 pub const PROCESS_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/cpu_usage");
37 pub const PROCESS_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/mem_usage");
39}
40
41#[derive(Debug, Resource)]
48pub struct SystemInfo {
49 pub os: String,
51 pub kernel: String,
53 pub cpu: String,
55 pub core_count: String,
57 pub memory: String,
59}
60
61#[cfg(all(
63 any(
64 target_os = "linux",
65 target_os = "windows",
66 target_os = "android",
67 target_os = "macos"
68 ),
69 not(feature = "dynamic_linking"),
70 feature = "std",
71))]
72mod internal {
73 use core::{
74 pin::Pin,
75 task::{Context, Poll},
76 };
77 use std::sync::mpsc::{self, Receiver, Sender};
78
79 use alloc::{
80 format,
81 string::{String, ToString},
82 sync::Arc,
83 };
84 use atomic_waker::AtomicWaker;
85 use bevy_app::{App, First, Startup, Update};
86 use bevy_ecs::resource::Resource;
87 use bevy_ecs::{prelude::ResMut, system::Commands};
88 use bevy_platform::{cell::SyncCell, time::Instant};
89 use bevy_tasks::{AsyncComputeTaskPool, Task};
90 use log::info;
91 use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
92
93 use crate::{Diagnostic, Diagnostics, DiagnosticsStore};
94
95 use super::{SystemInfo, SystemInformationDiagnosticsPlugin};
96
97 const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;
98
99 pub(super) fn setup_plugin(app: &mut App) {
107 app.add_systems(Startup, setup_system)
108 .add_systems(First, wake_diagnostic_task)
109 .add_systems(Update, read_diagnostic_task);
110 }
111
112 fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>, mut commands: Commands) {
113 let (tx, rx) = mpsc::channel();
114 let diagnostic_task = DiagnosticTask::new(tx);
115 let waker = Arc::clone(&diagnostic_task.waker);
116 let task = AsyncComputeTaskPool::get().spawn(diagnostic_task);
117 commands.insert_resource(SysinfoTask {
118 _task: task,
119 receiver: SyncCell::new(rx),
120 waker,
121 });
122
123 diagnostics.add(
124 Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"),
125 );
126 diagnostics.add(
127 Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE).with_suffix("%"),
128 );
129 diagnostics.add(
130 Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE).with_suffix("%"),
131 );
132 diagnostics.add(
133 Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE)
134 .with_suffix("GiB"),
135 );
136 }
137
138 struct SysinfoRefreshData {
139 system_cpu_usage: f64,
140 system_mem_usage: f64,
141 process_cpu_usage: f64,
142 process_mem_usage: f64,
143 }
144
145 impl SysinfoRefreshData {
146 fn new(system: &mut System) -> Self {
147 let pid = sysinfo::get_current_pid().expect("Failed to get current process ID");
148 system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
149
150 system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());
151 system.refresh_memory();
152
153 let system_cpu_usage = system.global_cpu_usage().into();
154 let total_mem = system.total_memory() as f64;
155 let used_mem = system.used_memory() as f64;
156 let system_mem_usage = used_mem / total_mem * 100.0;
157
158 let process_mem_usage = system
159 .process(pid)
160 .map(|p| p.memory() as f64 * BYTES_TO_GIB)
161 .unwrap_or(0.0);
162
163 let process_cpu_usage = system
164 .process(pid)
165 .map(|p| p.cpu_usage() as f64 / system.cpus().len() as f64)
166 .unwrap_or(0.0);
167
168 Self {
169 system_cpu_usage,
170 system_mem_usage,
171 process_cpu_usage,
172 process_mem_usage,
173 }
174 }
175 }
176
177 #[derive(Resource)]
178 struct SysinfoTask {
179 _task: Task<()>,
180 receiver: SyncCell<Receiver<SysinfoRefreshData>>,
181 waker: Arc<AtomicWaker>,
182 }
183
184 struct DiagnosticTask {
185 system: System,
186 last_refresh: Instant,
187 sender: Sender<SysinfoRefreshData>,
188 waker: Arc<AtomicWaker>,
189 }
190
191 impl DiagnosticTask {
192 fn new(sender: Sender<SysinfoRefreshData>) -> Self {
193 Self {
194 system: System::new_with_specifics(
195 RefreshKind::nothing()
196 .with_cpu(CpuRefreshKind::nothing().with_cpu_usage())
197 .with_memory(MemoryRefreshKind::everything()),
198 ),
199 last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL,
201 sender,
202 waker: Arc::default(),
203 }
204 }
205 }
206
207 impl Future for DiagnosticTask {
208 type Output = ();
209
210 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
211 self.waker.register(cx.waker());
212
213 if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL {
214 self.last_refresh = Instant::now();
215
216 let sysinfo_refresh_data = SysinfoRefreshData::new(&mut self.system);
217 self.sender.send(sysinfo_refresh_data).unwrap();
218 }
219
220 Poll::Pending
222 }
223 }
224
225 fn wake_diagnostic_task(task: ResMut<SysinfoTask>) {
226 task.waker.wake();
227 }
228
229 fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut<SysinfoTask>) {
230 while let Ok(data) = task.receiver.get().try_recv() {
231 diagnostics.add_measurement(
232 &SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,
233 || data.system_cpu_usage,
234 );
235 diagnostics.add_measurement(
236 &SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,
237 || data.system_mem_usage,
238 );
239 diagnostics.add_measurement(
240 &SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,
241 || data.process_cpu_usage,
242 );
243 diagnostics.add_measurement(
244 &SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,
245 || data.process_mem_usage,
246 );
247 }
248 }
249
250 impl Default for SystemInfo {
251 fn default() -> Self {
252 let sys = System::new_with_specifics(
253 RefreshKind::nothing()
254 .with_cpu(CpuRefreshKind::nothing())
255 .with_memory(MemoryRefreshKind::nothing().with_ram()),
256 );
257
258 let system_info = SystemInfo {
259 os: System::long_os_version().unwrap_or_else(|| String::from("not available")),
260 kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")),
261 cpu: sys
262 .cpus()
263 .first()
264 .map(|cpu| cpu.brand().trim().to_string())
265 .unwrap_or_else(|| String::from("not available")),
266 core_count: System::physical_core_count()
267 .map(|x| x.to_string())
268 .unwrap_or_else(|| String::from("not available")),
269 memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB),
271 };
272
273 info!("{system_info:?}");
274 system_info
275 }
276 }
277}
278
279#[cfg(not(all(
280 any(
281 target_os = "linux",
282 target_os = "windows",
283 target_os = "android",
284 target_os = "macos"
285 ),
286 not(feature = "dynamic_linking"),
287 feature = "std",
288)))]
289mod internal {
290 use alloc::string::ToString;
291 use bevy_app::{App, Startup};
292
293 pub(super) fn setup_plugin(app: &mut App) {
294 app.add_systems(Startup, setup_system);
295 }
296
297 fn setup_system() {
298 log::warn!("This platform and/or configuration is not supported!");
299 }
300
301 impl Default for super::SystemInfo {
302 fn default() -> Self {
303 let unknown = "Unknown".to_string();
304 Self {
305 os: unknown.clone(),
306 kernel: unknown.clone(),
307 cpu: unknown.clone(),
308 core_count: unknown.clone(),
309 memory: unknown.clone(),
310 }
311 }
312 }
313}