first commit
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user