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