Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

452
vendor/bevy_diagnostic/src/diagnostic.rs vendored Normal file
View File

@@ -0,0 +1,452 @@
use alloc::{borrow::Cow, collections::VecDeque, string::String};
use core::{
hash::{Hash, Hasher},
time::Duration,
};
use bevy_app::{App, SubApp};
use bevy_ecs::resource::Resource;
use bevy_ecs::system::{Deferred, Res, SystemBuffer, SystemParam};
use bevy_platform::{collections::HashMap, hash::PassHash, time::Instant};
use const_fnv1a_hash::fnv1a_hash_str_64;
use crate::DEFAULT_MAX_HISTORY_LENGTH;
/// Unique diagnostic path, separated by `/`.
///
/// Requirements:
/// - Can't be empty
/// - Can't have leading or trailing `/`
/// - Can't have empty components.
#[derive(Debug, Clone)]
pub struct DiagnosticPath {
path: Cow<'static, str>,
hash: u64,
}
impl DiagnosticPath {
/// Create a new `DiagnosticPath`. Usable in const contexts.
///
/// **Note**: path is not validated, so make sure it follows all the requirements.
pub const fn const_new(path: &'static str) -> DiagnosticPath {
DiagnosticPath {
path: Cow::Borrowed(path),
hash: fnv1a_hash_str_64(path),
}
}
/// Create a new `DiagnosticPath` from the specified string.
pub fn new(path: impl Into<Cow<'static, str>>) -> DiagnosticPath {
let path = path.into();
debug_assert!(!path.is_empty(), "diagnostic path can't be empty");
debug_assert!(
!path.starts_with('/'),
"diagnostic path can't be start with `/`"
);
debug_assert!(
!path.ends_with('/'),
"diagnostic path can't be end with `/`"
);
debug_assert!(
!path.contains("//"),
"diagnostic path can't contain empty components"
);
DiagnosticPath {
hash: fnv1a_hash_str_64(&path),
path,
}
}
/// Create a new `DiagnosticPath` from an iterator over components.
pub fn from_components<'a>(components: impl IntoIterator<Item = &'a str>) -> DiagnosticPath {
let mut buf = String::new();
for (i, component) in components.into_iter().enumerate() {
if i > 0 {
buf.push('/');
}
buf.push_str(component);
}
DiagnosticPath::new(buf)
}
/// Returns full path, joined by `/`
pub fn as_str(&self) -> &str {
&self.path
}
/// Returns an iterator over path components.
pub fn components(&self) -> impl Iterator<Item = &str> + '_ {
self.path.split('/')
}
}
impl From<DiagnosticPath> for String {
fn from(path: DiagnosticPath) -> Self {
path.path.into()
}
}
impl Eq for DiagnosticPath {}
impl PartialEq for DiagnosticPath {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash && self.path == other.path
}
}
impl Hash for DiagnosticPath {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.hash);
}
}
impl core::fmt::Display for DiagnosticPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.path.fmt(f)
}
}
/// A single measurement of a [`Diagnostic`].
#[derive(Debug)]
pub struct DiagnosticMeasurement {
pub time: Instant,
pub value: f64,
}
/// A timeline of [`DiagnosticMeasurement`]s of a specific type.
/// Diagnostic examples: frames per second, CPU usage, network latency
#[derive(Debug)]
pub struct Diagnostic {
path: DiagnosticPath,
pub suffix: Cow<'static, str>,
history: VecDeque<DiagnosticMeasurement>,
sum: f64,
ema: f64,
ema_smoothing_factor: f64,
max_history_length: usize,
pub is_enabled: bool,
}
impl Diagnostic {
/// Add a new value as a [`DiagnosticMeasurement`].
pub fn add_measurement(&mut self, measurement: DiagnosticMeasurement) {
if measurement.value.is_nan() {
// Skip calculating the moving average.
} else if let Some(previous) = self.measurement() {
let delta = (measurement.time - previous.time).as_secs_f64();
let alpha = (delta / self.ema_smoothing_factor).clamp(0.0, 1.0);
self.ema += alpha * (measurement.value - self.ema);
} else {
self.ema = measurement.value;
}
if self.max_history_length > 1 {
if self.history.len() >= self.max_history_length {
if let Some(removed_diagnostic) = self.history.pop_front() {
if !removed_diagnostic.value.is_nan() {
self.sum -= removed_diagnostic.value;
}
}
}
if measurement.value.is_finite() {
self.sum += measurement.value;
}
} else {
self.history.clear();
if measurement.value.is_nan() {
self.sum = 0.0;
} else {
self.sum = measurement.value;
}
}
self.history.push_back(measurement);
}
/// Create a new diagnostic with the given path.
pub fn new(path: DiagnosticPath) -> Diagnostic {
Diagnostic {
path,
suffix: Cow::Borrowed(""),
history: VecDeque::with_capacity(DEFAULT_MAX_HISTORY_LENGTH),
max_history_length: DEFAULT_MAX_HISTORY_LENGTH,
sum: 0.0,
ema: 0.0,
ema_smoothing_factor: 2.0 / 21.0,
is_enabled: true,
}
}
/// Set the maximum history length.
#[must_use]
pub fn with_max_history_length(mut self, max_history_length: usize) -> Self {
self.max_history_length = max_history_length;
// reserve/reserve_exact reserve space for n *additional* elements.
let expected_capacity = self
.max_history_length
.saturating_sub(self.history.capacity());
self.history.reserve_exact(expected_capacity);
self.history.shrink_to(expected_capacity);
self
}
/// Add a suffix to use when logging the value, can be used to show a unit.
#[must_use]
pub fn with_suffix(mut self, suffix: impl Into<Cow<'static, str>>) -> Self {
self.suffix = suffix.into();
self
}
/// The smoothing factor used for the exponential smoothing used for
/// [`smoothed`](Self::smoothed).
///
/// If measurements come in less frequently than `smoothing_factor` seconds
/// apart, no smoothing will be applied. As measurements come in more
/// frequently, the smoothing takes a greater effect such that it takes
/// approximately `smoothing_factor` seconds for 83% of an instantaneous
/// change in measurement to e reflected in the smoothed value.
///
/// A smoothing factor of 0.0 will effectively disable smoothing.
#[must_use]
pub fn with_smoothing_factor(mut self, smoothing_factor: f64) -> Self {
self.ema_smoothing_factor = smoothing_factor;
self
}
pub fn path(&self) -> &DiagnosticPath {
&self.path
}
/// Get the latest measurement from this diagnostic.
#[inline]
pub fn measurement(&self) -> Option<&DiagnosticMeasurement> {
self.history.back()
}
/// Get the latest value from this diagnostic.
pub fn value(&self) -> Option<f64> {
self.measurement().map(|measurement| measurement.value)
}
/// Return the simple moving average of this diagnostic's recent values.
/// N.B. this a cheap operation as the sum is cached.
pub fn average(&self) -> Option<f64> {
if !self.history.is_empty() {
Some(self.sum / self.history.len() as f64)
} else {
None
}
}
/// Return the exponential moving average of this diagnostic.
///
/// This is by default tuned to behave reasonably well for a typical
/// measurement that changes every frame such as frametime. This can be
/// adjusted using [`with_smoothing_factor`](Self::with_smoothing_factor).
pub fn smoothed(&self) -> Option<f64> {
if !self.history.is_empty() {
Some(self.ema)
} else {
None
}
}
/// Return the number of elements for this diagnostic.
pub fn history_len(&self) -> usize {
self.history.len()
}
/// Return the duration between the oldest and most recent values for this diagnostic.
pub fn duration(&self) -> Option<Duration> {
if self.history.len() < 2 {
return None;
}
if let Some(newest) = self.history.back() {
if let Some(oldest) = self.history.front() {
return Some(newest.time.duration_since(oldest.time));
}
}
None
}
/// Return the maximum number of elements for this diagnostic.
pub fn get_max_history_length(&self) -> usize {
self.max_history_length
}
pub fn values(&self) -> impl Iterator<Item = &f64> {
self.history.iter().map(|x| &x.value)
}
pub fn measurements(&self) -> impl Iterator<Item = &DiagnosticMeasurement> {
self.history.iter()
}
/// Clear the history of this diagnostic.
pub fn clear_history(&mut self) {
self.history.clear();
self.sum = 0.0;
self.ema = 0.0;
}
}
/// A collection of [`Diagnostic`]s.
#[derive(Debug, Default, Resource)]
pub struct DiagnosticsStore {
diagnostics: HashMap<DiagnosticPath, Diagnostic, PassHash>,
}
impl DiagnosticsStore {
/// Add a new [`Diagnostic`].
///
/// If possible, prefer calling [`App::register_diagnostic`].
pub fn add(&mut self, diagnostic: Diagnostic) {
self.diagnostics.insert(diagnostic.path.clone(), diagnostic);
}
pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> {
self.diagnostics.get(path)
}
pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> {
self.diagnostics.get_mut(path)
}
/// Get the latest [`DiagnosticMeasurement`] from an enabled [`Diagnostic`].
pub fn get_measurement(&self, path: &DiagnosticPath) -> Option<&DiagnosticMeasurement> {
self.diagnostics
.get(path)
.filter(|diagnostic| diagnostic.is_enabled)
.and_then(|diagnostic| diagnostic.measurement())
}
/// Return an iterator over all [`Diagnostic`]s.
pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.values()
}
/// Return an iterator over all [`Diagnostic`]s, by mutable reference.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Diagnostic> {
self.diagnostics.values_mut()
}
}
/// Record new [`DiagnosticMeasurement`]'s.
#[derive(SystemParam)]
pub struct Diagnostics<'w, 's> {
store: Res<'w, DiagnosticsStore>,
queue: Deferred<'s, DiagnosticsBuffer>,
}
impl<'w, 's> Diagnostics<'w, 's> {
/// Add a measurement to an enabled [`Diagnostic`]. The measurement is passed as a function so that
/// it will be evaluated only if the [`Diagnostic`] is enabled. This can be useful if the value is
/// costly to calculate.
pub fn add_measurement<F>(&mut self, path: &DiagnosticPath, value: F)
where
F: FnOnce() -> f64,
{
if self
.store
.get(path)
.is_some_and(|diagnostic| diagnostic.is_enabled)
{
let measurement = DiagnosticMeasurement {
time: Instant::now(),
value: value(),
};
self.queue.0.insert(path.clone(), measurement);
}
}
}
#[derive(Default)]
struct DiagnosticsBuffer(HashMap<DiagnosticPath, DiagnosticMeasurement, PassHash>);
impl SystemBuffer for DiagnosticsBuffer {
fn apply(
&mut self,
_system_meta: &bevy_ecs::system::SystemMeta,
world: &mut bevy_ecs::world::World,
) {
let mut diagnostics = world.resource_mut::<DiagnosticsStore>();
for (path, measurement) in self.0.drain() {
if let Some(diagnostic) = diagnostics.get_mut(&path) {
diagnostic.add_measurement(measurement);
}
}
}
}
/// Extend [`App`] with new `register_diagnostic` function.
pub trait RegisterDiagnostic {
/// Register a new [`Diagnostic`] with an [`App`].
///
/// Will initialize a [`DiagnosticsStore`] if it doesn't exist.
///
/// ```
/// use bevy_app::App;
/// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticPath, RegisterDiagnostic};
///
/// const UNIQUE_DIAG_PATH: DiagnosticPath = DiagnosticPath::const_new("foo/bar");
///
/// App::new()
/// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_PATH))
/// .add_plugins(DiagnosticsPlugin)
/// .run();
/// ```
fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self;
}
impl RegisterDiagnostic for SubApp {
fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self {
self.init_resource::<DiagnosticsStore>();
let mut diagnostics = self.world_mut().resource_mut::<DiagnosticsStore>();
diagnostics.add(diagnostic);
self
}
}
impl RegisterDiagnostic for App {
fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self {
SubApp::register_diagnostic(self.main_mut(), diagnostic);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clear_history() {
const MEASUREMENT: f64 = 20.0;
let mut diagnostic =
Diagnostic::new(DiagnosticPath::new("test")).with_max_history_length(5);
let mut now = Instant::now();
for _ in 0..3 {
for _ in 0..5 {
diagnostic.add_measurement(DiagnosticMeasurement {
time: now,
value: MEASUREMENT,
});
// Increase time to test smoothed average.
now += Duration::from_secs(1);
}
assert!((diagnostic.average().unwrap() - MEASUREMENT).abs() < 0.1);
assert!((diagnostic.smoothed().unwrap() - MEASUREMENT).abs() < 0.1);
diagnostic.clear_history();
}
}
}

View File

@@ -0,0 +1,27 @@
use bevy_app::prelude::*;
use bevy_ecs::entity::Entities;
use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
/// Adds "entity count" diagnostic to an App.
///
/// # See also
///
/// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console.
#[derive(Default)]
pub struct EntityCountDiagnosticsPlugin;
impl Plugin for EntityCountDiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT))
.add_systems(Update, Self::diagnostic_system);
}
}
impl EntityCountDiagnosticsPlugin {
pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count");
pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) {
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64);
}
}

