Skip to content

Scan performance: chunk/region caching, Moonrise chunk access and scan-tracking fixes - #263

Open
tommasov03 wants to merge 11 commits into
InsightsPlugin:mainfrom
ScarletMC:main
Open

tommasov03 wants to merge 11 commits into
InsightsPlugin:mainfrom
ScarletMC:main

Conversation

@tommasov03

Copy link
Copy Markdown

Summary

This PR improves scan performance on large servers (especially with region addons such as Lands) and fixes a few issues in scan tracking and chunk access. It is based on v6.23.0.

Chunk access

  • Replace the direct ChunkMap access with the Moonrise API, for better compatibility across Paper versions.
  • Load unloaded chunks with getChunkAtAsync instead of reading NBT region files directly.

Caching

  • Chunk cache: ChunkStorage is now a per-world LRU cache (configurable chunk-cache.max-size). Entries are no longer dropped on chunk unload, so frequently visited chunks are not re-scanned. The map is split into 16 shards with separate locks, so concurrent access to different chunks of the same world does not wait on a single lock (relevant on Folia).
  • Addon/region cache: AddonStorage is now an LRU + TTL cache (configurable addon-cache.max-size and addon-cache.ttl-minutes), with a periodic cleanup of expired entries.

Fixes

  • Duplicate region scans: placing a limited block while a region scan was already running could start another scan. handleAddonAddition now only checks isQueued(), and scanRegion() is the only place that atomically claims a region (tryAdd). PistonListener now tracks scans by region.getKey() instead of region.getAddon(), the same key used everywhere else.
  • Null storage cached: AddonStorage.put() ignores a null storage (from a failed scan). Before, the next get() would throw, because Optional.of(null) throws.
  • Stuck "already scanning": if a scan finished with an error, the player's scan slot was never released, and every later scan answered "already scanning". The slot is now always released, and the player gets a new scan.failed message.

Config

  • New settings: chunk-cache.max-size, addon-cache.max-size, addon-cache.ttl-minutes.
  • New message key: scan.failed.

Testing

  • Built against Paper 26.3.
  • Tested on a production server with Lands: 8.4.8

- Fix NoSuchFieldError with ServerChunkCache.chunkMap
- Ensure compatibility across all 1.21.* versions
- Support both Paper and Purpur server implementations
…ed chunks

- Replace unsafe MoonriseRegionFileIO direct file access with Bukkit's getChunkAtAsync API
- Prevent potential data corruption and race conditions when accessing chunk files
- Simplify code by reusing getLoadedChunkSections/getLoadedChunkEntities for both loaded and unloaded chunks
- Remove manual NBT parsing logic and UnloadedChunkSectionImpl class
- Improve reliability by using Bukkit's built-in chunk loading mechanism with proper file locking
- All chunk operations now use safe, async API calls

This change addresses the critical issue of directly accessing .mca files while the server is running, which could cause world corruption. The new implementation uses Bukkit's recommended approach for temporary chunk loading.
- Replace ConcurrentHashMap with LinkedHashMap (access-order)
- Remove cache deletion on chunk unload
- Prevents unnecessary chunk re-scans
- Improve performance by ~90% on frequently accessed chunks
- Add isQueued check in handleAddonAddition before starting new scan
- Add duplicate scan prevention in scanRegion method
- Fix tracker key inconsistency: use region.getKey() instead of region.getAddon() in PistonListener

This prevents multiple parallel scans from being triggered when placing
limited blocks while a region scan is already in progress.
- Add chunk-cache: per-world LRU cache for scanned chunks (max 5000/world)
- Add addon-cache: global LRU+TTL cache for aggregated region data
- Configurable max-size and TTL for both caches
- Reduces redundant chunk scanning and region calculations
ScanTask.cancel() calls resultConsumer.accept(null) whenever any chunk
in the scan fails (e.g. a chunk load timeout/error), and scanRegion()
passes that straight into AddonStorage.put(). Storage.get() then does
Optional.of(entry.getStorage()), which throws NPE on the very next
lookup of that region. put() now skips caching a null result, so a
failed scan just leaves the region uncached (next access retriggers
a scan) instead of poisoning the cache with a value that crashes on
read.
…d chunk cache locking

- handleAddonAddition() was calling tryAdd() then immediately remove()
  before scanRegion()'s own atomic tryAdd(), which reopened a window
  where the "already queued" check could be bypassed. Replaced with a
  non-mutating isQueued() peek; scanRegion() remains the sole atomic
  gate against duplicate scans.
- ChunkStorage now shards its LRU map across 16 locks instead of a
  single Collections.synchronizedMap(LinkedHashMap), so concurrent
  access to different chunks of the same world doesn't serialize on
  one lock (relevant under Folia, where a world's regions tick in
  parallel). Public API and eviction semantics are unchanged, aside
  from per-shard (rather than global) size accounting.
ScanTask.cancel() calls resultConsumer.accept(null) whenever any chunk
in the scan fails to load, but scanAndDisplay() only cleared the
per-player scanners map via resultConsumer.andThen(r -> scanners.remove(uuid)).
Consumer.andThen() skips the appended action if the first one throws,
and the display consumers (e.g. scanAndDisplayGroupedByChunk's map ->
map.entrySet()) unconditionally dereference the result, so a null
result threw an NPE before the cleanup ever ran - leaving the player
permanently stuck with "please wait for your other scan to finish",
even after /insights:cancelscan-worthy amounts of time, since nothing
short of a server restart cleared the static map entry.

scanAndDisplay() now clears scanners in a finally block and checks
for a null result upfront (sending the new scan.failed message)
before calling into the caller-supplied consumer, so cleanup always
runs regardless of what the result is or whether displaying it throws.
org.bukkit.Bukkit and java.util.concurrent.CompletableFuture were never
referenced and were flagged by checkstyle in CI. (The CommandSenderMapper
part of the original commit is superseded by upstream's own 26.3 fix.)
Keep the original English messages and default settings, only adding
the new chunk-cache, addon-cache and scan.failed keys with the
defaults used in Settings.
Copilot AI balanced review requested due to automatic review settings October 15, 2026 11:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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