# 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 `/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 [ ]` | 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.