View File

@@ -0,0 +1,101 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
#[cfg(feature = "serialize")]
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
/// Maintains a count of frames rendered since the start of the application.
///
/// [`FrameCount`] is incremented during [`Last`], providing predictable
/// behavior: it will be 0 during the first update, 1 during the next, and so forth.
///
/// # Overflows
///
/// [`FrameCount`] will wrap to 0 after exceeding [`u32::MAX`]. Within reasonable
/// assumptions, one may exploit wrapping arithmetic to determine the number of frames
/// that have elapsed between two observations see [`u32::wrapping_sub()`].
#[derive(Debug, Default, Resource, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FrameCount(pub u32);
/// Adds frame counting functionality to Apps.
#[derive(Default)]
pub struct FrameCountPlugin;
impl Plugin for FrameCountPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<FrameCount>();
app.add_systems(Last, update_frame_count);
}
}
/// A system used to increment [`FrameCount`] with wrapping addition.
///
/// See [`FrameCount`] for more details.
pub fn update_frame_count(mut frame_count: ResMut<FrameCount>) {
frame_count.0 = frame_count.0.wrapping_add(1);
}
#[cfg(feature = "serialize")]
// Manually implementing serialize/deserialize allows us to use a more compact representation as simple integers
impl Serialize for FrameCount {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u32(self.0)
}
}
#[cfg(feature = "serialize")]
impl<'de> Deserialize<'de> for FrameCount {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_u32(FrameVisitor)
}
}
#[cfg(feature = "serialize")]
struct FrameVisitor;
#[cfg(feature = "serialize")]
impl<'de> Visitor<'de> for FrameVisitor {
type Value = FrameCount;
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str(core::any::type_name::<FrameCount>())
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: Error,
{
Ok(FrameCount(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_counter_update() {
let mut app = App::new();
app.add_plugins(FrameCountPlugin);
app.update();
let frame_count = app.world().resource::<FrameCount>();
assert_eq!(1, frame_count.0);
}
}
#[cfg(all(test, feature = "serialize"))]
mod serde_tests {
use super::*;
use serde_test::{assert_tokens, Token};
#[test]
fn test_serde_frame_count() {
let frame_count = FrameCount(100);
assert_tokens(&frame_count, &[Token::U32(100)]);
}
}

View File

@@ -0,0 +1,81 @@
use crate::{
Diagnostic, DiagnosticPath, Diagnostics, FrameCount, RegisterDiagnostic,
DEFAULT_MAX_HISTORY_LENGTH,
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_time::{Real, Time};
/// Adds "frame time" diagnostic to an App, specifically "frame time", "fps" and "frame count"
///
/// # See also
///
/// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console.
pub struct FrameTimeDiagnosticsPlugin {
/// The total number of values to keep for averaging.
pub max_history_length: usize,
/// The smoothing factor for the exponential moving average. Usually `2.0 / (history_length + 1.0)`.
pub smoothing_factor: f64,
}
impl Default for FrameTimeDiagnosticsPlugin {
fn default() -> Self {
Self::new(DEFAULT_MAX_HISTORY_LENGTH)
}
}
impl FrameTimeDiagnosticsPlugin {
/// Creates a new `FrameTimeDiagnosticsPlugin` with the specified `max_history_length` and a
/// reasonable `smoothing_factor`.
pub fn new(max_history_length: usize) -> Self {
Self {
max_history_length,
smoothing_factor: 2.0 / (max_history_length as f64 + 1.0),
}
}
}
impl Plugin for FrameTimeDiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.register_diagnostic(
Diagnostic::new(Self::FRAME_TIME)
.with_suffix("ms")
.with_max_history_length(self.max_history_length)
.with_smoothing_factor(self.smoothing_factor),
)
.register_diagnostic(
Diagnostic::new(Self::FPS)
.with_max_history_length(self.max_history_length)
.with_smoothing_factor(self.smoothing_factor),
)
// An average frame count would be nonsensical, so we set the max history length
// to zero and disable smoothing.
.register_diagnostic(
Diagnostic::new(Self::FRAME_COUNT)
.with_smoothing_factor(0.0)
.with_max_history_length(0),
)
.add_systems(Update, Self::diagnostic_system);
}
}
impl FrameTimeDiagnosticsPlugin {
pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps");
pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count");
pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time");
pub fn diagnostic_system(
mut diagnostics: Diagnostics,
time: Res<Time<Real>>,
frame_count: Res<FrameCount>,
) {
diagnostics.add_measurement(&Self::FRAME_COUNT, || frame_count.0 as f64);
let delta_seconds = time.delta_secs_f64();
if delta_seconds == 0.0 {
return;
}
diagnostics.add_measurement(&Self::FRAME_TIME, || delta_seconds * 1000.0);
diagnostics.add_measurement(&Self::FPS, || 1.0 / delta_seconds);
}
}

52
vendor/bevy_diagnostic/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,52 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
#![no_std]
//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/).
//! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing
//! their ability to monitor and optimize their game's.
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
mod diagnostic;
mod entity_count_diagnostics_plugin;
mod frame_count_diagnostics_plugin;
mod frame_time_diagnostics_plugin;
mod log_diagnostics_plugin;
#[cfg(feature = "sysinfo_plugin")]
mod system_information_diagnostics_plugin;
pub use diagnostic::*;
pub use entity_count_diagnostics_plugin::EntityCountDiagnosticsPlugin;
pub use frame_count_diagnostics_plugin::{update_frame_count, FrameCount, FrameCountPlugin};
pub use frame_time_diagnostics_plugin::FrameTimeDiagnosticsPlugin;
pub use log_diagnostics_plugin::LogDiagnosticsPlugin;
#[cfg(feature = "sysinfo_plugin")]
pub use system_information_diagnostics_plugin::{SystemInfo, SystemInformationDiagnosticsPlugin};
use bevy_app::prelude::*;
/// Adds core diagnostics resources to an App.
#[derive(Default)]
pub struct DiagnosticsPlugin;
impl Plugin for DiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DiagnosticsStore>();
#[cfg(feature = "sysinfo_plugin")]
app.init_resource::<SystemInfo>();
}
}
/// Default max history length for new diagnostics.
pub const DEFAULT_MAX_HISTORY_LENGTH: usize = 120;

