mojira.dev
MC-151802

Spawning causes massive TPS CPU load on a flat world.

Hi,

There is a problem with spawning code, which can be clearly visible on the following clip:
https://youtu.be/GGIoeO5btng

tl;dr; in 1.14 A LOT has been added to entity contructor, which causes lots of CPU. Most mobs won't spawn anyways, so e.g. initializing their brains etc at Constructor rather than with some init() method afterwards is not a good idea. Second component to the CPU drain are collision checks (even before entity is created). 1.14 collision check code seems to be much more complex comparing to 1.13, and despite 1.13 calling it twice for each mob, one call in 1.14 costs about 10 times more. 

Reproduction steps: create a flat world with one block (white concrete, why not), press Alt F3, observe TPS graph, ->doMobSpawning false, observe graph.

Evidences:
Full code (replaced factory method with simple cow constructor for clarity - no effect on the final result)

[See Exhibit 1]

Commented out collision check with predefined BBs (without creating a mob)

[See Exhibit 2]

Commented out entity creation code - taking a sample cow instead (so skipping CTOR)

[See Exhibit 3]

Linked issues

Attachments

Comments 2

To overcome high memory turnaroundn and high CPU usage problem in creating mobs, I propose to use a level-specific cache of one mob per mob type and until they are not spawned, they remain in the cache, and get reused to check the spawning conditions.
On top of that, to combat high CPU requirements to measure block collisions, since most worlds are created mainly with solid and 'non-collidable' blocks, I propose an extra check if a spawn mob is in a non-collidable area. This on the other end is very cheap and allows to accept most cases quickly. 

You might need to test this solution, but I don't think its incorrect:
https://youtu.be/hxWk72xfAjY 

The following test were performed based on this partifular modification to the source code. 

 

@Mixin(SpawnHelper.class)
public class SpawnHelper_lagfreeMixin
{
    // in World: private static Map<EntityType, Entity> precookedMobs= new HashMap<>();

    @Redirect(method = "spawnEntitiesInChunk", at = @At(
            value = "INVOKE",
            target = "Lnet/minecraft/world/World;doesNotCollide(Lnet/minecraft/util/math/BoundingBox;)Z"
    ))
    private static boolean doesNotCollide(World world, BoundingBox bb)
    {
        //.doesNotCollide is VERY expensive. On the other side - most worlds are not made of trapdoors in
        // various configurations, but solid and 'passable' blocks, like air, water grass, etc.
        // checking if in the BB of the entity are only passable blocks is very cheap and covers most cases
        // in case something more complex happens - we default to full block collision check
        if (CarpetSettings.b_lagFreeSpawning)
        {
            BlockPos.Mutable blockpos = new BlockPos.Mutable();
            for (int y = (int)bb.minY, maxy = (int)Math.ceil(bb.maxY); y < maxy; y++)
                for (int x = (int)bb.minX,  maxx = (int)Math.ceil(bb.maxX); x < maxx; x++)
                    for (int z = (int)bb.minZ, maxz = (int)Math.ceil(bb.maxZ); z < maxz; z++)
                    {
                        blockpos.set(x, y, z);
                        VoxelShape box = world.getBlockState(blockpos).getCollisionShape(world, blockpos);
                        if ( box == VoxelShapes.empty())
                            continue;
                        if (Block.isShapeFullCube(box))
                            return false;
                        return world.doesNotCollide(bb);
                    }
            return true;
        }
        return world.doesNotCollide(bb);
    }

    @Redirect(method = "spawnEntitiesInChunk", at = @At(
            value = "INVOKE",
            target = "Lnet/minecraft/entity/EntityType;create(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;"
    ))
    private static Entity create(EntityType<?> entityType, World world_1)
    {
        if (CarpetSettings.b_lagFreeSpawning)
        {
            Map<EntityType, Entity> precookedMobs = ((WorldInterface)world_1).getPrecookedMobs();
            if (precookedMobs.containsKey(entityType))
                //this mob has been <init>'s but not used yet
                return precookedMobs.get(entityType);
            Entity e = entityType.create(world_1);
            precookedMobs.put(entityType, e);
            return e;
        }
        return entityType.create(world_1);
    }

    @Redirect(method = "spawnEntitiesInChunk", at = @At(
            value = "INVOKE",
            target = "Lnet/minecraft/world/World;spawnEntity(Lnet/minecraft/entity/Entity;)Z"
    ))
    private static boolean spawnEntity(World world, Entity entity_1)
    {
        if (CarpetSettings.b_lagFreeSpawning)
            // we used the mob - next time we will create a new one when needed
            ((WorldInterface) world).getPrecookedMobs().remove(entity_1.getType());
        return world.spawnEntity(entity_1);
    }
}

To add some data points to the discussion: run simple experiments in a regular world and 1 high flat world. Spawning cost in tested environments were using 1.3% and 62% of the 50ms tick. After fixing these two solutions: 0.74% and 7.2% respectively.

gnembon

boq

Confirmed

(Unassigned)

Minecraft 1.14.1, Minecraft 1.14.2 Pre-Release 2, Minecraft 1.14.2 Pre-Release 3, Minecraft 1.14.2 Pre-Release 4, Minecraft 1.14.2

Minecraft 1.14.3 Pre-Release 1

Retrieved