bevy_diagnostic/
log_diagnostics_plugin.rs

1use super::{Diagnostic, DiagnosticPath, DiagnosticsStore};
2
3use bevy_app::prelude::*;
4use bevy_ecs::prelude::*;
5use bevy_platform::collections::HashSet;
6use bevy_time::{Real, Time, Timer, TimerMode};
7use core::time::Duration;
8use log::{debug, info};
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    /// If `true` then the `Debug` representation of each `Diagnostic` is logged.
19    /// If `false` then a (smoothed) current value and historical average are logged.
20    ///
21    /// Defaults to `false`.
22    pub debug: bool,
23    /// Time to wait between logging diagnostics and logging them again.
24    pub wait_duration: Duration,
25    /// If `Some` then only these diagnostics are logged.
26    pub filter: Option<HashSet<DiagnosticPath>>,
27}
28
29/// State used by the [`LogDiagnosticsPlugin`]
30#[derive(Resource)]
31pub struct LogDiagnosticsState {
32    timer: Timer,
33    filter: Option<HashSet<DiagnosticPath>>,
34}
35
36impl LogDiagnosticsState {
37    /// Sets a new duration for the log timer
38    pub fn set_timer_duration(&mut self, duration: Duration) {
39        self.timer.set_duration(duration);
40        self.timer.set_elapsed(Duration::ZERO);
41    }
42
43    /// Add a filter to the log state, returning `true` if the [`DiagnosticPath`]
44    /// was not present
45    pub fn add_filter(&mut self, diagnostic_path: DiagnosticPath) -> bool {
46        if let Some(filter) = &mut self.filter {
47            filter.insert(diagnostic_path)
48        } else {
49            self.filter = Some(HashSet::from_iter([diagnostic_path]));
50            true
51        }
52    }
53
54    /// Extends the filter of the log state with multiple [`DiagnosticPaths`](DiagnosticPath)
55    pub fn extend_filter(&mut self, iter: impl IntoIterator<Item = DiagnosticPath>) {
56        if let Some(filter) = &mut self.filter {
57            filter.extend(iter);
58        } else {
59            self.filter = Some(HashSet::from_iter(iter));
60        }
61    }
62
63    /// Removes a filter from the log state, returning `true` if it was present
64    pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool {
65        if let Some(filter) = &mut self.filter {
66            filter.remove(diagnostic_path)
67        } else {
68            false
69        }
70    }
71
72    /// Clears the filters of the log state
73    pub fn clear_filter(&mut self) {
74        if let Some(filter) = &mut self.filter {
75            filter.clear();
76        }
77    }
78
79    /// Enables filtering with empty filters
80    pub fn enable_filtering(&mut self) {
81        self.filter = Some(HashSet::new());
82    }
83
84    /// Disables filtering
85    pub fn disable_filtering(&mut self) {
86        self.filter = None;
87    }
88}
89
90impl Default for LogDiagnosticsPlugin {
91    fn default() -> Self {
92        LogDiagnosticsPlugin {
93            debug: false,
94            wait_duration: Duration::from_secs(1),
95            filter: None,
96        }
97    }
98}
99
100impl Plugin for LogDiagnosticsPlugin {
101    fn build(&self, app: &mut App) {
102        app.insert_resource(LogDiagnosticsState {
103            timer: Timer::new(self.wait_duration, TimerMode::Repeating),
104            filter: self.filter.clone(),
105        });
106
107        if self.debug {
108            app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);
109        } else {
110            app.add_systems(PostUpdate, Self::log_diagnostics_system);
111        }
112    }
113}
114
115impl LogDiagnosticsPlugin {
116    /// Filter logging to only the paths in `filter`.
117    pub fn filtered(filter: HashSet<DiagnosticPath>) -> Self {
118        LogDiagnosticsPlugin {
119            filter: Some(filter),
120            ..Default::default()
121        }
122    }
123
124    fn for_each_diagnostic(
125        state: &LogDiagnosticsState,
126        diagnostics: &DiagnosticsStore,
127        mut callback: impl FnMut(&Diagnostic),
128    ) {
129        if let Some(filter) = &state.filter {
130            for path in filter.iter() {
131                if let Some(diagnostic) = diagnostics.get(path)
132                    && diagnostic.is_enabled
133                {
134                    callback(diagnostic);
135                }
136            }
137        } else {
138            for diagnostic in diagnostics.iter() {
139                if diagnostic.is_enabled {
140                    callback(diagnostic);
141                }
142            }
143        }
144    }
145
146    fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) {
147        let Some(value) = diagnostic.smoothed() else {
148            return;
149        };
150
151        if diagnostic.get_max_history_length() > 1 {
152            let Some(average) = diagnostic.average() else {
153                return;
154            };
155
156            info!(
157                target: "bevy_diagnostic",
158                // Suffix is only used for 's' or 'ms' currently,
159                // so we reserve two columns for it; however,
160                // Do not reserve columns for the suffix in the average
161                // The ) hugging the value is more aesthetically pleasing
162                "{path:<path_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
163                path = diagnostic.path(),
164                suffix = diagnostic.suffix,
165            );
166        } else {
167            info!(
168                target: "bevy_diagnostic",
169                "{path:<path_width$}: {value:>.6}{suffix:}",
170                path = diagnostic.path(),
171                suffix = diagnostic.suffix,
172            );
173        }
174    }
175
176    fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) {
177        let mut path_width = 0;
178        Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
179            let width = diagnostic.path().as_str().len();
180            path_width = path_width.max(width);
181        });
182
183        Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
184            Self::log_diagnostic(path_width, diagnostic);
185        });
186    }
187
188    fn log_diagnostics_system(
189        mut state: ResMut<LogDiagnosticsState>,
190        time: Res<Time<Real>>,
191        diagnostics: Res<DiagnosticsStore>,
192    ) {
193        if state.timer.tick(time.delta()).is_finished() {
194            Self::log_diagnostics(&state, &diagnostics);
195        }
196    }
197
198    fn log_diagnostics_debug_system(
199        mut state: ResMut<LogDiagnosticsState>,
200        time: Res<Time<Real>>,
201        diagnostics: Res<DiagnosticsStore>,
202    ) {
203        if state.timer.tick(time.delta()).is_finished() {
204            Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| {
205                debug!("{diagnostic:#?}\n");
206            });
207        }
208    }
209}