View File

@@ -0,0 +1,147 @@
use super::{Diagnostic, DiagnosticPath, DiagnosticsStore};
use alloc::vec::Vec;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_time::{Real, Time, Timer, TimerMode};
use core::time::Duration;
use log::{debug, info};
/// An App Plugin that logs diagnostics to the console.
///
/// Diagnostics are collected by plugins such as
/// [`FrameTimeDiagnosticsPlugin`](crate::FrameTimeDiagnosticsPlugin)
/// or can be provided by the user.
///
/// When no diagnostics are provided, this plugin does nothing.
pub struct LogDiagnosticsPlugin {
pub debug: bool,
pub wait_duration: Duration,
pub filter: Option<Vec<DiagnosticPath>>,
}
/// State used by the [`LogDiagnosticsPlugin`]
#[derive(Resource)]
struct LogDiagnosticsState {
timer: Timer,
filter: Option<Vec<DiagnosticPath>>,
}
impl Default for LogDiagnosticsPlugin {
fn default() -> Self {
LogDiagnosticsPlugin {
debug: false,
wait_duration: Duration::from_secs(1),
filter: None,
}
}
}
impl Plugin for LogDiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(LogDiagnosticsState {
timer: Timer::new(self.wait_duration, TimerMode::Repeating),
filter: self.filter.clone(),
});
if self.debug {
app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);
} else {
app.add_systems(PostUpdate, Self::log_diagnostics_system);
}
}
}
impl LogDiagnosticsPlugin {
pub fn filtered(filter: Vec<DiagnosticPath>) -> Self {
LogDiagnosticsPlugin {
filter: Some(filter),
..Default::default()
}
}
fn for_each_diagnostic(
state: &LogDiagnosticsState,
diagnostics: &DiagnosticsStore,
mut callback: impl FnMut(&Diagnostic),
) {
if let Some(filter) = &state.filter {
for path in filter {
if let Some(diagnostic) = diagnostics.get(path) {
if diagnostic.is_enabled {
callback(diagnostic);
}
}
}
} else {
for diagnostic in diagnostics.iter() {
if diagnostic.is_enabled {
callback(diagnostic);
}
}
}
}
fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) {
let Some(value) = diagnostic.smoothed() else {
return;
};
if diagnostic.get_max_history_length() > 1 {
let Some(average) = diagnostic.average() else {
return;
};
info!(
target: "bevy_diagnostic",
// Suffix is only used for 's' or 'ms' currently,
// so we reserve two columns for it; however,
// Do not reserve columns for the suffix in the average
// The ) hugging the value is more aesthetically pleasing
"{path:<path_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
path = diagnostic.path(),
suffix = diagnostic.suffix,
);
} else {
info!(
target: "bevy_diagnostic",
"{path:<path_width$}: {value:>.6}{suffix:}",
path = diagnostic.path(),
suffix = diagnostic.suffix,
);
}
}
fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) {
let mut path_width = 0;
Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
let width = diagnostic.path().as_str().len();
path_width = path_width.max(width);
});
Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
Self::log_diagnostic(path_width, diagnostic);
});
}
fn log_diagnostics_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.delta()).finished() {
Self::log_diagnostics(&state, &diagnostics);
}
}
fn log_diagnostics_debug_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.delta()).finished() {
Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| {
debug!("{:#?}\n", diagnostic);
});
}
}
}

