use std::hash::{Hash, Hasher};
use std::{borrow::Cow, collections::VecDeque};
use bevy_app::App;
use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam};
use bevy_utils::{hashbrown::HashMap, Duration, Instant, PassHash};
use const_fnv1a_hash::fnv1a_hash_str_64;
use crate::DEFAULT_MAX_HISTORY_LENGTH;
#[derive(Debug, Clone)]
pub struct DiagnosticPath {
path: Cow<'static, str>,
hash: u64,
}
impl DiagnosticPath {
pub const fn const_new(path: &'static str) -> DiagnosticPath {
DiagnosticPath {
path: Cow::Borrowed(path),
hash: fnv1a_hash_str_64(path),
}
}
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,
}
}
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)
}
pub fn as_str(&self) -> &str {
&self.path
}
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 std::fmt::Display for DiagnosticPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.path.fmt(f)
}
}
#[derive(Debug)]
pub struct DiagnosticMeasurement {
pub time: Instant,
pub value: f64,
}
#[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 {
pub fn add_measurement(&mut self, measurement: DiagnosticMeasurement) {
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() {
self.sum -= removed_diagnostic.value;
}
}
self.sum += measurement.value;
} else {
self.history.clear();
self.sum = measurement.value;
}
self.history.push_back(measurement);
}
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,
}
}
#[must_use]
pub fn with_max_history_length(mut self, max_history_length: usize) -> Self {
self.max_history_length = max_history_length;
self.history.reserve(self.max_history_length);
self.history.shrink_to(self.max_history_length);
self
}
#[must_use]
pub fn with_suffix(mut self, suffix: impl Into<Cow<'static, str>>) -> Self {
self.suffix = suffix.into();
self
}
#[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
}
#[inline]
pub fn measurement(&self) -> Option<&DiagnosticMeasurement> {
self.history.back()
}
pub fn value(&self) -> Option<f64> {
self.measurement().map(|measurement| measurement.value)
}
pub fn average(&self) -> Option<f64> {
if !self.history.is_empty() {
Some(self.sum / self.history.len() as f64)
} else {
None
}
}
pub fn smoothed(&self) -> Option<f64> {
if !self.history.is_empty() {
Some(self.ema)
} else {
None
}
}
pub fn history_len(&self) -> usize {
self.history.len()
}
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
}
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()
}
pub fn clear_history(&mut self) {
self.history.clear();
}
}
#[derive(Debug, Default, Resource)]
pub struct DiagnosticsStore {
diagnostics: HashMap<DiagnosticPath, Diagnostic, PassHash>,
}
impl DiagnosticsStore {
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)
}
pub fn get_measurement(&self, path: &DiagnosticPath) -> Option<&DiagnosticMeasurement> {
self.diagnostics
.get(path)
.filter(|diagnostic| diagnostic.is_enabled)
.and_then(|diagnostic| diagnostic.measurement())
}
pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.values()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Diagnostic> {
self.diagnostics.values_mut()
}
}
#[derive(SystemParam)]
pub struct Diagnostics<'w, 's> {
store: Res<'w, DiagnosticsStore>,
queue: Deferred<'s, DiagnosticsBuffer>,
}
impl<'w, 's> Diagnostics<'w, 's> {
pub fn add_measurement<F>(&mut self, path: &DiagnosticPath, value: F)
where
F: FnOnce() -> f64,
{
if self
.store
.get(path)
.filter(|diagnostic| diagnostic.is_enabled)
.is_some()
{
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);
}
}
}
}
pub trait RegisterDiagnostic {
fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self;
}
impl RegisterDiagnostic for App {
fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self {
self.init_resource::<DiagnosticsStore>();
let mut diagnostics = self.world.resource_mut::<DiagnosticsStore>();
diagnostics.add(diagnostic);
self
}
}