Files
chunkinspector/README.md
T
2026-07-31 10:46:40 +00:00

5.6 KiB

Chunk Inspector

Finds out why chunks stay loaded on a heavily modded Minecraft 1.21.1 / NeoForge server.

Every loaded chunk on a vanilla-or-modded server is loaded because some chunk ticket holds it, or because it is within propagation range of one. Chunk Inspector reads the live ticket table, replays the game's own level propagation, and writes a report naming the tickets, the chunks they keep alive, and — if you ask for it — the exact line of code that created each one.

Reports land in <levelDir>/chunk-summaries (i.e. next to level.dat in the world folder).

Two cost tiers

Always on -Dchunkinspector.deep=true
Bytecode injected into Minecraft none DistanceManager.addTicket, Ticket
Per-ticket cost zero one stack walk
Names the ticket type, level, age, owner key yes yes
Names the code that created the ticket no yes

The default tier is genuinely free: an access transformer widens a few ticket fields, and a snapshot walks them on demand. Nothing runs between snapshots. The instrumenting Mixins are not merely disabled without deep — a Mixin config plugin declines to apply them, so Minecraft's classes are left untouched.

Reach for deep when the always-on tier shows you which tickets leak but the owner key is opaque. Many tickets already identify themselves (a NeoForge forced chunk carries the requesting mod's id), so it is often unnecessary.

Commands

All require permission level 2.

Command What it does
/chunkinspector status Cheap in-chat summary of the current dimension
/chunkinspector why [<chunkX> <chunkZ>] Every ticket contributing to one chunk's level, strongest first. Defaults to the chunk you are standing in
/chunkinspector snapshot Writes a report now
/chunkinspector snapshot full ... including the expensive chunk-to-ticket attribution pass

A report is also written every interval ticks and once on shutdown. The shutdown one always runs the full analysis, because it is the one people come back to.

System properties

Pass these on the server's JVM command line, before -jar / @libraries/....

Property Default Meaning
chunkinspector.enabled true Master switch. When off, no hooks and no reports
chunkinspector.deep false Record the stack trace behind every ticket
chunkinspector.interval 600 Ticks between automatic snapshots; 0 disables them
chunkinspector.attribution false Run the chunk-to-ticket pass on periodic snapshots too
chunkinspector.retention 48 Snapshot files to keep; 0 keeps everything
chunkinspector.textReport true Also write the human-readable latest.txt
chunkinspector.stackDepth 24 Frames kept per call site (deep mode)
chunkinspector.maxOrigins 8192 Distinct call sites remembered (deep mode)
chunkinspector.sampleRate 1 Capture one stack per N ticket adds (deep mode)

Example — free monitoring, one snapshot per minute, a day's history:

-Dchunkinspector.interval=1200 -Dchunkinspector.retention=1440

Example — a full investigation:

-Dchunkinspector.deep=true -Dchunkinspector.attribution=true

Output

chunk-summaries/
  latest.json                            the newest report
  latest.txt                             the same thing, readable
  summary-20260731-103326-periodic.json  timestamped history, pruned to `retention`
  summary-20260731-103326-command.json
  summary-20260731-103340-shutdown.json

latest.txt of a server with a 4x4 /forceload:

══ minecraft:overworld ══  game time 528
  chunks: 113 loaded, 61 block-ticking, 25 entity-ticking (1741 tracked)
  tickets: 17 on 17 chunks, 17 never expire, 16 from /forceload

  by ticket type
    forced        16 tickets   16 chunks  level>=31  keeps 64 loaded  oldest 26s
    start          1 tickets    1 chunks  level>=30  keeps 49 loaded  oldest 26s

  most suspicious tickets
    chunk 103, 102       forced      level 31  chunk 103, 102
      -> permanent ticket, alive 26s, keeping 9 chunks loaded; entity-ticking, so mobs and machines run here

  loaded chunk clusters
       64 chunks  centre 101, 101  bounds 98, 98 .. 105, 105     forced x64
       49 chunks  centre 0, 0      bounds -3, -3 .. 3, 3         start x49

The JSON carries the same information plus everything that did not fit — see InspectionReport for the field-by-field documentation.

How to read it

  • suspects are tickets with no timeout that the game does not manage itself. Only these can leak forever. keeping N chunks loaded is a partition, not a sum: each loaded chunk is credited to the single strongest ticket holding it, so the numbers across tickets add up rather than overlap.
  • loaded chunk clusters group contiguous loaded chunks. A leak usually shows up as one cluster far from spawn with a single cause.
  • level is the vanilla chunk level: <=31 entity-ticking, <=32 block-ticking, <=33 full. An entity-ticking leak is far more expensive than a merely-loaded one.
  • unattributed lists loaded chunks no ticket explains. Normally empty; entries here mean something is holding chunks outside the ticket system.

Building

./gradlew build          # jar + unit tests
./gradlew e2eTest        # installs a real NeoForge dedicated server and asserts on its reports

e2eTest is deliberately not part of check: it downloads NeoForge and runs two servers end to end, taking a few minutes. It boots a dedicated server in each tier, force-loads a 4x4 square of chunks over the console, and verifies the reports name it.