Path: blob/main/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs
6595 views
use super::{Diagnostic, DiagnosticPath, DiagnosticsStore};12use bevy_app::prelude::*;3use bevy_ecs::prelude::*;4use bevy_platform::collections::HashSet;5use bevy_time::{Real, Time, Timer, TimerMode};6use core::time::Duration;7use log::{debug, info};89/// An App Plugin that logs diagnostics to the console.10///11/// Diagnostics are collected by plugins such as12/// [`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/// If `true` then the `Debug` representation of each `Diagnostic` is logged.18/// If `false` then a (smoothed) current value and historical average are logged.19///20/// Defaults to `false`.21pub debug: bool,22/// Time to wait between logging diagnostics and logging them again.23pub wait_duration: Duration,24/// If `Some` then only these diagnostics are logged.25pub filter: Option<HashSet<DiagnosticPath>>,26}2728/// State used by the [`LogDiagnosticsPlugin`]29#[derive(Resource)]30pub struct LogDiagnosticsState {31timer: Timer,32filter: Option<HashSet<DiagnosticPath>>,33}3435impl LogDiagnosticsState {36/// Sets a new duration for the log timer37pub fn set_timer_duration(&mut self, duration: Duration) {38self.timer.set_duration(duration);39self.timer.set_elapsed(Duration::ZERO);40}4142/// Add a filter to the log state, returning `true` if the [`DiagnosticPath`]43/// was not present44pub fn add_filter(&mut self, diagnostic_path: DiagnosticPath) -> bool {45if let Some(filter) = &mut self.filter {46filter.insert(diagnostic_path)47} else {48self.filter = Some(HashSet::from_iter([diagnostic_path]));49true50}51}5253/// Extends the filter of the log state with multiple [`DiagnosticPaths`](DiagnosticPath)54pub fn extend_filter(&mut self, iter: impl IntoIterator<Item = DiagnosticPath>) {55if let Some(filter) = &mut self.filter {56filter.extend(iter);57} else {58self.filter = Some(HashSet::from_iter(iter));59}60}6162/// Removes a filter from the log state, returning `true` if it was present63pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool {64if let Some(filter) = &mut self.filter {65filter.remove(diagnostic_path)66} else {67false68}69}7071/// Clears the filters of the log state72pub fn clear_filter(&mut self) {73if let Some(filter) = &mut self.filter {74filter.clear();75}76}7778/// Enables filtering with empty filters79pub fn enable_filtering(&mut self) {80self.filter = Some(HashSet::new());81}8283/// Disables filtering84pub fn disable_filtering(&mut self) {85self.filter = None;86}87}8889impl Default for LogDiagnosticsPlugin {90fn default() -> Self {91LogDiagnosticsPlugin {92debug: false,93wait_duration: Duration::from_secs(1),94filter: None,95}96}97}9899impl Plugin for LogDiagnosticsPlugin {100fn build(&self, app: &mut App) {101app.insert_resource(LogDiagnosticsState {102timer: Timer::new(self.wait_duration, TimerMode::Repeating),103filter: self.filter.clone(),104});105106if self.debug {107app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);108} else {109app.add_systems(PostUpdate, Self::log_diagnostics_system);110}111}112}113114impl LogDiagnosticsPlugin {115/// Filter logging to only the paths in `filter`.116pub fn filtered(filter: HashSet<DiagnosticPath>) -> Self {117LogDiagnosticsPlugin {118filter: Some(filter),119..Default::default()120}121}122123fn for_each_diagnostic(124state: &LogDiagnosticsState,125diagnostics: &DiagnosticsStore,126mut callback: impl FnMut(&Diagnostic),127) {128if let Some(filter) = &state.filter {129for path in filter.iter() {130if let Some(diagnostic) = diagnostics.get(path)131&& diagnostic.is_enabled132{133callback(diagnostic);134}135}136} else {137for diagnostic in diagnostics.iter() {138if diagnostic.is_enabled {139callback(diagnostic);140}141}142}143}144145fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) {146let Some(value) = diagnostic.smoothed() else {147return;148};149150if diagnostic.get_max_history_length() > 1 {151let Some(average) = diagnostic.average() else {152return;153};154155info!(156target: "bevy_diagnostic",157// Suffix is only used for 's' or 'ms' currently,158// so we reserve two columns for it; however,159// Do not reserve columns for the suffix in the average160// The ) hugging the value is more aesthetically pleasing161"{path:<path_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",162path = diagnostic.path(),163suffix = diagnostic.suffix,164);165} else {166info!(167target: "bevy_diagnostic",168"{path:<path_width$}: {value:>.6}{suffix:}",169path = diagnostic.path(),170suffix = diagnostic.suffix,171);172}173}174175fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) {176let mut path_width = 0;177Self::for_each_diagnostic(state, diagnostics, |diagnostic| {178let width = diagnostic.path().as_str().len();179path_width = path_width.max(width);180});181182Self::for_each_diagnostic(state, diagnostics, |diagnostic| {183Self::log_diagnostic(path_width, diagnostic);184});185}186187fn log_diagnostics_system(188mut state: ResMut<LogDiagnosticsState>,189time: Res<Time<Real>>,190diagnostics: Res<DiagnosticsStore>,191) {192if state.timer.tick(time.delta()).is_finished() {193Self::log_diagnostics(&state, &diagnostics);194}195}196197fn log_diagnostics_debug_system(198mut state: ResMut<LogDiagnosticsState>,199time: Res<Time<Real>>,200diagnostics: Res<DiagnosticsStore>,201) {202if state.timer.tick(time.delta()).is_finished() {203Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| {204debug!("{diagnostic:#?}\n");205});206}207}208}209210211