Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Physics Diagnostics #653

Merged
merged 20 commits into from
Feb 8, 2025
Merged

Physics Diagnostics #653

merged 20 commits into from
Feb 8, 2025

Conversation

Jondolf
Copy link
Owner

@Jondolf Jondolf commented Feb 8, 2025

Objective

Fixes #564.
Expands significantly on #576.

For both benchmarking and optimizing Avian itself, and monitoring physics performance on the user side, it can be very useful to have timing information and various other statistics tracked by the physics engine. This is also done by engines such as Rapier and Box2D.

Solution

Summary:

  • Track physics timers and counters in resources like CollisionDiagnostics and SolverDiagnostics.
  • Add bevy_diagnostic feature and PhysicsDiagnosticsPlugin for optionally writing these diagnostics to bevy_diagnostic::DiagnosticsStore.
  • Add diagnostic_ui feature and PhysicsDiagnosticsUiPlugin for optionally displaying physics diagnostics with a debug UI.

Physics Diagnostics Resources

The natural place for diagnostics in Bevy would be bevy_diagnostic::DiagnosticsStore. However:

  • It is not suitable for tracking timers spanning across several substeps without storing a timer externally.
  • It likely has a small amount of additional overhead.
  • All measurements for Bevy's diagnostics use f64, which makes counters a bit more awkward.

Thus, most diagnostics are tracked in separate resources such as SolverDiagnostics:

/// Diagnostics for the physics solver.
#[derive(Resource, Debug, Default, Reflect)]
#[reflect(Resource, Debug)]
pub struct SolverDiagnostics {
    /// Time spent integrating velocities.
    pub integrate_velocities: Duration,
    /// Time spent warm starting the solver.
    pub warm_start: Duration,
    /// Time spent solving constraints with bias.
    pub solve_constraints: Duration,
    /// Time spent integrating positions.
    pub integrate_positions: Duration,
    /// Time spent relaxing velocities.
    pub relax_velocities: Duration,
    /// Time spent applying restitution.
    pub apply_restitution: Duration,
    /// Time spent finalizing positions.
    pub finalize: Duration,
    /// Time spent storing impulses for warm starting.
    pub store_impulses: Duration,
    /// Time spent on swept CCD.
    pub swept_ccd: Duration,
    /// The number of contact constraints generated.
    pub contact_constraint_count: u32,
}

These are updated in the relevant systems. Timers should have a very small, fixed cost, so they are currently tracked by default and cannot be disabled (same as Box2D), aside from disabling e.g. the SolverPlugin itself. If it is deemed to have measurable overhead down the line, we could try putting timers behind a feature flag, though it does add some complexity.

Integration With DiagnosticsStore

It can still be valuable to also record physics diagnostics to bevy_diagnostic::DiagnosticsStore to benefit from the history and smoothing functionality, and to monitor things from a single shared resource. This is supported by adding the PhysicsDiagnosticsPlugin with the bevy_diagnostic feature enabled.

There is some boilerplate required for registering the diagnostics, clearing the timers and counters, and writing them to the DiagnosticsStore. To keep things more manageable, this has been abstracted with a PhysicsDiagnostics trait, register_physics_diagnostics method, and impl_diagnostic_paths! macro.

For the earlier SolverDiagnostics, the implementation looks like this:

impl PhysicsDiagnostics for SolverDiagnostics {
    fn timer_paths(&self) -> Vec<(&'static DiagnosticPath, Duration)> {
        vec![
            (Self::INTEGRATE_VELOCITIES, self.integrate_velocities),
            (Self::WARM_START, self.warm_start),
            (Self::SOLVE_CONSTRAINTS, self.solve_constraints),
            (Self::INTEGRATE_POSITIONS, self.integrate_positions),
            (Self::RELAX_VELOCITIES, self.relax_velocities),
            (Self::APPLY_RESTITUTION, self.apply_restitution),
            (Self::FINALIZE, self.finalize),
            (Self::STORE_IMPULSES, self.store_impulses),
            (Self::SWEPT_CCD, self.swept_ccd),
        ]
    }

    fn counter_paths(&self) -> Vec<(&'static DiagnosticPath, u32)> {
        vec![(
            Self::CONTACT_CONSTRAINT_COUNT,
            self.contact_constraint_count,
        )]
    }
}

