-
-
Notifications
You must be signed in to change notification settings - Fork 0
Research Instance Container
Question. Build an InstanceContainer that is "1:1 compatible with Minestom" but faster and more
maintainable than the original, developed with DRY, SOLID and TDD, using current Java 25 features.
This page records what that investigation found; it describes neither shipped behaviour nor a
benchmark result.
Method. Agents reading the Minestom sources at 2026.06.20-26.1.2, javap -v and reflection
against the binary jar of that version, and probe code compiled and run against it. The probes live
outside this repository and are not checked in. The concurrency section additionally quotes figures
from standalone probes whose machine, run configuration and uncertainty were not recorded — those are
marked where they appear and must not be read as benchmark results.
Established, and durable. That Instance and InstanceContainer are not sealed and can be
extended from a foreign package, proven end to end against a real MinecraftServer.init(). That
seven instanceof / cast sites in Minestom couple behaviour to the concrete InstanceContainer
type, four of them silently. That the chunk lifecycle hooks are protected and unreachable from a
foreign package. That the chunk and entity tick parallelism lives in the global ThreadDispatcher
of ServerProcessImpl, not in the instance, so replacing the instance cannot make ticking faster.
These are checkable against the same jar and do not depend on who ran the investigation.
Open, or not established. Every performance figure below. None was produced by the JMH harness, none has a recorded machine or run configuration, and none can be reproduced from this repository. The direction of the locking findings is supported by the structure of the code; the magnitudes are not supported by anything a reader can re-run.
True at. Written 2026-07-31, entered the repository at
fc0aef5, against Minestom
2026.06.20-26.1.2. The structural claims were re-checked against that jar at ca79507. Nothing
in this document was implemented as written — what falco-instance became is described in
Rationale: Instances and Chunks, and the module claims no speed
advantage.
Unlike Palette, there is no sealing here. Verified with javap -v and reflection against the
actual jar:
| Type | Modifiers | Sealed? |
|---|---|---|
Palette |
public interface |
yes — PermittedSubclasses: PaletteImpl
|
Instance |
public abstract class |
no, permitted=null
|
InstanceContainer |
public class |
no, not final, not abstract |
Both class FalcoInstanceA extends InstanceContainer and class FalcoInstanceB extends Instance
compile. InstanceManager.registerInstance(Instance) is public, and the Instance Javadoc
explicitly invites custom implementations.
An agent wrote a complete ForeignInstance extends Instance in a foreign package — all 19 abstract
methods — compiled it and ran it against a real MinecraftServer.init():
[1] constructed custom Instance: falcoprobe.ForeignInstance
[2] registerInstance() OK, isRegistered=true
[4] loadChunk(0,0) -> DynamicChunk
[5] getBlock(1,64,1) = minecraft:diamond_block
[6] tick() OK
The scale is manageable: InstanceContainer is 776 lines, 58 methods, 16 fields. Instance itself
contributes 103 methods that are inherited for free (world border, weather, clocks, entity tracker,
scheduler, event node, snapshots). The 776 lines and the 19 abstract methods were re-checked at
ca79507; the method and field counts were not, and are the investigation's own figures.
Seven instanceof / cast sites in the whole Minestom codebase couple behaviour to the concrete
InstanceContainer type — SharedInstance.java:143, :145 and :154, InstanceManager.java:121,
Chunk.java:74, AbsoluteBlockBatch.java:144 and ChunkBatch.java:235, at version
2026.06.20-26.1.2, re-checked at ca79507. Four of them silently take a different path for a
foreign implementation — no exception, no warning:
| Site | Effect on a foreign implementation |
|---|---|
InstanceManager.unregisterInstance |
Chunks are not unloaded and partitions not deleted. Reproduced: before unregister: chunks=1 → after unregister: chunks STILL loaded = 1. A real leak. |
SharedInstance |
Field and constructor are typed on InstanceContainer. Reproduced: createSharedInstance(customInstance) → ClassCastException. areLinked always returns false. |
Chunk constructor |
Computes its shared-viewer list via instance instanceof InstanceContainer. A foreign instance gives every chunk a permanently empty list, so SharedInstance players never see chunk updates. |
ChunkBatch / AbsoluteBlockBatch
|
refreshLastBlockChangeTime() is only called on InstanceContainer, so batch/copy semantics drift. |
Silent divergence is worse than a compile error, because it surfaces as a bug much later.
There is a second obstacle: the chunk lifecycle hooks are protected and unreachable from a
foreign package. Deriving from InstanceContainer and overriding the hot paths does not work.
An escape hatch exists — a custom Chunk subclass re-exposing the protected hooks — and it compiles,
but it means the replacement drags a chunk implementation along.
The chunk and entity tick parallelism does not live in InstanceContainer. It lives in the global
ThreadDispatcher of ServerProcessImpl, which defaults to a single thread. A replacement container
changes nothing about it.
What the analysis did find is a real and severe concurrency problem — but in locking, not in threading. The findings split cleanly into what is readable from the source and what rests on a probe, and they are separated here for that reason.
Readable from the source, and therefore the part that carries:
-
The instance monitor serialises all block writes.
setBlockholds it for the whole write, so writes to unrelated chunks cannot proceed in parallel. -
LightingChunktakes the same instance monitor, so a relight blocks every writer in the instance for its duration. -
4 of 9 mutable fields are unsynchronised (verified with
javap). -
currentlyChangingBlocksis aHashMapguarded by two different locks;changingBlockLockprotects nothing. -
The chunk locks are
asserts — without-eathey are inert in production, and Minestom violates them itself. -
Long2ObjectSyncMaprebuilds its dirty map after a promotion.InstanceContainer:77holds its chunks in one;Long2ObjectSyncMapImpl.dirtyLocked(flare-fastutil 2.0.1,:487-495) allocates a fresh map and copies every non-expunged entry of the read map into it. That is O(loaded chunks) on the first write after a promotion, and chunk loading and unloading are writes.
From standalone probes, with no recorded machine, run configuration or uncertainty, and not reproducible from this repository:
| Figure | What it was said to show |
|---|---|
| ≈400 000× | how much longer a setBlock waited behind a relight holding the instance monitor, in a constructed worst case |
| ≈1 600 000× | the same for a setBlock().join() executed under the instance monitor |
| up to 19.8× slower |
Long2ObjectSyncMap against ConcurrentHashMap in the probe's chunk-streaming access pattern |
Read the first two as "the wait is unbounded in the pathological case", which is what the structure of the code establishes on its own, and not as factors. The third is explicitly discarded by Rationale: Instances and Chunks: it came from a probe outside this repository, there is no instance benchmark here, and no argument in the wiki relies on it. Whether either structure wins for chunk streaming is unmeasured — Instance Performance specifies the benchmark that would settle it.
One earlier suspicion was refuted: retrieveChunk does not leak loadingChunks entries. It does
leave a permanent zombie chunk when unloadChunk races a running loadChunk, and
loadChunk().join() can then hand back a dead chunk.
Clarify the goal before building anything. Cleaner and more maintainable is very achievable — the original concentrates nine responsibilities in one class. Faster through multithreading is not achievable this way, because the parallelism sits in the dispatcher.
If it is built, then as FalcoInstance extends Instance in net.onelitefeather.falco.instance, not
as an InstanceContainer subclass, and with:
- an own unload path, because
InstanceManager.unregisterInstancedoes not clean up foreign types — this is the one defect that must not be inherited; - the protected-chunk barrier solved through an own
Chunksubclass, not through reflection; - all four
instanceofbreak points captured as explicit versioned compatibility tests, so a Minestom upgrade that adds a fifth is caught by CI; - the generator path not reimplemented in a first version.
Worthwhile sub-goals in order: a setBlock pipeline without the global synchronized, consistent
locking for currentlyChangingBlocks, and a chunk map suited to streaming.
falco-instance follows the first three recommendations and not the fourth: the generator path was
implemented after all. At ca79507, FalcoInstance.setGenerator stores the generator, generator()
returns it, and generateChunk runs it over clones of the chunk's palettes so a generator that fails
halfway leaves nothing behind (FalcoInstance.java:840-858, :875-895, :922ff).
What the module does not do is claim a speed advantage — the reason is the dispatcher finding above, and it is stated in that form in the README and in Rationale: Instances and Chunks. Where a gain could exist at all, and what would have to be measured before anyone said so, is catalogued in Instance Performance, which is a hypothesis and labelled as one.
- Minestom
2026.06.20-26.1.2, sources jar from the Gradle cache:instance/InstanceContainer.java,instance/Instance.java,instance/SharedInstance.java:143,:145,:154,instance/InstanceManager.java:121,instance/Chunk.java:74,instance/batch/AbsoluteBlockBatch.java:144,instance/batch/ChunkBatch.java:235. -
space.vectrix.flare:flare-fastutil:2.0.1,Long2ObjectSyncMapImpl.java:487-495. -
FalcoInstance— what the module became. - The sweep that extended the four coupling sites to twelve, and found
EntityTrackersealed, is in Research: Shared Instances and Batches.