Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs
9367 views
1
use crate::DiagnosticPath;
2
use alloc::string::String;
3
use bevy_app::prelude::*;
4
use 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)]
23
pub struct SystemInformationDiagnosticsPlugin;
24
impl Plugin for SystemInformationDiagnosticsPlugin {
25
fn build(&self, app: &mut App) {
26
internal::setup_plugin(app);
27
}
28
}
29
30
impl 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)]
48
pub 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
))]
72
mod 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
)))]
289
mod 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
}
314
315