Chunk Fragmentation

Hello,

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.
    5360451--542022--01 - After creation of 10 objects per frame.png

  • Randomly destroy half of the entities. This will result in all chunks to be filled only half as much as before (on average).
    5360451--542025--02 - After deleting half the entities.png

  • Create new entities. Again, these will not fill the existing chunks. Instead, new chunks will be created.
    5360451--542028--03 - After adding new entities again.png

  • 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.

5361861--542241--upload_2020-1-12_11-49-40.png

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.

1 Like

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.

1 Like

Thanks for your replies.

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.
Entities.ForEach((Entity instance, ref NewbornMarker marker) =>
{
  // Doesn't do anything? entityManager.RemoveChunkComponent<ChunkWorldRenderBounds>(instance);
  entityManager.RemoveComponent<NewbornMarker>(instance);
}).WithStructuralChanges().Run();
  • 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):

    Entities.ForEach((Entity instance, ref NewbornMarker2 marker) =>
    {
      entityManager.RemoveComponent<NewbornMarker2>(instance);
    }).WithStructuralChanges().Run();
    Entities.ForEach((Entity instance, ref NewbornMarker marker) =>
    {
      entityManager.AddComponent<NewbornMarker2>(instance);
      entityManager.RemoveComponent<NewbornMarker>(instance);
    }).WithStructuralChanges().Run();

It works, but I have no idea whether I’m doing this right at all.

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)

1 Like

It looks like using the EntityQuery methods don’t work in this case.

While the following works and doesn’t cause fragmentation (same as last post)

Entities.ForEach((Entity instance, ref NewbornMarker2 marker) =>
{
  entityManager.RemoveComponent<NewbornMarker2>(instance);
}).WithStructuralChanges().Run();
Entities.ForEach((Entity instance, ref NewbornMarker marker) =>
{
  entityManager.AddComponent<NewbornMarker2>(instance);
  entityManager.RemoveComponent<NewbornMarker>(instance);
}).WithStructuralChanges().Run();

the following still causes fragmentation:

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.