avian3d/diagnostics/mod.rs
1//! Diagnostics support for tracking physics timers and counters. Useful for profiling and debugging.
2//!
3//! # Overview
4//!
5//! Each physics plugin such as [`NarrowPhasePlugin`] and [`SolverPlugin`] is responsible
6//! for implementing its own diagnostics resource using the [`PhysicsDiagnostics`] trait
7//! and registering it using [`AppDiagnosticsExt::register_physics_diagnostics`].
8//!
9//! If the `bevy_diagnostic` feature is enabled and the [`PhysicsDiagnosticsPlugin`] is added to the app,
10//! these diagnostics will also be automatically written to the [`DiagnosticsStore`] resource.
11//!
12//! If the `diagnostic_ui` feature is enabled and the [`PhysicsDiagnosticsUiPlugin`] is added to the app,
13//! a debug UI will also be available for displaying these diagnostics in real-time.
14//!
15//! [`NarrowPhasePlugin`]: crate::collision::narrow_phase::NarrowPhasePlugin
16//! [`SolverPlugin`]: crate::dynamics::solver::SolverPlugin
17//! [`DiagnosticsStore`]: bevy::diagnostic::DiagnosticsStore
18//! [`PhysicsDiagnosticsUiPlugin`]: crate::diagnostics::ui::PhysicsDiagnosticsUiPlugin
19//!
20//! # Example
21//!
22//! ```no_run
23#![cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
24#![cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
25//! use bevy::prelude::*;
26//!
27//! fn main() {
28//! App::new()
29//! .add_plugins((
30//! DefaultPlugins,
31//! PhysicsPlugins::default(),
32//! // Add the `PhysicsDiagnosticsPlugin` to write physics diagnostics
33//! // to the `DiagnosticsStore` resource in `bevy_diagnostic`.
34//! // Requires the `bevy_diagnostic` feature.
35//! # #[cfg(feature = "bevy_diagnostic")]
36//! PhysicsDiagnosticsPlugin,
37//! // Add the `PhysicsDiagnosticsUiPlugin` to display physics diagnostics
38//! // in a debug UI. Requires the `diagnostic_ui` feature.
39//! # #[cfg(feature = "diagnostic_ui")]
40//! PhysicsDiagnosticsUiPlugin,
41//! ))
42//! // ...your other plugins, systems and resources
43//! .run();
44//! }
45//! ```
46//!
47//! # Supported Diagnostics
48//!
49//! The following diagnostics are available but not added by default:
50//!
51//! - [`PhysicsTotalDiagnostics`]: Total physics timers and counters.
52//! - [`PhysicsEntityDiagnostics`]: Physics entity counters.
53//!
54//! Additionally, physics plugins may implement their own diagnostics that *are* enabled by default.
55//! These include:
56//!
57//! - [`CollisionDiagnostics`]: Diagnostics for collision detection.
58//! - [`SolverDiagnostics`]: Diagnostics for the physics solver.
59//! - [`SpatialQueryDiagnostics`]: Diagnostics for spatial queries.
60//! - [`PhysicsPickingDiagnostics`]: Diagnostics for physics picking.
61//!
62//! [`CollisionDiagnostics`]: crate::collision::CollisionDiagnostics
63//! [`SolverDiagnostics`]: crate::dynamics::solver::SolverDiagnostics
64//! [`SpatialQueryDiagnostics`]: crate::spatial_query::SpatialQueryDiagnostics
65//! [`PhysicsPickingDiagnostics`]: crate::picking::PhysicsPickingDiagnostics
66
67#[cfg(feature = "bevy_diagnostic")]
68mod entity_counters;
69mod path_macro;
70#[cfg(feature = "bevy_diagnostic")]
71mod total;
72
73#[cfg(feature = "diagnostic_ui")]
74pub mod ui;
75#[cfg(feature = "bevy_diagnostic")]
76pub use entity_counters::{PhysicsEntityDiagnostics, PhysicsEntityDiagnosticsPlugin};
77pub(crate) use path_macro::impl_diagnostic_paths;
78#[cfg(feature = "bevy_diagnostic")]
79pub use total::{PhysicsTotalDiagnostics, PhysicsTotalDiagnosticsPlugin};
80
81use crate::{PhysicsStepSystems, schedule::PhysicsSchedule};
82use bevy::{
83 diagnostic::DiagnosticPath,
84 ecs::component::Mutable,
85 prelude::{App, IntoScheduleConfigs, ResMut, Resource, SystemSet},
86};
87#[cfg(feature = "bevy_diagnostic")]
88use bevy::{
89 diagnostic::{Diagnostic, Diagnostics, RegisterDiagnostic},
90 prelude::{Plugin, Res},
91};
92use core::time::Duration;
93
94/// A plugin that enables writing [physics diagnostics](crate::diagnostics)
95/// to [`bevy::diagnostic::DiagnosticsStore`]. It is not enabled by default
96/// and must be added manually.
97///
98/// To add a debug UI for physics diagnostics, enable the `diagnostic_ui` feature, and add the
99/// [`PhysicsDiagnosticsUiPlugin`] to your app.
100///
101/// See the [module-level documentation](crate::diagnostics) for more information.
102///
103/// [`PhysicsDiagnosticsUiPlugin`]: crate::diagnostics::ui::PhysicsDiagnosticsUiPlugin
104#[cfg(feature = "bevy_diagnostic")]
105pub struct PhysicsDiagnosticsPlugin;
106
107#[cfg(feature = "bevy_diagnostic")]
108impl Plugin for PhysicsDiagnosticsPlugin {
109 fn build(&self, app: &mut App) {
110 // Configure system sets for physics diagnostics.
111 app.configure_sets(
112 PhysicsSchedule,
113 (
114 PhysicsDiagnosticsSystems::Reset.before(PhysicsStepSystems::First),
115 PhysicsDiagnosticsSystems::WriteDiagnostics.after(PhysicsStepSystems::Last),
116 ),
117 );
118 }
119}
120
121/// A system set for [physics diagnostics](crate::diagnostics).
122#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
123pub enum PhysicsDiagnosticsSystems {
124 /// Resets diagnostics to their default values.
125 Reset,
126 /// Writes physics diagnostics to other resources, commonly `DiagnosticsStore`.
127 WriteDiagnostics,
128}
129
130/// A trait for resources storing timers and counters for [physics diagnostics](crate::diagnostics).
131pub trait PhysicsDiagnostics: Default + Resource<Mutability = Mutable> {
132 /// Maps diagnostic paths to their respective duration fields.
133 fn timer_paths(&self) -> Vec<(&'static DiagnosticPath, Duration)> {
134 Vec::new()
135 }
136
137 /// Maps diagnostic paths to their respective counter fields.
138 fn counter_paths(&self) -> Vec<(&'static DiagnosticPath, u32)> {
139 Vec::new()
140 }
141
142 /// A system that resets the diagnostics to their default values.
143 fn reset(mut physics_diagnostics: ResMut<Self>) {
144 *physics_diagnostics = Self::default();
145 }
146
147 /// A system that writes diagnostics to the given [`Diagnostics`] instance.
148 #[cfg(feature = "bevy_diagnostic")]
149 fn write_diagnostics(physics_diagnostics: Res<Self>, mut diagnostics: Diagnostics) {
150 for (path, duration) in physics_diagnostics.timer_paths() {
151 diagnostics.add_measurement(path, || duration.as_secs_f64() * 1000.0);
152 }
153
154 for (path, count) in physics_diagnostics.counter_paths() {
155 diagnostics.add_measurement(path, || count as f64);
156 }
157 }
158}
159
160/// An extension trait for registering [physics diagnostics](crate::diagnostics) in an [`App`].
161pub trait AppDiagnosticsExt {
162 /// Registers timer and counter diagnostics for a resource implementing [`PhysicsDiagnostics`].
163 ///
164 /// This method should be called in [`Plugin::finish`] to ensure that [`Diagnostic`]s
165 /// are only tracked if the [`PhysicsDiagnosticsPlugin`] was added to the app.
166 fn register_physics_diagnostics<T: PhysicsDiagnostics>(&mut self);
167}
168
169impl AppDiagnosticsExt for App {
170 fn register_physics_diagnostics<T: PhysicsDiagnostics>(&mut self) {
171 // Avoid duplicate registrations.
172 if self.world().is_resource_added::<T>() {
173 return;
174 }
175
176 // Initialize the diagnostics resource.
177 self.init_resource::<T>();
178
179 // Make sure the system set exists, even if `PhysicsDiagnosticsPlugin` is not added.
180 self.configure_sets(
181 PhysicsSchedule,
182 PhysicsDiagnosticsSystems::Reset.before(PhysicsStepSystems::First),
183 );
184
185 // Add a system to reset the resource, even if `PhysicsDiagnosticsPlugin` is not added.
186 self.add_systems(
187 PhysicsSchedule,
188 T::reset
189 .in_set(PhysicsDiagnosticsSystems::Reset)
190 .ambiguous_with_all(),
191 );
192
193 #[cfg(feature = "bevy_diagnostic")]
194 {
195 // If physics diagnostics are not enabled, return early.
196 if !self.is_plugin_added::<PhysicsDiagnosticsPlugin>() {
197 return;
198 }
199
200 // Register diagnostics for the paths returned by the diagnostics resource.
201 let diagnostics = T::default();
202 let timer_paths = diagnostics.timer_paths();
203 let counter_paths = diagnostics.counter_paths();
204
205 for path in timer_paths.iter().map(|(path, _)| *path) {
206 // All timers are in milliseconds.
207 self.register_diagnostic(Diagnostic::new(path.clone()).with_suffix("ms"));
208 }
209
210 for path in counter_paths.iter().map(|(path, _)| *path) {
211 // All counters are in whole numbers.
212 self.register_diagnostic(Diagnostic::new(path.clone()).with_smoothing_factor(0.0));
213 }
214
215 // Add systems to reset the diagnostics and write them to the `DiagnosticsStore` resource.
216 self.add_systems(
217 PhysicsSchedule,
218 T::write_diagnostics
219 .in_set(PhysicsDiagnosticsSystems::WriteDiagnostics)
220 .ambiguous_with_all(),
221 );
222 }
223 }
224}