diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index e5098d6c6f..588b3276f6 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -29,7 +29,7 @@ pub use diagnostic::*; pub use entity_count_diagnostics_plugin::EntityCountDiagnosticsPlugin; pub use frame_count_diagnostics_plugin::{update_frame_count, FrameCount, FrameCountPlugin}; pub use frame_time_diagnostics_plugin::FrameTimeDiagnosticsPlugin; -pub use log_diagnostics_plugin::LogDiagnosticsPlugin; +pub use log_diagnostics_plugin::{LogDiagnosticsPlugin, LogDiagnosticsState}; #[cfg(feature = "sysinfo_plugin")] pub use system_information_diagnostics_plugin::{SystemInfo, SystemInformationDiagnosticsPlugin}; diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index 1246b03f81..8b01d10402 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -1,7 +1,8 @@ use super::{Diagnostic, DiagnosticPath, DiagnosticsStore}; -use alloc::vec::Vec; + 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}; @@ -16,14 +17,68 @@ use log::{debug, info}; pub struct LogDiagnosticsPlugin { pub debug: bool, pub wait_duration: Duration, - pub filter: Option>, + pub filter: Option>, } /// State used by the [`LogDiagnosticsPlugin`] #[derive(Resource)] -struct LogDiagnosticsState { +pub struct LogDiagnosticsState { timer: Timer, - filter: Option>, + filter: Option>, +} + +impl LogDiagnosticsState { + /// Sets a new duration for the log timer + pub fn set_timer_duration(&mut self, duration: Duration) { + self.timer.set_duration(duration); + self.timer.set_elapsed(Duration::ZERO); + } + + /// Add a filter to the log state, returning `true` if the [`DiagnosticPath`] + /// was not present + 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 + } + } + + /// Extends the filter of the log state with multiple [`DiagnosticPaths`](DiagnosticPath) + pub fn extend_filter(&mut self, iter: impl IntoIterator) { + if let Some(filter) = &mut self.filter { + filter.extend(iter); + } else { + self.filter = Some(HashSet::from_iter(iter)); + } + } + + /// Removes a filter from the log state, returning `true` if it was present + pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool { + if let Some(filter) = &mut self.filter { + filter.remove(diagnostic_path) + } else { + false + } + } + + /// Clears the filters of the log state + pub fn clear_filter(&mut self) { + if let Some(filter) = &mut self.filter { + filter.clear(); + } + } + + /// Enables filtering with empty filters + pub fn enable_filtering(&mut self) { + self.filter = Some(HashSet::new()); + } + + /// Disables filtering + pub fn disable_filtering(&mut self) { + self.filter = None; + } } impl Default for LogDiagnosticsPlugin { @@ -52,7 +107,7 @@ impl Plugin for LogDiagnosticsPlugin { } impl LogDiagnosticsPlugin { - pub fn filtered(filter: Vec) -> Self { + pub fn filtered(filter: HashSet) -> Self { LogDiagnosticsPlugin { filter: Some(filter), ..Default::default() @@ -65,7 +120,7 @@ impl LogDiagnosticsPlugin { mut callback: impl FnMut(&Diagnostic), ) { if let Some(filter) = &state.filter { - for path in filter { + for path in filter.iter() { if let Some(diagnostic) = diagnostics.get(path) { if diagnostic.is_enabled { callback(diagnostic); diff --git a/examples/diagnostics/log_diagnostics.rs b/examples/diagnostics/log_diagnostics.rs index f487a87133..0e00e69ccd 100644 --- a/examples/diagnostics/log_diagnostics.rs +++ b/examples/diagnostics/log_diagnostics.rs @@ -1,10 +1,27 @@ //! Shows different built-in plugins that logs diagnostics, like frames per second (FPS), to the console. use bevy::{ - diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + color::palettes, + diagnostic::{ + DiagnosticPath, EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin, + LogDiagnosticsPlugin, LogDiagnosticsState, SystemInformationDiagnosticsPlugin, + }, prelude::*, }; +const FRAME_TIME_DIAGNOSTICS: [DiagnosticPath; 3] = [ + FrameTimeDiagnosticsPlugin::FPS, + FrameTimeDiagnosticsPlugin::FRAME_COUNT, + FrameTimeDiagnosticsPlugin::FRAME_TIME, +]; +const ENTITY_COUNT_DIAGNOSTICS: [DiagnosticPath; 1] = [EntityCountDiagnosticsPlugin::ENTITY_COUNT]; +const SYSTEM_INFO_DIAGNOSTICS: [DiagnosticPath; 4] = [ + SystemInformationDiagnosticsPlugin::PROCESS_CPU_USAGE, + SystemInformationDiagnosticsPlugin::PROCESS_MEM_USAGE, + SystemInformationDiagnosticsPlugin::SYSTEM_CPU_USAGE, + SystemInformationDiagnosticsPlugin::SYSTEM_MEM_USAGE, +]; + fn main() { App::new() .add_plugins(( @@ -16,9 +33,9 @@ fn main() { // Adds frame time, FPS and frame count diagnostics. FrameTimeDiagnosticsPlugin::default(), // Adds an entity count diagnostic. - bevy::diagnostic::EntityCountDiagnosticsPlugin, + EntityCountDiagnosticsPlugin, // Adds cpu and memory usage diagnostics for systems and the entire game process. - bevy::diagnostic::SystemInformationDiagnosticsPlugin, + SystemInformationDiagnosticsPlugin, // Forwards various diagnostics from the render app to the main app. // These are pretty verbose but can be useful to pinpoint performance issues. bevy::render::diagnostic::RenderDiagnosticsPlugin, @@ -26,6 +43,14 @@ fn main() { // No rendering diagnostics are emitted unless something is drawn to the screen, // so we spawn a small scene. .add_systems(Startup, setup) + .add_systems(Update, filters_inputs) + .add_systems( + Update, + update_commands.run_if( + resource_exists_and_changed:: + .or(resource_exists_and_changed::), + ), + ) .run(); } @@ -60,4 +85,223 @@ fn setup( Camera3d::default(), Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), )); + + commands.init_resource::(); + commands.init_resource::(); + + commands.spawn(( + LogDiagnosticsCommands, + Node { + top: Val::Px(5.), + left: Val::Px(5.), + flex_direction: FlexDirection::Column, + ..default() + }, + )); } + +fn filters_inputs( + keys: Res>, + mut status: ResMut, + mut filters: ResMut, + mut log_state: ResMut, +) { + if keys.just_pressed(KeyCode::KeyQ) { + *status = match *status { + LogDiagnosticsStatus::Enabled => { + log_state.disable_filtering(); + LogDiagnosticsStatus::Disabled + } + LogDiagnosticsStatus::Disabled => { + log_state.enable_filtering(); + if filters.frame_time { + enable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS); + } + if filters.entity_count { + enable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS); + } + if filters.system_info { + enable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS); + } + LogDiagnosticsStatus::Enabled + } + }; + } + + let enabled = *status == LogDiagnosticsStatus::Enabled; + if keys.just_pressed(KeyCode::Digit1) { + filters.frame_time = !filters.frame_time; + if enabled { + if filters.frame_time { + enable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS); + } else { + disable_filters(&mut log_state, FRAME_TIME_DIAGNOSTICS); + } + } + } + if keys.just_pressed(KeyCode::Digit2) { + filters.entity_count = !filters.entity_count; + if enabled { + if filters.entity_count { + enable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS); + } else { + disable_filters(&mut log_state, ENTITY_COUNT_DIAGNOSTICS); + } + } + } + if keys.just_pressed(KeyCode::Digit3) { + filters.system_info = !filters.system_info; + if enabled { + if filters.system_info { + enable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS); + } else { + disable_filters(&mut log_state, SYSTEM_INFO_DIAGNOSTICS); + } + } + } +} + +fn enable_filters( + log_state: &mut LogDiagnosticsState, + diagnostics: impl IntoIterator, +) { + log_state.extend_filter(diagnostics); +} + +fn disable_filters( + log_state: &mut LogDiagnosticsState, + diagnostics: impl IntoIterator, +) { + for diagnostic in diagnostics { + log_state.remove_filter(&diagnostic); + } +} + +fn update_commands( + mut commands: Commands, + log_commands: Single>, + status: Res, + filters: Res, +) { + let enabled = *status == LogDiagnosticsStatus::Enabled; + let alpha = if enabled { 1. } else { 0.25 }; + let enabled_color = |enabled| { + if enabled { + Color::from(palettes::tailwind::GREEN_400) + } else { + Color::from(palettes::tailwind::RED_400) + } + }; + commands + .entity(*log_commands) + .despawn_related::() + .insert(children![ + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(5.), + ..default() + }, + children![ + Text::new("[Q] Toggle filtering:"), + ( + Text::new(format!("{:?}", *status)), + TextColor(enabled_color(enabled)) + ) + ] + ), + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(5.), + ..default() + }, + children![ + ( + Text::new("[1] Frame times:"), + TextColor(Color::WHITE.with_alpha(alpha)) + ), + ( + Text::new(format!("{:?}", filters.frame_time)), + TextColor(enabled_color(filters.frame_time).with_alpha(alpha)) + ) + ] + ), + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(5.), + ..default() + }, + children![ + ( + Text::new("[2] Entity count:"), + TextColor(Color::WHITE.with_alpha(alpha)) + ), + ( + Text::new(format!("{:?}", filters.entity_count)), + TextColor(enabled_color(filters.entity_count).with_alpha(alpha)) + ) + ] + ), + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(5.), + ..default() + }, + children![ + ( + Text::new("[3] System info:"), + TextColor(Color::WHITE.with_alpha(alpha)) + ), + ( + Text::new(format!("{:?}", filters.system_info)), + TextColor(enabled_color(filters.system_info).with_alpha(alpha)) + ) + ] + ), + ( + Node { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(5.), + ..default() + }, + children![ + ( + Text::new("[4] Render diagnostics:"), + TextColor(Color::WHITE.with_alpha(alpha)) + ), + ( + Text::new("Private"), + TextColor(enabled_color(false).with_alpha(alpha)) + ) + ] + ), + ]); +} + +#[derive(Debug, Default, PartialEq, Eq, Resource)] +enum LogDiagnosticsStatus { + /// No filtering, showing all logs + #[default] + Disabled, + /// Filtering enabled, showing only subset of logs + Enabled, +} + +#[derive(Default, Resource)] +struct LogDiagnosticsFilters { + frame_time: bool, + entity_count: bool, + system_info: bool, + #[expect( + dead_code, + reason = "Currently the diagnostic paths referent to RenderDiagnosticPlugin are private" + )] + render_diagnostics: bool, +} + +#[derive(Component)] +/// Marks the UI node that has instructions on how to change the filtering +struct LogDiagnosticsCommands; diff --git a/examples/stress_tests/many_components.rs b/examples/stress_tests/many_components.rs index f047445f94..88384dd721 100644 --- a/examples/stress_tests/many_components.rs +++ b/examples/stress_tests/many_components.rs @@ -22,6 +22,7 @@ use bevy::{ world::FilteredEntityMut, }, log::LogPlugin, + platform::collections::HashSet, prelude::{App, In, IntoSystem, Query, Schedule, SystemParamBuilder, Update}, ptr::{OwningPtr, PtrMut}, MinimalPlugins, @@ -168,9 +169,9 @@ fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) { .add_plugins(DiagnosticsPlugin) .add_plugins(LogPlugin::default()) .add_plugins(FrameTimeDiagnosticsPlugin::default()) - .add_plugins(LogDiagnosticsPlugin::filtered(vec![DiagnosticPath::new( - "fps", - )])); + .add_plugins(LogDiagnosticsPlugin::filtered(HashSet::from_iter([ + DiagnosticPath::new("fps"), + ]))); app.run(); } diff --git a/release-content/migration-guides/log-diagnostics-hash-set.md b/release-content/migration-guides/log-diagnostics-hash-set.md new file mode 100644 index 0000000000..2d05f8ce3c --- /dev/null +++ b/release-content/migration-guides/log-diagnostics-hash-set.md @@ -0,0 +1,8 @@ +--- +title: Change filters container of `LogDiagnosticsState` to `HashSet` +authors: ["@hukasu"] +pull_requests: [19323] +--- + +Make `LogDiagnosticsState`'s filter container and argument of +`LogDiagnosticPlugin::filtered` into `HashSet`. diff --git a/release-content/release-notes/log-diagnostics-state-public.md b/release-content/release-notes/log-diagnostics-state-public.md new file mode 100644 index 0000000000..4e05ed4202 --- /dev/null +++ b/release-content/release-notes/log-diagnostics-state-public.md @@ -0,0 +1,8 @@ +--- +title: LogDiagnosticsState is now public +authors: ["@hukasu"] +pull_requests: [19323] +--- + +Make `LogDiagnosticsState` public to allow editing its duration and filters during +runtime.