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

128 lines
5.6 KiB
Markdown

# 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.