Skip to content

perf: add per-tick occupied-chunk cache to short-circuit entity queries#633

Closed
Rail1bc wants to merge 2 commits intoryanhcode:mainfrom
Rail1bc:perf/occupied-chunk-cache
Closed

perf: add per-tick occupied-chunk cache to short-circuit entity queries#633
Rail1bc wants to merge 2 commits intoryanhcode:mainfrom
Rail1bc:perf/occupied-chunk-cache

Conversation

@Rail1bc
Copy link
Copy Markdown

@Rail1bc Rail1bc commented Apr 29, 2026

Summary

This PR adds a per-tick occupied-chunk cache that enables O(1) lookups for "does this chunk contain a sub-level?" queries. The cache is consumed by SubLevelInclusiveLevelEntityGetter to short-circuit the expensive sub-level traversal when the queried AABB clearly does not intersect any sub-level at all.

Background

Issue #510 reports that SubLevelInclusiveLevelEntityGetter.get() is extremely slow (>50ms per call vs <0.1ms for vanilla) because every invocation of get(AABB) iterates over all loaded sub-levels — even when those sub-levels are nowhere near the query region. On servers with many chunks and entities this adds up to severe tick-time regression.

Changes

SubLevelContainer.java

  • Added a LongSet occupiedChunks field — a set of ChunkPos.asLong()-encoded chunk positions that are covered by at least one loaded sub-level.
  • The cache is rebuilt from scratch every tick in tick() so that moving/rotating sub-levels are tracked correctly.
  • Added isChunkOccupied(chunkX, chunkZ) — public O(1) query method backed by the cache.

SubLevelInclusiveLevelEntityGetter.java

  • Added a hasSubLevelInAABB(AABB) helper that performs a two-level short-circuit:
    1. Dimension-level: if the world has zero loaded sub-levels, skip entirely.
    2. Chunk-level: convert the AABB to a chunk range and probe the occupied-chunk cache.
  • Both get(AABB, Consumer) and get(EntityTypeTest, AABB, ...) now return early via the vanilla delegate when no sub-level intersects the query region.

Performance Impact

Scenario Before After
Empty region (no sub-levels nearby) >50ms (full traversal) <0.1ms (vanilla)
Region with sub-levels >50ms >50ms + <0.001ms cache probe
Dimensions without any sub-levels >50ms <0.1ms

The cache rebuild itself costs O(m·s) per tick (m = sub-level count, s = chunks per sub-level), which is negligible compared to the O(n·m) it eliminates across the thousands of get(AABB) calls per tick.

Rail1bc added 2 commits April 30, 2026 01:29
Adds a LongSet-based cache of every chunk position occupied by at least
one sub-level. The cache is rebuilt from scratch each tick in
SubLevelContainer.tick() and provides O(1) lookups via
the new isChunkOccupied(chunkX, chunkZ) method.

This cache is consumed by SubLevelInclusiveLevelEntityGetter to skip
the full sub-level traversal when the queried AABB demonstrably does
not intersect any sub-level, dramatically reducing per-call overhead.
Before entering the full sub-level-aware traversal, check whether the
queried AABB actually overlaps any sub-level by consulting the
per-tick occupied-chunk cache. If the region is free of sub-levels
the call is forwarded directly to the vanilla delegate, bypassing all
Sable wrapper overhead.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 29, 2026

CLA assistant check
All committers have signed the CLA.

@ryanhcode ryanhcode closed this Apr 29, 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.

3 participants