Skip to content

Render assets diagnostics #19311

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

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4e93746
Create `MeshAllocatorDiagnosticPlugin`
hukasu May 20, 2025
415dc70
Fix doc
hukasu May 20, 2025
da21313
Add more metrics to `MeshAllocatorDiagnosticPlugin`
hukasu May 20, 2025
36a6683
Add `RenderAssetDiagnosticPlugin`
hukasu May 20, 2025
eca3de2
Expose `DiagnosticPath` though the plugins
hukasu May 20, 2025
fb41396
First attempt at allocator leak detection
hukasu May 20, 2025
f229b3d
Create diagnostics for material allocations
hukasu May 20, 2025
a4054a9
Rename render asset leak test
hukasu May 20, 2025
08c5677
Add release note
hukasu May 20, 2025
ef7fa48
Fix wrong settings for test
hukasu May 20, 2025
e924106
Add test to check leaks through churn
hukasu May 20, 2025
0bc7dd1
Fix internal imports
hukasu May 20, 2025
d04208a
Add test that reproduces #18808
hukasu May 20, 2025
4ff946f
CONTENTIOUS: Run tests on linux through xvfb
hukasu May 20, 2025
93f7a3f
Rename release notes file
hukasu May 20, 2025
6a94ba4
Fix `MaterialDiagnosticPlugin` using `meshes` as suffix
hukasu May 20, 2025
e9f000c
Fix wrong name on release notes
hukasu May 20, 2025
dc2d1cc
Skip `render_asset_leak` tests on `ci test`
hukasu May 24, 2025
db9352b
Add CI for render asset leaks
hukasu May 24, 2025
8c7d72a
Remove duplicated comment
hukasu May 24, 2025
24f1fca
Update CI to run render asset tests
hukasu May 24, 2025
bf85333
Enable `WindowPlugin` but with no window
hukasu May 25, 2025
326b0ab
Refactor shared code into method
hukasu May 25, 2025
a8faad7
Add issues to ignore reason
hukasu May 25, 2025
e34c056
Merge branch 'main' into render-diagnostics
hukasu Jun 7, 2025
39eb074
Remove `ignore` from fixed tests
hukasu Jun 7, 2025
592f664
Merge branch 'main' into render-diagnostics
hukasu Jun 27, 2025
ec22af4
First attempt to use `NOOP`
hukasu Jun 27, 2025
370041f
Revert CI changes
hukasu Jun 27, 2025
e740b00
Revert one last CI change
hukasu Jun 27, 2025
d17b7c6
Unnecessary formatting
hukasu Jul 2, 2025
26a5bcf
Too much white space
hukasu Jul 2, 2025
cb159ef
Merge branch 'main' into render-diagnostics
hukasu Jul 2, 2025
156f596
Fix errors due to new dynamic materials API
hukasu Jul 2, 2025
00f06f9
`cargo test --benches` does not accept `--test-threads`
hukasu Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions crates/bevy_pbr/src/diagnostic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use core::{
any::{type_name, Any, TypeId},
marker::PhantomData,
};

use bevy_app::{Plugin, PreUpdate};
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
use bevy_ecs::{resource::Resource, system::Res};
use bevy_platform::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use bevy_render::{Extract, ExtractSchedule, RenderApp};

use crate::{Material, MaterialBindGroupAllocators};

pub struct MaterialAllocatorDiagnosticPlugin<M: Material> {
suffix: &'static str,
_phantom: PhantomData<M>,
}

impl<M: Material> MaterialAllocatorDiagnosticPlugin<M> {
pub fn new(suffix: &'static str) -> Self {
Self {
suffix,
_phantom: PhantomData,
}
}
}

impl<M: Material> Default for MaterialAllocatorDiagnosticPlugin<M> {
fn default() -> Self {
Self {
suffix: " materials",
_phantom: PhantomData,
}
}
}

impl<M: Material> MaterialAllocatorDiagnosticPlugin<M> {
/// Get the [`DiagnosticPath`] for slab count
pub fn slabs_diagnostic_path() -> DiagnosticPath {
DiagnosticPath::from_components(["material_allocator_slabs", type_name::<M>()])
}
/// Get the [`DiagnosticPath`] for total slabs size
pub fn slabs_size_diagnostic_path() -> DiagnosticPath {
DiagnosticPath::from_components(["material_allocator_slabs_size", type_name::<M>()])
}
/// Get the [`DiagnosticPath`] for material allocations
pub fn allocations_diagnostic_path() -> DiagnosticPath {
DiagnosticPath::from_components(["material_allocator_allocations", type_name::<M>()])
}
}

impl<M: Material> Plugin for MaterialAllocatorDiagnosticPlugin<M> {
fn build(&self, app: &mut bevy_app::App) {
app.register_diagnostic(
Diagnostic::new(Self::slabs_diagnostic_path()).with_suffix(" slabs"),
)
.register_diagnostic(
Diagnostic::new(Self::slabs_size_diagnostic_path()).with_suffix(" bytes"),
)
.register_diagnostic(
Diagnostic::new(Self::allocations_diagnostic_path()).with_suffix(self.suffix),
)
.init_resource::<MaterialAllocatorMeasurements<M>>()
.add_systems(PreUpdate, add_material_allocator_measurement::<M>);

if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(ExtractSchedule, measure_allocator::<M>);
}
}
}