impl_diagnostic_paths! {
    impl SolverDiagnostics {
        INTEGRATE_VELOCITIES: "avian/solver/integrate_velocities",
        WARM_START: "avian/solver/warm_start",
        SOLVE_CONSTRAINTS: "avian/solver/solve_constraints",
        INTEGRATE_POSITIONS: "avian/solver/integrate_positions",
        RELAX_VELOCITIES: "avian/solver/relax_velocities",
        APPLY_RESTITUTION: "avian/solver/apply_restitution",
        FINALIZE: "avian/solver/finalize",
        STORE_IMPULSES: "avian/solver/store_impulses",
        SWEPT_CCD: "avian/solver/swept_ccd",
        CONTACT_CONSTRAINT_COUNT: "avian/solver/contact_constraint_count",
    }
}

The SolverPlugin can then simply call app.register_physics_diagnostics::<SolverDiagnostics>(), and everything should work automatically. The timers will only be written to the DiagnosticsStore if PhysicsDiagnosticsPlugin is enabled, keeping overhead small if the use of DiagnosticsStore is not needed.

A nice benefit here is that each plugin is responsible for adding its own diagnostics, rather than there being a single place where all diagnostics are registered and stored. This is nice for modularity, and means that e.g. SpatialQueryDiagnostics are only added if the SpatialQueryPlugin is added.

Physics Diagnostics UI

Having all of these diagnostics available is nice, but viewing and displaying them in a useful way involves a decent amount of code and effort.

To make this easier (and prettier), an optional debug UI for displaying physics diagnostics is provided with the diagnostic_ui feature and PhysicsDiagnosticsUiPlugin. It displays all active built-in physics diagnostics in neat groups, with both current and smoothed times shown.

Diagnostics UI

The visibility and settings of the UI can be configured using the PhysicsDiagnosticsUiSettings.

Example Improvements

The ExampleCommonPlugin has been updated to replace the FPS counter with these physics diagnostics, and there are now text instructions to show what the different keys do.

move_marbles example

The diagnostics UI is hidden by default in the examples.

Differences to #576

#576 by @ptsd has an initial WIP diagnostics implementation with a simpler approach that more closely matches my original proposal in #564. It was a valuable base to build this PR on top of, but as I iterated on it, I ultimately went with quite a different approach.

That PR used a single PhysicsDiagnosticsPlugin that set up all diagnostics manually. Timers were implemented by adding system sets before and after various parts of the simulation, and adding systems there to record spans, which were then written to the DiagnosticsStore.

I didn't go with this approach, because:

  • Adding so many system sets just for diagnostics didn't feel very appealing
  • Adding two systems for each span adds more overhead, and might not provide as accurate timing information as just tracking time inside the actual systems
  • The spans in that PR were not suitable for substepping, as they didn't support accumulating time over several substeps
  • Registering all diagnostics in a single plugin makes things less modular, and means that you might end up with unnecessary diagnostics

Instead, I simply have each plugin define its own resource for its diagnostics where relevant. Timers are handled by tracking elapsed time inside systems with bevy::utils::Instant (for wide platform support), and stored as Durations. Writing this information to the DiagnosticsStore is optional.

This is more modular, has less overhead, and works with substepping. It does add some complexity to the actual diagnostic management though, and makes diagnostics more spread out over the codebase, for better and for worse.

ptsd and others added 14 commits December 5, 2024 22:14
- Diagnostics are now stored in resources like `SolverDiagnostics` and `CollisionDiagnostics`.
- Timers and counters are updated in the relevant systems, instead of wrapping every timed system with two other system.
- Each plugins is responsible for setting up its own diagnostics, but a `PhysicsDiagnostic` trait and `App::register_physics_diagnostics` are used to abstract setup logic for `Diagnostics`.
- Added a lot more diagnostics.
@Jondolf Jondolf added C-Enhancement New feature or request C-Performance Improvements or questions related to performance labels Feb 8, 2025
@Jondolf Jondolf added this to the 0.3 milestone Feb 8, 2025
@Jondolf
Copy link
Owner Author

Jondolf commented Feb 8, 2025

Box2D's similar timers and counters, for reference:

Box2D's diagnostics

@Jondolf Jondolf merged commit 95bbd06 into main Feb 8, 2025
5 checks passed
@Jondolf Jondolf deleted the diagnostics branch February 8, 2025 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Enhancement New feature or request C-Performance Improvements or questions related to performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add physics diagnostics
2 participants