View File

@@ -0,0 +1,274 @@
use crate::DiagnosticPath;
use alloc::string::String;
use bevy_app::prelude::*;
use bevy_ecs::resource::Resource;
/// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %)
///
/// Note that gathering system information is a time intensive task and therefore can't be done on every frame.
/// Any system diagnostics gathered by this plugin may not be current when you access them.
///
/// Supported targets:
/// * linux,
/// * windows,
/// * android,
/// * macOS
///
/// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets
///
/// # See also
///
/// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console.
#[derive(Default)]
pub struct SystemInformationDiagnosticsPlugin;
impl Plugin for SystemInformationDiagnosticsPlugin {
fn build(&self, app: &mut App) {
internal::setup_plugin(app);
}
}
impl SystemInformationDiagnosticsPlugin {
/// Total system cpu usage in %
pub const SYSTEM_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage");
/// Total system memory usage in %
pub const SYSTEM_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage");
/// Process cpu usage in %
pub const PROCESS_CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/cpu_usage");
/// Process memory usage in %
pub const PROCESS_MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("process/mem_usage");
}
/// A resource that stores diagnostic information about the system.
/// This information can be useful for debugging and profiling purposes.
///
/// # See also
///
/// [`SystemInformationDiagnosticsPlugin`] for more information.
#[derive(Debug, Resource)]
pub struct SystemInfo {
pub os: String,
pub kernel: String,
pub cpu: String,
pub core_count: String,
pub memory: String,
}
// NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on Wasm
#[cfg(all(
any(
target_os = "linux",
target_os = "windows",
target_os = "android",
target_os = "macos"
),
not(feature = "dynamic_linking"),
feature = "std",
))]
pub mod internal {
use alloc::{
format,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use bevy_app::{App, First, Startup, Update};
use bevy_ecs::resource::Resource;
use bevy_ecs::{prelude::ResMut, system::Local};
use bevy_platform::time::Instant;
use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task};
use log::info;
use std::sync::Mutex;
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, launch_diagnostic_tasks)
.add_systems(Update, read_diagnostic_tasks)
.init_resource::<SysinfoTasks>();
}
fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
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,
}
#[derive(Resource, Default)]
struct SysinfoTasks {
tasks: Vec<Task<SysinfoRefreshData>>,
}
fn launch_diagnostic_tasks(
mut tasks: ResMut<SysinfoTasks>,
// TODO: Consider a fair mutex
mut sysinfo: Local<Option<Arc<Mutex<System>>>>,
// TODO: FromWorld for Instant?
mut last_refresh: Local<Option<Instant>>,
) {
let sysinfo = sysinfo.get_or_insert_with(|| {
Arc::new(Mutex::new(System::new_with_specifics(
RefreshKind::nothing()
.with_cpu(CpuRefreshKind::nothing().with_cpu_usage())
.with_memory(MemoryRefreshKind::everything()),
)))
});
let last_refresh = last_refresh.get_or_insert_with(Instant::now);
let thread_pool = AsyncComputeTaskPool::get();
// Only queue a new system refresh task when necessary
// Queuing earlier than that will not give new data
if last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL
// These tasks don't yield and will take up all of the task pool's
// threads if we don't limit their amount.
&& tasks.tasks.len() * 2 < available_parallelism()
{
let sys = Arc::clone(sysinfo);
let task = thread_pool.spawn(async move {
let mut sys = sys.lock().unwrap();
let pid = sysinfo::get_current_pid().expect("Failed to get current process ID");
sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
sys.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());
sys.refresh_memory();
let system_cpu_usage = sys.global_cpu_usage().into();
let total_mem = sys.total_memory() as f64;
let used_mem = sys.used_memory() as f64;
let system_mem_usage = used_mem / total_mem * 100.0;
let process_mem_usage = sys
.process(pid)
.map(|p| p.memory() as f64 * BYTES_TO_GIB)
.unwrap_or(0.0);
let process_cpu_usage = sys
.process(pid)
.map(|p| p.cpu_usage() as f64 / sys.cpus().len() as f64)
.unwrap_or(0.0);
SysinfoRefreshData {
system_cpu_usage,
system_mem_usage,
process_cpu_usage,
process_mem_usage,
}
});
tasks.tasks.push(task);
*last_refresh = Instant::now();
}
}
fn read_diagnostic_tasks(mut diagnostics: Diagnostics, mut tasks: ResMut<SysinfoTasks>) {
tasks.tasks.retain_mut(|task| {
let Some(data) = block_on(poll_once(task)) else {
return true;
};
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,
);
false
});
}
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")),
// Convert from Bytes to GibiBytes since it's probably what people expect most of the time
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",
)))]
pub 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(),
}
}
}
}