Skip to content

Avoid allocations in the entity spawn listener hot path - #3528

Closed
bobhenl wants to merge 1 commit into
Multiverse:mainfrom
bobhenl:perf/spawn-listener-fast-path
Closed

bobhenl wants to merge 1 commit into
Multiverse:mainfrom
bobhenl:perf/spawn-listener-fast-path

Conversation

@bobhenl

@bobhenl bobhenl commented Oct 30, 2026

Copy link
Copy Markdown

Problem

PreCreatureSpawnEvent and CreatureSpawnEvent fire thousands of times per second on busy servers. Every call allocated several short-lived objects even when no world restricts spawning:

  • WorldManager#getLoadedWorld(World): three Option instances and two lambdas
  • Option#peek with a lambda capturing the event
  • SpawnCategoryMapper#getSpawnCategory: an Option plus the map lambda
  • EntitySpawnConfig#getSpawnCategoryConfig: computeIfAbsent with a capturing lambda

On a production Paper server with about 6,900 PreCreatureSpawnEvent/s, a spark profile showed MVEntityListener$1.onEvent at about 4 s of CPU per 25 minutes and over 6 MB/s of allocations. All worlds on that server allow every spawn, so all of this was overhead.

Changes

  • Add WorldManager#getLoadedWorldOrNull(World) (@ApiStatus.Internal) and WorldStore#getLoadedWorldOrNull(String). The existing Option methods now delegate to them.
  • Add package-private SpawnCategoryMapper#getSpawnCategoryOrNull(EntityType), used by EntitySpawnConfig#shouldAllowSpawn.
  • EntitySpawnConfig#getSpawnCategoryConfig does a plain get before falling back to computeIfAbsent.
  • MVEntityListener spawn handlers share one small helper that uses the null-returning lookups.

The spawn decision is unchanged: worlds not managed by Multiverse and entity types without a known spawn category still always allow spawning, and restricted worlds cancel exactly as before. No config or public API behaviour changes.

Testing

  • ./gradlew build passes, all 183 existing tests green (same as before the change).

@benwoo1110

Copy link
Copy Markdown
Member

And with this change, what is the percentage improvement compared to the previous implementation?

Also it will be good to provide how you logged those values, such as spark report links

@benwoo1110

benwoo1110 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

And doing the math, 4 seconds out of 25 minutes of CPU time (30k ticks) is averaging out to 4s / 30000 ticks = 0.133 ms/tick. So it's seems to be only taking 0.27% of each server tick on average which is not much of an issue.

@bobhenl

bobhenl commented Oct 30, 2026 •

Copy link
Copy Markdown
Author

Fair point, the CPU cost is small – your 0.27 %/tick math matches what I see. The motivation was mostly that it’s one of the hottest events on our server (~6.9k PreCreatureSpawnEvent/s) and every call allocated Option/lambda objects. In our alloc profiles Multiverse-Core is ~0.35 % of all allocations. So it’s a small cleanup of GC churn on a hot path, not a big win. I’ll post before/after spark links

Before:
CPU https://spark.lucko.me/amCqYCWpDA
alloc https://spark.lucko.me/0FUBMF9qKk

I'll share new ones soon

@benwoo1110

Copy link
Copy Markdown
Member

The entire reason of using optional api is to remove these types of null checks. 0.27 %/tick for like so many spawn events shows that its not a performance issue at all.

@benwoo1110 benwoo1110 closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants