`specialized_material_pipeline_cache` grows forever, causing performance to greatly degrade over time
- 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
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