1use alloc::{borrow::Cow, collections::VecDeque, string::String};
2use core::{
3 hash::{Hash, Hasher},
4 time::Duration,
5};
6
7use bevy_app::{App, SubApp};
8use bevy_ecs::resource::Resource;
9use bevy_ecs::system::{Deferred, Res, SystemBuffer, SystemParam};
10use bevy_platform::{collections::HashMap, hash::PassHash, time::Instant};
11use const_fnv1a_hash::fnv1a_hash_str_64;
12
13use crate::DEFAULT_MAX_HISTORY_LENGTH;
14
15#[derive(Debug, Clone)]
22pub struct DiagnosticPath {
23 path: Cow<'static, str>,
24 hash: u64,
25}
26
27impl DiagnosticPath {
28 pub const fn const_new(path: &'static str) -> DiagnosticPath {
32 DiagnosticPath {
33 path: Cow::Borrowed(path),
34 hash: fnv1a_hash_str_64(path),
35 }
36 }
37
38 pub fn new(path: impl Into<Cow<'static, str>>) -> DiagnosticPath {
40 let path = path.into();
41
42 debug_assert!(!path.is_empty(), "diagnostic path can't be empty");
43 debug_assert!(
44 !path.starts_with('/'),
45 "diagnostic path can't be start with `/`"
46 );
47 debug_assert!(
48 !path.ends_with('/'),
49 "diagnostic path can't be end with `/`"
50 );
51 debug_assert!(
52 !path.contains("//"),
53 "diagnostic path can't contain empty components"
54 );
55
56 DiagnosticPath {
57 hash: fnv1a_hash_str_64(&path),
58 path,
59 }
60 }
61
62 pub fn from_components<'a>(components: impl IntoIterator<Item = &'a str>) -> DiagnosticPath {
64 let mut buf = String::new();
65
66 for (i, component) in components.into_iter().enumerate() {
67 if i > 0 {
68 buf.push('/');
69 }
70 buf.push_str(component);
71 }
72
73 DiagnosticPath::new(buf)
74 }
75
76 pub fn as_str(&self) -> &str {
78 &self.path
79 }
80
81 pub fn components(&self) -> impl Iterator<Item = &str> + '_ {
83 self.path.split('/')
84 }
85}
86
87impl From<DiagnosticPath> for String {
88 fn from(path: DiagnosticPath) -> Self {
89 path.path.into()
90 }
91}
92
93impl Eq for DiagnosticPath {}
94
95impl PartialEq for DiagnosticPath {
96 fn eq(&self, other: &Self) -> bool {
97 self.hash == other.hash && self.path == other.path
98 }
99}
100
101impl Hash for DiagnosticPath {
102 fn hash<H: Hasher>(&self, state: &mut H) {
103 state.write_u64(self.hash);
104 }
105}
106
107impl core::fmt::Display for DiagnosticPath {
108 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109 self.path.fmt(f)
110 }
111}
112
113#[derive(Debug)]
115pub struct DiagnosticMeasurement {
116 pub time: Instant,
117 pub value: f64,
118}
119
120#[derive(Debug)]
123pub struct Diagnostic {
124 path: DiagnosticPath,
125 pub suffix: Cow<'static, str>,
126 history: VecDeque<DiagnosticMeasurement>,
127 sum: f64,
128 ema: f64,
129 ema_smoothing_factor: f64,
130 max_history_length: usize,
131 pub is_enabled: bool,
132}
133
134impl Diagnostic {
135 pub fn add_measurement(&mut self, measurement: DiagnosticMeasurement) {
137 if measurement.value.is_nan() {
138 } else if let Some(previous) = self.measurement() {
140 let delta = (measurement.time - previous.time).as_secs_f64();
141 let alpha = (delta / self.ema_smoothing_factor).clamp(0.0, 1.0);
142 self.ema += alpha * (measurement.value - self.ema);
143 } else {
144 self.ema = measurement.value;
145 }
146
147 if self.max_history_length > 1 {
148 if self.history.len() >= self.max_history_length {
149 if let Some(removed_diagnostic) = self.history.pop_front() {
150 if !removed_diagnostic.value.is_nan() {
151 self.sum -= removed_diagnostic.value;
152 }
153 }
154 }
155
156 if measurement.value.is_finite() {
157 self.sum += measurement.value;
158 }
159 } else {
160 self.history.clear();
161 if measurement.value.is_nan() {
162 self.sum = 0.0;
163 } else {
164 self.sum = measurement.value;
165 }
166 }
167
168 self.history.push_back(measurement);
169 }
170
171 pub fn new(path: DiagnosticPath) -> Diagnostic {
173 Diagnostic {
174 path,
175 suffix: Cow::Borrowed(""),
176 history: VecDeque::with_capacity(DEFAULT_MAX_HISTORY_LENGTH),
177 max_history_length: DEFAULT_MAX_HISTORY_LENGTH,
178 sum: 0.0,
179 ema: 0.0,
180 ema_smoothing_factor: 2.0 / 21.0,
181 is_enabled: true,
182 }
183 }
184
185 #[must_use]
187 pub fn with_max_history_length(mut self, max_history_length: usize) -> Self {
188 self.max_history_length = max_history_length;
189
190 let expected_capacity = self
192 .max_history_length
193 .saturating_sub(self.history.capacity());
194 self.history.reserve_exact(expected_capacity);
195 self.history.shrink_to(expected_capacity);
196 self
197 }
198
199 #[must_use]
201 pub fn with_suffix(mut self, suffix: impl Into<Cow<'static, str>>) -> Self {
202 self.suffix = suffix.into();
203 self
204 }
205
206 #[must_use]
217 pub fn with_smoothing_factor(mut self, smoothing_factor: f64) -> Self {
218 self.ema_smoothing_factor = smoothing_factor;
219 self
220 }
221
222 pub fn path(&self) -> &DiagnosticPath {
223 &self.path
224 }
225
226 #[inline]
228 pub fn measurement(&self) -> Option<&DiagnosticMeasurement> {
229 self.history.back()
230 }
231
232 pub fn value(&self) -> Option<f64> {
234 self.measurement().map(|measurement| measurement.value)
235 }
236
237 pub fn average(&self) -> Option<f64> {
240 if !self.history.is_empty() {
241 Some(self.sum / self.history.len() as f64)
242 } else {
243 None
244 }
245 }
246
247 pub fn smoothed(&self) -> Option<f64> {
253 if !self.history.is_empty() {
254 Some(self.ema)
255 } else {
256 None
257 }
258 }
259
260 pub fn history_len(&self) -> usize {
262 self.history.len()
263 }
264
265 pub fn duration(&self) -> Option<Duration> {
267 if self.history.len() < 2 {
268 return None;
269 }
270
271 if let Some(newest) = self.history.back() {
272 if let Some(oldest) = self.history.front() {
273 return Some(newest.time.duration_since(oldest.time));
274 }
275 }
276
277 None
278 }
279
280 pub fn get_max_history_length(&self) -> usize {
282 self.max_history_length
283 }
284
285 pub fn values(&self) -> impl Iterator<Item = &f64> {
286 self.history.iter().map(|x| &x.value)
287 }
288
289 pub fn measurements(&self) -> impl Iterator<Item = &DiagnosticMeasurement> {
290 self.history.iter()
291 }
292
293 pub fn clear_history(&mut self) {
295 self.history.clear();
296 }
297}
298
299#[derive(Debug, Default, Resource)]
301pub struct DiagnosticsStore {
302 diagnostics: HashMap<DiagnosticPath, Diagnostic, PassHash>,
303}
304
305impl DiagnosticsStore {
306 pub fn add(&mut self, diagnostic: Diagnostic) {
310 self.diagnostics.insert(diagnostic.path.clone(), diagnostic);
311 }
312
313 pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> {
314 self.diagnostics.get(path)
315 }
316
317 pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> {
318 self.diagnostics.get_mut(path)
319 }
320
321 pub fn get_measurement(&self, path: &DiagnosticPath) -> Option<&DiagnosticMeasurement> {
323 self.diagnostics
324 .get(path)
325 .filter(|diagnostic| diagnostic.is_enabled)
326 .and_then(|diagnostic| diagnostic.measurement())
327 }
328
329 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
331 self.diagnostics.values()
332 }
333
334 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Diagnostic> {
336 self.diagnostics.values_mut()
337 }
338}
339
340#[derive(SystemParam)]
342pub struct Diagnostics<'w, 's> {
343 store: Res<'w, DiagnosticsStore>,
344 queue: Deferred<'s, DiagnosticsBuffer>,
345}
346
347impl<'w, 's> Diagnostics<'w, 's> {
348 pub fn add_measurement<F>(&mut self, path: &DiagnosticPath, value: F)
352 where
353 F: FnOnce() -> f64,
354 {
355 if self
356 .store
357 .get(path)
358 .is_some_and(|diagnostic| diagnostic.is_enabled)
359 {
360 let measurement = DiagnosticMeasurement {
361 time: Instant::now(),
362 value: value(),
363 };
364 self.queue.0.insert(path.clone(), measurement);
365 }
366 }
367}
368
369#[derive(Default)]
370struct DiagnosticsBuffer(HashMap<DiagnosticPath, DiagnosticMeasurement, PassHash>);
371
372impl SystemBuffer for DiagnosticsBuffer {
373 fn apply(
374 &mut self,
375 _system_meta: &bevy_ecs::system::SystemMeta,
376 world: &mut bevy_ecs::world::World,
377 ) {
378 let mut diagnostics = world.resource_mut::<DiagnosticsStore>();
379 for (path, measurement) in self.0.drain() {
380 if let Some(diagnostic) = diagnostics.get_mut(&path) {
381 diagnostic.add_measurement(measurement);
382 }
383 }
384 }
385}
386
387pub trait RegisterDiagnostic {
389 fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self;
405}
406
407impl RegisterDiagnostic for SubApp {
408 fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self {
409 self.init_resource::<DiagnosticsStore>();
410 let mut diagnostics = self.world_mut().resource_mut::<DiagnosticsStore>();
411 diagnostics.add(diagnostic);
412
413 self
414 }
415}
416
417impl RegisterDiagnostic for App {
418 fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self {
419 SubApp::register_diagnostic(self.main_mut(), diagnostic);
420 self
421 }
422}