use super::{Diagnostic, DiagnosticPath, DiagnosticsStore};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_time::{Real, Time, Timer, TimerMode};
use core::time::Duration;
use log::{debug, info};
pub struct LogDiagnosticsPlugin {
pub debug: bool,
pub wait_duration: Duration,
pub filter: Option<HashSet<DiagnosticPath>>,
}
#[derive(Resource)]
pub struct LogDiagnosticsState {
timer: Timer,
filter: Option<HashSet<DiagnosticPath>>,
}
impl LogDiagnosticsState {
pub fn set_timer_duration(&mut self, duration: Duration) {
self.timer.set_duration(duration);
self.timer.set_elapsed(Duration::ZERO);
}
pub fn add_filter(&mut self, diagnostic_path: DiagnosticPath) -> bool {
if let Some(filter) = &mut self.filter {
filter.insert(diagnostic_path)
} else {
self.filter = Some(HashSet::from_iter([diagnostic_path]));
true
}
}
pub fn extend_filter(&mut self, iter: impl IntoIterator<Item = DiagnosticPath>) {
if let Some(filter) = &mut self.filter {
filter.extend(iter);
} else {
self.filter = Some(HashSet::from_iter(iter));
}
}
pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool {
if let Some(filter) = &mut self.filter {
filter.remove(diagnostic_path)
} else {
false
}
}
pub fn clear_filter(&mut self) {
if let Some(filter) = &mut self.filter {
filter.clear();
}
}
pub fn enable_filtering(&mut self) {
self.filter = Some(HashSet::new());
}
pub fn disable_filtering(&mut self) {
self.filter = None;
}
}
impl Default for LogDiagnosticsPlugin {
fn default() -> Self {
LogDiagnosticsPlugin {
debug: false,
wait_duration: Duration::from_secs(1),
filter: None,
}
}
}
impl Plugin for LogDiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(LogDiagnosticsState {
timer: Timer::new(self.wait_duration, TimerMode::Repeating),
filter: self.filter.clone(),
});
if self.debug {
app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);
} else {
app.add_systems(PostUpdate, Self::log_diagnostics_system);
}
}
}
impl LogDiagnosticsPlugin {
pub fn filtered(filter: HashSet<DiagnosticPath>) -> Self {
LogDiagnosticsPlugin {
filter: Some(filter),
..Default::default()
}
}
fn for_each_diagnostic(
state: &LogDiagnosticsState,
diagnostics: &DiagnosticsStore,
mut callback: impl FnMut(&Diagnostic),
) {
if let Some(filter) = &state.filter {
for path in filter.iter() {
if let Some(diagnostic) = diagnostics.get(path)
&& diagnostic.is_enabled
{
callback(diagnostic);
}
}
} else {
for diagnostic in diagnostics.iter() {
if diagnostic.is_enabled {
callback(diagnostic);
}
}
}
}
fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) {
let Some(value) = diagnostic.smoothed() else {
return;
};
if diagnostic.get_max_history_length() > 1 {
let Some(average) = diagnostic.average() else {
return;
};
info!(
target: "bevy_diagnostic",
"{path:<path_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
path = diagnostic.path(),
suffix = diagnostic.suffix,
);
} else {
info!(
target: "bevy_diagnostic",
"{path:<path_width$}: {value:>.6}{suffix:}",
path = diagnostic.path(),
suffix = diagnostic.suffix,
);
}
}
fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) {
let mut path_width = 0;
Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
let width = diagnostic.path().as_str().len();
path_width = path_width.max(width);
});
Self::for_each_diagnostic(state, diagnostics, |diagnostic| {
Self::log_diagnostic(path_width, diagnostic);
});
}
fn log_diagnostics_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.delta()).is_finished() {
Self::log_diagnostics(&state, &diagnostics);
}
}
fn log_diagnostics_debug_system(
mut state: ResMut<LogDiagnosticsState>,
time: Res<Time<Real>>,
diagnostics: Res<DiagnosticsStore>,
) {
if state.timer.tick(time.delta()).is_finished() {
Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| {
debug!("{diagnostic:#?}\n");
});
}
}
}