bevy_diagnostic/
log_diagnostics_plugin.rs

1use super::{Diagnostic, DiagnosticPath, DiagnosticsStore};
2use bevy_app::prelude::*;
3use bevy_ecs::prelude::*;
4use bevy_time::{Real, Time, Timer, TimerMode};
5use bevy_utils::tracing::{debug, info};
6use bevy_utils::Duration;
7
8/// An App Plugin that logs diagnostics to the console.
9///
10/// Diagnostics are collected by plugins such as
11/// [`FrameTimeDiagnosticsPlugin`](crate::FrameTimeDiagnosticsPlugin)
12/// or can be provided by the user.
13///
14/// When no diagnostics are provided, this plugin does nothing.
15pub struct LogDiagnosticsPlugin {
16    pub debug: bool,
17    pub wait_duration: Duration,
18    pub filter: Option<Vec<DiagnosticPath>>,
19}
20
21/// State used by the [`LogDiagnosticsPlugin`]
22#[derive(Resource)]
23struct LogDiagnosticsState {
24    timer: Timer,
25    filter: Option<Vec<DiagnosticPath>>,
26}
27
28impl Default for LogDiagnosticsPlugin {
29    fn default() -> Self {
30        LogDiagnosticsPlugin {
31            debug: false,
32            wait_duration: Duration::from_secs(1),
33            filter: None,
34        }
35    }
36}
37
38impl Plugin for LogDiagnosticsPlugin {
39    fn build(&self, app: &mut App) {
40        app.insert_resource(LogDiagnosticsState {
41            timer: Timer::new(self.wait_duration, TimerMode::Repeating),
42            filter: self.filter.clone(),
43        });
44
45        if self.debug {
46            app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);
47        } else {
48            app.add_systems(PostUpdate, Self::log_diagnostics_system);
49        }
50    }
51}
52
53impl LogDiagnosticsPlugin {
54    pub fn filtered(filter: Vec<DiagnosticPath>) -> Self {
55        LogDiagnosticsPlugin {
56            filter: Some(filter),
57            ..Default::default()
58        }
59    }
60
61    fn for_each_diagnostic(
62        state: &LogDiagnosticsState,
63        diagnostics: &DiagnosticsStore,
64        mut callback: impl FnMut(&Diagnostic),
65    ) {
66        if let Some(filter) = &state.filter {
67            for path in filter {
68                if let Some(diagnostic) = diagnostics.get(path) {
69                    if diagnostic.is_enabled {
70                        callback(diagnostic);
71                    }
72                }
73            }
74        } else {
75            for diagnostic in diagnostics.iter() {
76                if diagnostic.is_enabled {
77                    callback(diagnostic);
78                }
79            }
80        }
81    }
82
83    fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) {
84        let Some(value) = diagnostic.smoothed() else {
85            return;
86        };
87
88        if diagnostic.get_max_history_length() > 1 {
89            let Some(average) = diagnostic.average() else {
90                return;
91            };
92
93            info!(
94                target: "bevy diagnostic",
95                // Suffix is only used for 's' or 'ms' currently,
96                // so we reserve two columns for it; however,
97                // Do not reserve columns for the suffix in the average
98                // The ) hugging the value is more aesthetically pleasing
99                "{path:<path_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
100                path = diagnostic.path(),
101                suffix = diagnostic.suffix,
102            );
103        } else {
104            info!(
105                target: "bevy diagnostic",
106                "{path:<path_width$}: {value:>.6}{suffix:}",
107                path = diagnostic.path(),
108                suffix = diagnostic.suffix,
109            );
110        }
111    }
112
113    fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) {
114        let mut path_width = 0;
115        Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
116            let width = diagnostic.path().as_str().len();
117            path_width = path_width.max(width);
118        });
119
120        Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
121            Self::log_diagnostic(path_width, diagnostic);
122        });
123    }
124
125    fn log_diagnostics_system(
126        mut state: ResMut<LogDiagnosticsState>,
127        time: Res<Time<Real>>,
128        diagnostics: Res<DiagnosticsStore>,
129    ) {
130        if state.timer.tick(time.delta()).finished() {
131            Self::log_diagnostics(&state, &diagnostics);
132        }
133    }
134
135    fn log_diagnostics_debug_system(
136        mut state: ResMut<LogDiagnosticsState>,
137        time: Res<Time<Real>>,
138        diagnostics: Res<DiagnosticsStore>,
139    ) {
140        if state.timer.tick(time.delta()).finished() {
141            Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| {
142                debug!("{:#?}\n", diagnostic);
143            });
144        }
145    }
146}