bevy_diagnostic/
system_information_diagnostics_plugin.rs

1use crate::DiagnosticPath;
2use alloc::string::String;
3use bevy_app::prelude::*;
4use bevy_ecs::resource::Resource;
5
6/// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %)
7///
8/// Note that gathering system information is a time intensive task and therefore can't be done on every frame.
9/// Any system diagnostics gathered by this plugin may not be current when you access them.
10///
11/// Supported targets:
12/// * linux
13/// * windows
14/// * android
15/// * macOS
16///
17/// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets.
18///
19/// # See also
20///
21/// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console.
22#[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    /// Total system cpu usage in %
32    pub const SYSTEM_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage");
33    /// Total system memory usage in %
34    pub const SYSTEM_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage");
35    /// Process cpu usage in %
36    pub const PROCESS_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/cpu_usage");
37    /// Process memory usage in %
38    pub const PROCESS_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/mem_usage");
39}
40
41/// A resource that stores diagnostic information about the system.
42/// This information can be useful for debugging and profiling purposes.
43///
44/// # See also
45///
46/// [`SystemInformationDiagnosticsPlugin`] for more information.
47#[derive(Debug, Resource)]
48pub struct SystemInfo {
49    /// OS name and version.
50    pub os: String,
51    /// System kernel version.
52    pub kernel: String,
53    /// CPU model name.
54    pub cpu: String,
55    /// Physical core count.
56    pub core_count: String,
57    /// System RAM.
58    pub memory: String,
59}
60
61// NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on Wasm
62#[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    /// Sets up the system information diagnostics plugin.
100    ///
101    /// The plugin spawns a single background task in the async task pool that always reschedules.
102    /// The [`wake_diagnostic_task`] system wakes this task once per frame during the [`First`]
103    /// schedule. If enough time has passed since the last refresh, it sends [`SysinfoRefreshData`]
104    /// through a channel. The [`read_diagnostic_task`] system receives this data during the
105    /// [`Update`] schedule and adds it as diagnostic measurements.
106    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                // Avoids initial delay on first refresh
200                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            // Always reschedules
221            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                // Convert from Bytes to GibiBytes since it's probably what people expect most of the time
270                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}