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
6595 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::{
78
mpsc::{self, Receiver, Sender},
79
Arc,
80
};
81
82
use alloc::{
83
format,
84
string::{String, ToString},
85
};
86
use atomic_waker::AtomicWaker;
87
use bevy_app::{App, First, Startup, Update};
88
use bevy_ecs::resource::Resource;
89
use bevy_ecs::{prelude::ResMut, system::Commands};
90
use bevy_platform::{cell::SyncCell, time::Instant};
91
use bevy_tasks::{AsyncComputeTaskPool, Task};
92
use log::info;
93
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
94
95
use crate::{Diagnostic, Diagnostics, DiagnosticsStore};
96
97
use super::{SystemInfo, SystemInformationDiagnosticsPlugin};
98
99
const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;
100
101
/// Sets up the system information diagnostics plugin.
102
///
103
/// The plugin spawns a single background task in the async task pool that always reschedules.
104
/// The [`wake_diagnostic_task`] system wakes this task once per frame during the [`First`]
105
/// schedule. If enough time has passed since the last refresh, it sends [`SysinfoRefreshData`]
106
/// through a channel. The [`read_diagnostic_task`] system receives this data during the
107
/// [`Update`] schedule and adds it as diagnostic measurements.
108
pub(super) fn setup_plugin(app: &mut App) {
109
app.add_systems(Startup, setup_system)
110
.add_systems(First, wake_diagnostic_task)
111
.add_systems(Update, read_diagnostic_task);
112
}
113
114
fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>, mut commands: Commands) {
115
let (tx, rx) = mpsc::channel();
116
let diagnostic_task = DiagnosticTask::new(tx);
117
let waker = Arc::clone(&diagnostic_task.waker);
118
let task = AsyncComputeTaskPool::get().spawn(diagnostic_task);
119
commands.insert_resource(SysinfoTask {
120
_task: task,
121
receiver: SyncCell::new(rx),
122
waker,
123
});
124
125
diagnostics.add(
126
Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE).with_suffix("%"),
127
);
128
diagnostics.add(
129
Diagnostic::new(SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE).with_suffix("%"),
130
);
131
diagnostics.add(
132
Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE).with_suffix("%"),
133
);
134
diagnostics.add(
135
Diagnostic::new(SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE)
136
.with_suffix("GiB"),
137
);
138
}
139
140
struct SysinfoRefreshData {
141
system_cpu_usage: f64,
142
system_mem_usage: f64,
143
process_cpu_usage: f64,
144
process_mem_usage: f64,
145
}
146
147
impl SysinfoRefreshData {
148
fn new(system: &mut System) -> Self {
149
let pid = sysinfo::get_current_pid().expect("Failed to get current process ID");
150
system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
151
152
system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());
153
system.refresh_memory();
154
155
let system_cpu_usage = system.global_cpu_usage().into();
156
let total_mem = system.total_memory() as f64;
157
let used_mem = system.used_memory() as f64;
158
let system_mem_usage = used_mem / total_mem * 100.0;
159
160
let process_mem_usage = system
161
.process(pid)
162
.map(|p| p.memory() as f64 * BYTES_TO_GIB)
163
.unwrap_or(0.0);
164
165
let process_cpu_usage = system
166
.process(pid)
167
.map(|p| p.cpu_usage() as f64 / system.cpus().len() as f64)
168
.unwrap_or(0.0);
169
170
Self {
171
system_cpu_usage,
172
system_mem_usage,
173
process_cpu_usage,
174
process_mem_usage,
175
}
176
}
177
}
178
179
#[derive(Resource)]
180
struct SysinfoTask {
181
_task: Task<()>,
182
receiver: SyncCell<Receiver<SysinfoRefreshData>>,
183
waker: Arc<AtomicWaker>,
184
}
185
186
struct DiagnosticTask {
187
system: System,
188
last_refresh: Instant,
189
sender: Sender<SysinfoRefreshData>,
190
waker: Arc<AtomicWaker>,
191
}
192
193
impl DiagnosticTask {
194
fn new(sender: Sender<SysinfoRefreshData>) -> Self {
195
Self {
196
system: System::new_with_specifics(
197
RefreshKind::nothing()
198
.with_cpu(CpuRefreshKind::nothing().with_cpu_usage())
199
.with_memory(MemoryRefreshKind::everything()),
200
),
201
// Avoids initial delay on first refresh
202
last_refresh: Instant::now() - sysinfo::MINIMUM_CPU_UPDATE_INTERVAL,
203
sender,
204
waker: Arc::default(),
205
}
206
}
207
}
208
209
impl Future for DiagnosticTask {
210
type Output = ();
211
212
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
213
self.waker.register(cx.waker());
214
215
if self.last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL {
216
self.last_refresh = Instant::now();
217
218
let sysinfo_refresh_data = SysinfoRefreshData::new(&mut self.system);
219
self.sender.send(sysinfo_refresh_data).unwrap();
220
}
221
222
// Always reschedules
223
Poll::Pending
224
}
225
}
226
227
fn wake_diagnostic_task(task: ResMut<SysinfoTask>) {
228
task.waker.wake();
229
}
230
231
fn read_diagnostic_task(mut diagnostics: Diagnostics, mut task: ResMut<SysinfoTask>) {
232
while let Ok(data) = task.receiver.get().try_recv() {
233
diagnostics.add_measurement(
234
&SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE,
235
|| data.system_cpu_usage,
236
);
237
diagnostics.add_measurement(
238
&SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE,
239
|| data.system_mem_usage,
240
);
241
diagnostics.add_measurement(
242
&SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE,
243
|| data.process_cpu_usage,
244
);
245
diagnostics.add_measurement(
246
&SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE,
247
|| data.process_mem_usage,
248
);
249
}
250
}
251
252
impl Default for SystemInfo {
253
fn default() -> Self {
254
let sys = System::new_with_specifics(
255
RefreshKind::nothing()
256
.with_cpu(CpuRefreshKind::nothing())
257
.with_memory(MemoryRefreshKind::nothing().with_ram()),
258
);
259
260
let system_info = SystemInfo {
261
os: System::long_os_version().unwrap_or_else(|| String::from("not available")),
262
kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")),
263
cpu: sys
264
.cpus()
265
.first()
266
.map(|cpu| cpu.brand().trim().to_string())
267
.unwrap_or_else(|| String::from("not available")),
268
core_count: System::physical_core_count()
269
.map(|x| x.to_string())
270
.unwrap_or_else(|| String::from("not available")),
271
// Convert from Bytes to GibiBytes since it's probably what people expect most of the time
272
memory: format!("{:.1} GiB", sys.total_memory() as f64 * BYTES_TO_GIB),
273
};
274
275
info!("{system_info:?}");
276
system_info
277
}
278
}
279
}
280
281
#[cfg(not(all(
282
any(
283
target_os = "linux",
284
target_os = "windows",
285
target_os = "android",
286
target_os = "macos"
287
),
288
not(feature = "dynamic_linking"),
289
feature = "std",
290
)))]
291
mod internal {
292
use alloc::string::ToString;
293
use bevy_app::{App, Startup};
294
295
pub(super) fn setup_plugin(app: &mut App) {
296
app.add_systems(Startup, setup_system);
297
}
298
299
fn setup_system() {
300
log::warn!("This platform and/or configuration is not supported!");
301
}
302
303
impl Default for super::SystemInfo {
304
fn default() -> Self {
305
let unknown = "Unknown".to_string();
306
Self {
307
os: unknown.clone(),
308
kernel: unknown.clone(),
309
cpu: unknown.clone(),
310
core_count: unknown.clone(),
311
memory: unknown.clone(),
312
}
313
}
314
}
315
}
316
317