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