#[derive(Debug, Resource)]
struct MaterialAllocatorMeasurements<M: Material> {
slabs: AtomicUsize,
slabs_size: AtomicUsize,
allocations: AtomicU64,
_phantom: PhantomData<M>,
}

impl<M: Material> Default for MaterialAllocatorMeasurements<M> {
fn default() -> Self {
Self {
slabs: AtomicUsize::default(),
slabs_size: AtomicUsize::default(),
allocations: AtomicU64::default(),
_phantom: PhantomData,
}
}
}

fn add_material_allocator_measurement<M: Material>(
mut diagnostics: Diagnostics,
measurements: Res<MaterialAllocatorMeasurements<M>>,
) {
diagnostics.add_measurement(
&MaterialAllocatorDiagnosticPlugin::<M>::slabs_diagnostic_path(),
|| measurements.slabs.load(Ordering::Relaxed) as f64,
);
diagnostics.add_measurement(
&MaterialAllocatorDiagnosticPlugin::<M>::slabs_size_diagnostic_path(),
|| measurements.slabs_size.load(Ordering::Relaxed) as f64,
);
diagnostics.add_measurement(
&MaterialAllocatorDiagnosticPlugin::<M>::allocations_diagnostic_path(),
|| measurements.allocations.load(Ordering::Relaxed) as f64,
);
}

fn measure_allocator<M: Material + Any>(
measurements: Extract<Res<MaterialAllocatorMeasurements<M>>>,
allocators: Res<MaterialBindGroupAllocators>,
) {
if let Some(allocator) = allocators.get(&TypeId::of::<M>()) {
measurements
.slabs
.store(allocator.slab_count(), Ordering::Relaxed);
measurements
.slabs_size
.store(allocator.slabs_size(), Ordering::Relaxed);
measurements
.allocations
.store(allocator.allocations(), Ordering::Relaxed);
}
}
1 change: 1 addition & 0 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod cluster;
mod components;
pub mod decal;
pub mod deferred;
pub mod diagnostic;
mod extended_material;
mod fog;
mod light;
Expand Down
39 changes: 39 additions & 0 deletions crates/bevy_pbr/src/material_bind_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,45 @@ impl MaterialBindGroupAllocator {
}
}
}

/// Get number of allocated slabs for bindless material, returns 0 if it is
/// [`Self::NonBindless`].
pub fn slab_count(&self) -> usize {
match self {
Self::Bindless(bless) => bless.slabs.len(),
Self::NonBindless(_) => 0,
}
}

/// Get total size of slabs allocated for bindless material, returns 0 if it is
/// [`Self::NonBindless`].
pub fn slabs_size(&self) -> usize {
match self {
Self::Bindless(bless) => bless
.slabs
.iter()
.flat_map(|slab| {
slab.data_buffers
.iter()
.map(|(_, buffer)| buffer.buffer.len())
})
.sum(),
Self::NonBindless(_) => 0,
}
}

/// Get number of bindless material allocations in slabs, returns 0 if it is
/// [`Self::NonBindless`].
pub fn allocations(&self) -> u64 {
match self {
Self::Bindless(bless) => bless
.slabs
.iter()
.map(|slab| u64::from(slab.allocated_resource_count))
.sum(),
Self::NonBindless(_) => 0,
}
}
}

impl MaterialBindlessIndexTable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
use bevy_ecs::{resource::Resource, system::Res};
use bevy_platform::sync::atomic::{AtomicU64, AtomicUsize, Ordering};

use crate::{mesh::allocator::MeshAllocator, Extract, ExtractSchedule, RenderApp};

/// Number of meshes allocated by the allocator
static MESH_ALLOCATOR_SLABS: DiagnosticPath = DiagnosticPath::const_new("mesh_allocator_slabs");

/// Total size of all slabs
static MESH_ALLOCATOR_SLABS_SIZE: DiagnosticPath =
DiagnosticPath::const_new("mesh_allocator_slabs_size");

/// Number of meshes allocated into slabs
static MESH_ALLOCATOR_ALLOCATIONS: DiagnosticPath =
DiagnosticPath::const_new("mesh_allocator_allocations");

pub struct MeshAllocatorDiagnosticPlugin;

impl MeshAllocatorDiagnosticPlugin {
/// Get the [`DiagnosticPath`] for slab count
pub fn slabs_diagnostic_path() -> &'static DiagnosticPath {
&MESH_ALLOCATOR_SLABS
}
/// Get the [`DiagnosticPath`] for total slabs size
pub fn slabs_size_diagnostic_path() -> &'static DiagnosticPath {
&MESH_ALLOCATOR_SLABS_SIZE
}
/// Get the [`DiagnosticPath`] for mesh allocations
pub fn allocations_diagnostic_path() -> &'static DiagnosticPath {
&MESH_ALLOCATOR_ALLOCATIONS
}
}

impl Plugin for MeshAllocatorDiagnosticPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.register_diagnostic(
Diagnostic::new(MESH_ALLOCATOR_SLABS.clone()).with_suffix(" slabs"),
)
.register_diagnostic(
Diagnostic::new(MESH_ALLOCATOR_SLABS_SIZE.clone()).with_suffix(" bytes"),
)
.register_diagnostic(
Diagnostic::new(MESH_ALLOCATOR_ALLOCATIONS.clone()).with_suffix(" meshes"),
)
.init_resource::<MeshAllocatorMeasurements>()
.add_systems(PreUpdate, add_mesh_allocator_measurement);

if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(ExtractSchedule, measure_allocator);
}
}
}

#[derive(Debug, Default, Resource)]
struct MeshAllocatorMeasurements {
slabs: AtomicUsize,
slabs_size: AtomicU64,
allocations: AtomicUsize,
}

fn add_mesh_allocator_measurement(
mut diagnostics: Diagnostics,
measurements: Res<MeshAllocatorMeasurements>,
) {
diagnostics.add_measurement(&MESH_ALLOCATOR_SLABS, || {
measurements.slabs.load(Ordering::Relaxed) as f64
});
diagnostics.add_measurement(&MESH_ALLOCATOR_SLABS_SIZE, || {
measurements.slabs_size.load(Ordering::Relaxed) as f64
});
diagnostics.add_measurement(&MESH_ALLOCATOR_ALLOCATIONS, || {
measurements.allocations.load(Ordering::Relaxed) as f64
});
}

fn measure_allocator(
measurements: Extract<Res<MeshAllocatorMeasurements>>,
allocator: Res<MeshAllocator>,
) {
measurements
.slabs
.store(allocator.slab_count(), Ordering::Relaxed);
measurements
.slabs_size
.store(allocator.slabs_size(), Ordering::Relaxed);
measurements
.allocations
.store(allocator.allocations(), Ordering::Relaxed);
}
6 changes: 6 additions & 0 deletions crates/bevy_render/src/diagnostic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! For more info, see [`RenderDiagnosticsPlugin`].

pub(crate) mod internal;
mod mesh_allocator_diagnostic_plugin;
mod render_asset_diagnostic_plugin;
#[cfg(feature = "tracing-tracy")]
mod tracy_gpu;

Expand All @@ -16,6 +18,10 @@ use crate::{renderer::RenderAdapterInfo, RenderApp};
use self::internal::{
sync_diagnostics, DiagnosticsRecorder, Pass, RenderDiagnosticsMutex, WriteTimestamp,
};
pub use self::{
mesh_allocator_diagnostic_plugin::MeshAllocatorDiagnosticPlugin,
render_asset_diagnostic_plugin::RenderAssetDiagnosticPlugin,
};

use super::{RenderDevice, RenderQueue};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use core::{any::type_name, marker::PhantomData};

use bevy_app::{Plugin, PreUpdate};
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
use bevy_ecs::{resource::Resource, system::Res};
use bevy_platform::sync::atomic::{AtomicUsize, Ordering};

use crate::{
render_asset::{RenderAsset, RenderAssets},
Extract, ExtractSchedule, RenderApp,
};

pub struct RenderAssetDiagnosticPlugin<A: RenderAsset> {
suffix: &'static str,
_phantom: PhantomData<A>,
}

impl<A: RenderAsset> RenderAssetDiagnosticPlugin<A> {
pub fn new(suffix: &'static str) -> Self {
Self {
suffix,
_phantom: PhantomData,
}
}

pub fn render_asset_diagnostic_path() -> DiagnosticPath {
DiagnosticPath::from_components(["render_asset", type_name::<A>()])
}
}

impl<A: RenderAsset> Plugin for RenderAssetDiagnosticPlugin<A> {
fn build(&self, app: &mut bevy_app::App) {
app.register_diagnostic(
Diagnostic::new(Self::render_asset_diagnostic_path()).with_suffix(self.suffix),
)
.init_resource::<RenderAssetMeasurements<A>>()
.add_systems(PreUpdate, add_render_asset_measurement::<A>);

if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(ExtractSchedule, measure_render_asset::<A>);
}
}
}

#[derive(Debug, Resource)]
struct RenderAssetMeasurements<A: RenderAsset> {
assets: AtomicUsize,
_phantom: PhantomData<A>,
}

impl<A: RenderAsset> Default for RenderAssetMeasurements<A> {
fn default() -> Self {
Self {
assets: AtomicUsize::default(),
_phantom: PhantomData,
}
}
}

fn add_render_asset_measurement<A: RenderAsset>(
mut diagnostics: Diagnostics,
measurements: Res<RenderAssetMeasurements<A>>,
) {
diagnostics.add_measurement(
&RenderAssetDiagnosticPlugin::<A>::render_asset_diagnostic_path(),
|| measurements.assets.load(Ordering::Relaxed) as f64,
);
}

fn measure_render_asset<A: RenderAsset>(
measurements: Extract<Res<RenderAssetMeasurements<A>>>,
assets: Res<RenderAssets<A>>,
) {
measurements
.assets
.store(assets.iter().count(), Ordering::Relaxed);
}
Loading