Skip to content

Commit 320ac65

Browse files
authored
Replace DiagnosticId by DiagnosticPath (#9266)
# Objective Implements #9216 ## Solution - Replace `DiagnosticId` by `DiagnosticPath`. It's pre-hashed using `const-fnv1a-hash` crate, so it's possible to create path in const contexts. --- ## Changelog - Replaced `DiagnosticId` by `DiagnosticPath` - Set default history length to 120 measurements (2 seconds on 60 fps). I've noticed hardcoded constant 20 everywhere and decided to change it to `DEFAULT_MAX_HISTORY_LENGTH` , which is set to new diagnostics by default. To override it, use `with_max_history_length`. ## Migration Guide ```diff - const UNIQUE_DIAG_ID: DiagnosticId = DiagnosticId::from_u128(42); + const UNIQUE_DIAG_PATH: DiagnosticPath = DiagnosticPath::const_new("foo/bar"); - Diagnostic::new(UNIQUE_DIAG_ID, "example", 10) + Diagnostic::new(UNIQUE_DIAG_PATH).with_max_history_length(10) - diagnostics.add_measurement(UNIQUE_DIAG_ID, || 42); + diagnostics.add_measurement(&UNIQUE_DIAG_ID, || 42); ```
1 parent 04aedf1 commit 320ac65

File tree

12 files changed

+241
-172
lines changed

12 files changed

+241
-172
lines changed

crates/bevy_diagnostic/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ bevy_log = { path = "../bevy_log", version = "0.12.0" }
2121
bevy_time = { path = "../bevy_time", version = "0.12.0" }
2222
bevy_utils = { path = "../bevy_utils", version = "0.12.0" }
2323

24+
const-fnv1a-hash = "1.1.0"
25+
2426
# MacOS
2527
[target.'cfg(all(target_os="macos"))'.dependencies]
2628
# Some features of sysinfo are not supported by apple. This will disable those features on apple devices

crates/bevy_diagnostic/src/diagnostic.rs

Lines changed: 133 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,108 @@
1+
use std::hash::{Hash, Hasher};
2+
use std::{borrow::Cow, collections::VecDeque};
3+
14
use bevy_app::App;
25
use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam};
3-
use bevy_log::warn;
4-
use bevy_utils::{Duration, Instant, StableHashMap, Uuid};
5-
use std::{borrow::Cow, collections::VecDeque};
6+
use bevy_utils::{hashbrown::HashMap, Duration, Instant, PassHash};
7+
use const_fnv1a_hash::fnv1a_hash_str_64;
8+
9+
use crate::DEFAULT_MAX_HISTORY_LENGTH;
10+
11+
/// Unique diagnostic path, separated by `/`.
12+
///
13+
/// Requirements:
14+
/// - Can't be empty
15+
/// - Can't have leading or trailing `/`
16+
/// - Can't have empty components.
17+
#[derive(Debug, Clone)]
18+
pub struct DiagnosticPath {
19+
path: Cow<'static, str>,
20+
hash: u64,
21+
}
22+
23+
impl DiagnosticPath {
24+
/// Create a new `DiagnosticPath`. Usable in const contexts.
25+
///
26+
/// **Note**: path is not validated, so make sure it follows all the requirements.
27+
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+
/// Create a new `DiagnosticPath` from the specified string.
35+
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+
/// Create a new `DiagnosticPath` from an iterator over components.
59+
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+
}
671

7-
use crate::MAX_DIAGNOSTIC_NAME_WIDTH;
72+
/// Returns full path, joined by `/`
73+
pub fn as_str(&self) -> &str {
74+
&self.path
75+
}
876

9-
/// Unique identifier for a [`Diagnostic`].
10-
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
11-
pub struct DiagnosticId(pub Uuid);
77+
/// Returns an iterator over path components.
78+
pub fn components(&self) -> impl Iterator<Item = &str> + '_ {
79+
self.path.split('/')
80+
}
81+
}
1282

