while playing around with ECS I have run into a performance problem due to chunk fragmentation.
Here’s how one can reproduce the problem (the problem already occurs in step 1, but the other steps make it worse):
Create new entities and add component data. Depending on the amount of entities spawned, almost all new chunks will be filled completely (as long as more entities are created than fit into a single chunk). Existing chunks that still have free space will not be used.
In the following image, I only spawned 10 entities per frame causing multiple chunks to be created that each hold 10 entities.
Randomly destroy half of the entities. This will result in all chunks to be filled only half as much as before (on average).
Create new entities. Again, these will not fill the existing chunks. Instead, new chunks will be created.
Repeating these steps leads to more fragmentation that will not be resolved automatically.
What I’d like to know is whether there’s a way to perform the required defragmentation automatically, or rather a way that makes new entities use existing chunks instead of creating new ones. Also, since I’m quite new to ECS, maybe I’m just not using it correctly?
There’s an older topic discussing fragmentation, however it seems to assume that new entities will fill existing chunks:
I have tested your 1. (create then add for each single entity without archetype) but I got expected result
[UnityTest]
public IEnumerator ChunkUtilization2()
{
var w = new World("Test World");
var em = w.EntityManager;
for (int i = 0; i < 1000; i++)
{
var e = em.CreateEntity();
em.AddComponent<TakeSpace>(e);
}
yield return new WaitForSeconds(3);
w.Dispose();
}
The TakeSpace component is 8x float4 = 128 + Entity = 136 bytes each. Therefore one chunk fits 16000 / 136 = 118 entities. This graph means there are total of 9 chunks. 8 chunks has 118 full entities. 1 chunk has about half of 118.
I see WorldRenderBounds in your screenshot. Maybe your Entities has some SCD from hybrid renderer added (for material, culling, etc.) that constrain the chunk from being merged.
Can it have something to do with chunk components? You have a WorldRenderBounds component so I guess you are instantiating an entity that should be rendered. The renderer system sets a ChunkWorldRenderBounds as a chunk component. I’ve also noticed that dynamic renderers have very bad chunk utilization.
You’re both right - I totally missed the ChunkWorldRenderBounds component that is attached to the entities and indeed prevents new entities from being added to the existing chunks.
Since I’m spawning entities at random position in the world, I suspect that these bounds don’t hold any useful data anyway, unless they are automatically grouped together?
I’m not sure how to remove chunk components in jobs, since the buffers don’t seem to have a corresponding method, however, the following approach worked for me:
Add a marker component to new entities.
Have a system iterate over all those entities and remove the marker component along with the “ChunkWorldRenderBounds” component.
Removing the chunk component doesn’t seem to change anything, so I disabled it.
It still feels like there should be another way to do this.
If this removal of the marker component is done each frame, it won’t work, but manually triggering it from time to time will. Then I tried a two staged approach which worked fine calling it each frame (because it introduces a delay of one frame):
If you have WithStructuralChanges then it is not in a jobs anyway. They are all on main thread and not bursted. So you better try using the overload that take EntityQuery. (EntityManager.RemoveChunkComponentData) Then make EQ that has NewbornMarker to give to it. (Or just EQ with the chunk component data type)
DefragQuery1 = GetEntityQuery(ComponentType.ReadWrite<NewbornMarker>());
DefragQuery2 = GetEntityQuery(ComponentType.ReadWrite<NewbornMarker2>());
....
// Remove the second marker from all entities that currently have it
entityManager.RemoveComponent<NewbornMarker2>(DefragQuery2);
// Entities having the first marker will get the second...
entityManager.AddComponent<NewbornMarker2>(DefragQuery1);
// ... and get rid of the first
entityManager.RemoveComponent<NewbornMarker>(DefragQuery1);
Maybe that’s because the EntityQuery methods are optimized to work on whole chunks? Anyway, the first example works and shouldn’t be too slow as long as there aren’t a lot of entities added per frame. Thanks again for your help.