bevyengine / bevyengine/bevy

`specialized_material_pipeline_cache` grows forever, causing performance to greatly degrade over time

Open
#22,065 3 comments 0 reactions 0 assignees View on GitHub
A-Rendering C-Bug C-Performance D-Modest S-Needs-Design
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy 0.17.3

Features: `file_watcher`, `mp3`, `serialize`, `dynamic_linking`

Windows 10

This "performance leak" began occurring for me in Bevy 0.16, but persists to Bevy 0.17.3.

## The Bug

Over time, `specialized_material_pipeline_cache.len()` grows, increasing with no limit.

Each level in my game has a ton of mesh entities, which are all despawned upon unloading the level. Leaving and reloading one level a handful of times can drop FPS from ~230 to below 60, even though the entity count remains the same. Performance continues dropping the more I do it.

It seems very similar to https://github.com/bevyengine/bevy/issues/21526, but the issue wasn't fixed for me with 0.17.3. When I print `specialized_material_pipeline_cache.len()` in the `specialize_material_meshes` system, I can witness it growing in a similar manner, and the functions that actually start taking up frametime is similarly `specialize_material_meshes` and `queue_material_meshes`.

My game uses two cameras, but removing the second camera didn't stop this from occurring.

## Workaround

I'm using a local clone of Bevy 0.17.3, where I made a change to `bevy_pbr\src\material.rs` around line ~870:

Before:

```rust
entity_specialization_ticks.remove(&MainEntity::from(entity));
for view in views {
if let Some(cache) =
specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
{
cache.remove(&MainEntity::from(entity));
}
if let Some(cache) = specialized_prepass_material_pipeline_cache
.as_mut()
.and_then(|c| c.get_mut(&view.retained_view_entity))
{
cache.remove(&MainEntity::from(entity));
}
[...]
}
```

After:
```rust
for (_, cache) in specialized_material_pipeline_cache.iter_mut() {
cache.remove(&MainEntity::from(entity));
}

entity_specialization_ticks.remove(&MainEntity::from(entity));
for view in views {
if let Some(cache) = specialized_prepass_material_pipeline_cache
.as_mut()
.and_then(|c| c.get_mut(&view.retained_view_entity))
{
cache.remove(&MainEntity::from(entity));
}
[...]
}
```

This seems to have fully fixed the issue.

It caused me no immediately apparent performance loss.

However I have no idea what cold specialization is, nor do I know what exactly I've done. Just kinda seemed like the growth was a problem with it not being cleaned out so I thought iterating through it all and deleting everything unconditionally seemed good and wouldnt have consequences, um, so that's why I didn't make this a PR.

In my game, most of the times I'll despawn a mesh/entity is when I'm despawning almost everything and loading a new map, and I'm not sure if I ever modify a mesh/material after creating it, so perhaps that's why this workaround isn't causing me immediate issues.

Contributor guide

Open the contributing guide

Research direction

Start in bevy_pbr\src\material.rs around line 870 and trace specialize_material_meshes, queue_material_meshes, and the cleanup of specialized_material_pipeline_cache when entities are despawned. Compare the reported workaround with the existing view-based cleanup and investigate cold specialization before deciding the correct lifecycle behavior. Done means repeated level unloads and reloads no longer grow the cache or cause sustained frame-time degradation.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
game-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.