bevy_diagnostic/
log_diagnostics_plugin.rs

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