13-
impl DiagnosticId {
14-
pub const fn from_u128(value: u128) -> Self {
15-
DiagnosticId(Uuid::from_u128(value))
83+
impl From<DiagnosticPath> for String {
84+
fn from(path: DiagnosticPath) -> Self {
85+
path.path.into()
1686
}
1787
}
1888

19-
impl Default for DiagnosticId {
20-
fn default() -> Self {
21-
DiagnosticId(Uuid::new_v4())
89+
impl Eq for DiagnosticPath {}
90+
91+
impl PartialEq for DiagnosticPath {
92+
fn eq(&self, other: &Self) -> bool {
93+
self.hash == other.hash && self.path == other.path
94+
}
95+
}
96+
97+
impl Hash for DiagnosticPath {
98+
fn hash<H: Hasher>(&self, state: &mut H) {
99+
state.write_u64(self.hash);
100+
}
101+
}
102+
103+
impl std::fmt::Display for DiagnosticPath {
104+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105+
self.path.fmt(f)
22106
}
23107
}
24108

@@ -33,8 +117,7 @@ pub struct DiagnosticMeasurement {
33117
/// Diagnostic examples: frames per second, CPU usage, network latency
34118
#[derive(Debug)]
35119
pub struct Diagnostic {
36-
pub id: DiagnosticId,
37-
pub name: Cow<'static, str>,
120+
path: DiagnosticPath,
38121
pub suffix: Cow<'static, str>,
39122
history: VecDeque<DiagnosticMeasurement>,
40123
sum: f64,
@@ -71,34 +154,29 @@ impl Diagnostic {
71154
self.history.push_back(measurement);
72155
}
73156

74-
/// Create a new diagnostic with the given ID, name and maximum history.
75-
pub fn new(
76-
id: DiagnosticId,
77-
name: impl Into<Cow<'static, str>>,
78-
max_history_length: usize,
79-
) -> Diagnostic {
80-
let name = name.into();
81-
if name.chars().count() > MAX_DIAGNOSTIC_NAME_WIDTH {
82-
// This could be a false positive due to a unicode width being shorter
83-
warn!(
84-
"Diagnostic {:?} has name longer than {} characters, and so might overflow in the LogDiagnosticsPlugin\
85-
Consider using a shorter name.",
86-
name, MAX_DIAGNOSTIC_NAME_WIDTH
87-
);
88-
}
157+
/// Create a new diagnostic with the given path.
158+
pub fn new(path: DiagnosticPath) -> Diagnostic {
89159
Diagnostic {
90-
id,
91-
name,
160+
path,
92161
suffix: Cow::Borrowed(""),
93-
history: VecDeque::with_capacity(max_history_length),
94-
max_history_length,
162+
history: VecDeque::with_capacity(DEFAULT_MAX_HISTORY_LENGTH),
163+
max_history_length: DEFAULT_MAX_HISTORY_LENGTH,
95164
sum: 0.0,
96165
ema: 0.0,
97166
ema_smoothing_factor: 2.0 / 21.0,
98167
is_enabled: true,
99168
}
100169
}
101170

171+
/// Set the maximum history length.
172+
#[must_use]
173+
pub fn with_max_history_length(mut self, max_history_length: usize) -> Self {
174+
self.max_history_length = max_history_length;
175+
self.history.reserve(self.max_history_length);
176+
self.history.shrink_to(self.max_history_length);
177+
self
178+
}
179+
102180
/// Add a suffix to use when logging the value, can be used to show a unit.
103181
#[must_use]
104182
pub fn with_suffix(mut self, suffix: impl Into<Cow<'static, str>>) -> Self {
@@ -122,6 +200,10 @@ impl Diagnostic {
122200
self
123201
}
124202

203+
pub fn path(&self) -> &DiagnosticPath {
204+
&self.path
205+
}
206+
125207
/// Get the latest measurement from this diagnostic.
126208
#[inline]
127209
pub fn measurement(&self) -> Option<&DiagnosticMeasurement> {
@@ -198,31 +280,29 @@ impl Diagnostic {
198280
/// A collection of [`Diagnostic`]s.
199281
#[derive(Debug, Default, Resource)]
200282
pub struct DiagnosticsStore {
201-
// This uses a [`StableHashMap`] to ensure that the iteration order is deterministic between
202-
// runs when all diagnostics are inserted in the same order.
203-
diagnostics: StableHashMap<DiagnosticId, Diagnostic>,
283+
diagnostics: HashMap<DiagnosticPath, Diagnostic, PassHash>,
204284
}
205285

206286
impl DiagnosticsStore {
207287
/// Add a new [`Diagnostic`].
208288
///
209289
/// If possible, prefer calling [`App::register_diagnostic`].
210290
pub fn add(&mut self, diagnostic: Diagnostic) {
211-
self.diagnostics.insert(diagnostic.id, diagnostic);
291+
self.diagnostics.insert(diagnostic.path.clone(), diagnostic);
212292
}
213293

214-
pub fn get(&self, id: DiagnosticId) -> Option<&Diagnostic> {
215-
self.diagnostics.get(&id)
294+
pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> {
295+
self.diagnostics.get(path)
216296
}
217297

218-
pub fn get_mut(&mut self, id: DiagnosticId) -> Option<&mut Diagnostic> {
219-
self.diagnostics.get_mut(&id)
298+
pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> {
299+
self.diagnostics.get_mut(path)
220300
}
221301

222302
/// Get the latest [`DiagnosticMeasurement`] from an enabled [`Diagnostic`].
223-
pub fn get_measurement(&self, id: DiagnosticId) -> Option<&DiagnosticMeasurement> {
303+
pub fn get_measurement(&self, path: &DiagnosticPath) -> Option<&DiagnosticMeasurement> {
224304
self.diagnostics
225-
.get(&id)
305+
.get(path)
226306
.filter(|diagnostic| diagnostic.is_enabled)
227307
.and_then(|diagnostic| diagnostic.measurement())
228308
}
@@ -249,27 +329,27 @@ impl<'w, 's> Diagnostics<'w, 's> {
249329
/// Add a measurement to an enabled [`Diagnostic`]. The measurement is passed as a function so that
250330
/// it will be evaluated only if the [`Diagnostic`] is enabled. This can be useful if the value is
251331
/// costly to calculate.
252-
pub fn add_measurement<F>(&mut self, id: DiagnosticId, value: F)
332+
pub fn add_measurement<F>(&mut self, path: &DiagnosticPath, value: F)
253333
where
254334
F: FnOnce() -> f64,
255335
{
256336
if self
257337
.store
258-
.get(id)
338+
.get(path)
259339
.filter(|diagnostic| diagnostic.is_enabled)
260340
.is_some()
261341
{
262342
let measurement = DiagnosticMeasurement {
263343
time: Instant::now(),
264344
value: value(),
265345
};
266-
self.queue.0.insert(id, measurement);
346+
self.queue.0.insert(path.clone(), measurement);
267347
}
268348
}
269349
}
270350

271351
#[derive(Default)]
272-
struct DiagnosticsBuffer(StableHashMap<DiagnosticId, DiagnosticMeasurement>);
352+
struct DiagnosticsBuffer(HashMap<DiagnosticPath, DiagnosticMeasurement, PassHash>);
273353

274354
impl SystemBuffer for DiagnosticsBuffer {
275355
fn apply(
@@ -278,8 +358,8 @@ impl SystemBuffer for DiagnosticsBuffer {
278358
world: &mut bevy_ecs::world::World,
279359
) {
280360
let mut diagnostics = world.resource_mut::<DiagnosticsStore>();
281-
for (id, measurement) in self.0.drain() {
282-
if let Some(diagnostic) = diagnostics.get_mut(id) {
361+
for (path, measurement) in self.0.drain() {
362+
if let Some(diagnostic) = diagnostics.get_mut(&path) {
283363
diagnostic.add_measurement(measurement);
284364
}
285365
}
@@ -298,12 +378,12 @@ impl RegisterDiagnostic for App {
298378
///
299379
/// ```
300380
/// use bevy_app::App;
301-
/// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticId, RegisterDiagnostic};
381+
/// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticPath, RegisterDiagnostic};
302382
///
303-
/// const UNIQUE_DIAG_ID: DiagnosticId = DiagnosticId::from_u128(42);
383+
/// const UNIQUE_DIAG_PATH: DiagnosticPath = DiagnosticPath::const_new("foo/bar");
304384
///
305385
/// App::new()
306-
/// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_ID, "example", 10))
386+
/// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_PATH))
307387
/// .add_plugins(DiagnosticsPlugin)
308388
/// .run();
309389
/// ```
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bevy_app::prelude::*;
22
use bevy_ecs::entity::Entities;
33

4-
use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic};
4+
use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
55

66
/// Adds "entity count" diagnostic to an App.
77
///
@@ -13,16 +13,15 @@ pub struct EntityCountDiagnosticsPlugin;
1313

1414
impl Plugin for EntityCountDiagnosticsPlugin {
1515
fn build(&self, app: &mut App) {
16-
app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT, "entity_count", 20))
16+
app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT))
1717
.add_systems(Update, Self::diagnostic_system);
1818
}
1919
}
2020

2121
impl EntityCountDiagnosticsPlugin {
22-
pub const ENTITY_COUNT: DiagnosticId =
23-
DiagnosticId::from_u128(187513512115068938494459732780662867798);
22+
pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count");
2423

2524
pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) {
26-
diagnostics.add_measurement(Self::ENTITY_COUNT, || entities.len() as f64);
25+
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64);
2726
}
2827
}
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic};
1+
use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
22
use bevy_app::prelude::*;
33
use bevy_core::FrameCount;
44
use bevy_ecs::prelude::*;
@@ -13,39 +13,33 @@ use bevy_time::{Real, Time};
1313
pub struct FrameTimeDiagnosticsPlugin;
1414

1515
impl Plugin for FrameTimeDiagnosticsPlugin {
16-
fn build(&self, app: &mut App) {
17-
app.register_diagnostic(
18-
Diagnostic::new(Self::FRAME_TIME, "frame_time", 20).with_suffix("ms"),
19-
)
20-
.register_diagnostic(Diagnostic::new(Self::FPS, "fps", 20))
21-
.register_diagnostic(
22-
Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1).with_smoothing_factor(0.0),
23-
)
24-
.add_systems(Update, Self::diagnostic_system);
16+
fn build(&self, app: &mut bevy_app::App) {
17+
app.register_diagnostic(Diagnostic::new(Self::FRAME_TIME).with_suffix("ms"))
18+
.register_diagnostic(Diagnostic::new(Self::FPS))
19+
.register_diagnostic(Diagnostic::new(Self::FRAME_COUNT).with_smoothing_factor(0.0))
20+
.add_systems(Update, Self::diagnostic_system);
2521
}
2622
}
2723

2824
impl FrameTimeDiagnosticsPlugin {
29-
pub const FPS: DiagnosticId = DiagnosticId::from_u128(288146834822086093791974408528866909483);
30-
pub const FRAME_COUNT: DiagnosticId =
31-
DiagnosticId::from_u128(54021991829115352065418785002088010277);
32-
pub const FRAME_TIME: DiagnosticId =
33-
DiagnosticId::from_u128(73441630925388532774622109383099159699);
25+
pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps");
26+
pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count");
27+
pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time");
3428

3529
pub fn diagnostic_system(
3630
mut diagnostics: Diagnostics,
3731
time: Res<Time<Real>>,
3832
frame_count: Res<FrameCount>,
3933
) {
40-
diagnostics.add_measurement(Self::FRAME_COUNT, || frame_count.0 as f64);
34+
diagnostics.add_measurement(&Self::FRAME_COUNT, || frame_count.0 as f64);
4135

4236
let delta_seconds = time.delta_seconds_f64();
4337
if delta_seconds == 0.0 {
4438
return;
4539
}
4640

47-
diagnostics.add_measurement(Self::FRAME_TIME, || delta_seconds * 1000.0);
41+
diagnostics.add_measurement(&Self::FRAME_TIME, || delta_seconds * 1000.0);
4842

49-
diagnostics.add_measurement(Self::FPS, || 1.0 / delta_seconds);
43+
diagnostics.add_measurement(&Self::FPS, || 1.0 / delta_seconds);
5044
}
5145
}

crates/bevy_diagnostic/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,5 @@ impl Plugin for DiagnosticsPlugin {
2828
}
2929
}
3030

31-
/// The width which diagnostic names will be printed as
32-
/// Plugin names should not be longer than this value
33-
pub const MAX_DIAGNOSTIC_NAME_WIDTH: usize = 32;
31+
/// Default max history length for new diagnostics.
32+
pub const DEFAULT_MAX_HISTORY_LENGTH: usize = 120;

0 commit comments

Comments
 (0)