-
-
Notifications
You must be signed in to change notification settings - Fork 0
Instances And Chunks
falco-instance is the third module. It provides an Instance that unloads its chunks when it is
unregistered, a Chunk that allocates its sections when something writes into them, and a
SharedInstance that keeps its settings to itself instead of writing them into the container every
other view is looking at.
This page is how to use it. Why it exists — the chunk leak that is the whole argument for it, and what it deliberately refuses to do — is Rationale: Instances and Chunks, and it is worth reading before depending on the module.
Take it if any of these apply:
-
You unregister worlds while the server runs.
InstanceManager#unregisterInstanceunloads chunks only for anInstanceContainer; every chunk, tick partition and entity of any other instance is left behind. This is the reason the module exists. -
You want the light engine to keep a chunk up to date without a listener of your own.
ChunkLightScheduler#supplier()producesFalcoChunks, and since1.0.0those carry Falco's light and Falco's lifecycle on one class. Before1.0.0this combination did not work at all. - Empty sections cost you. A chunk that allocates its sections on demand retains 25 objects and 840 bytes where Minestom's retains 192 and 6 848, and on a generated overworld 62.24 % of sections hold nothing. The tables and what they do not say are in Counted.
Keep using InstanceContainer if you need SharedInstance views onto the world you are building —
see Shared worlds for what is and is not possible — or if you are relying on any of
the four Minestom sites that branch on instanceof InstanceContainer.
No speed claim. Nothing here is faster, no benchmark in this repository says it is, and the memory figures above are counts rather than timings. The distinction is spelled out in No speed gain is claimed.
FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
.register(MinecraftServer.getInstanceManager());register builds the instance and registers it, in that order, and returns it. The builder is
immutable — every method returns a new one — so a half-configured builder can be handed around
without anybody changing it underneath.
The constructors are public as well if a builder is not what you want:
new FalcoInstance(UUID.randomUUID(), DimensionType.OVERWORLD).
FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key());
FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
.chunkLoader(loader)
.autoChunkLoad(true)
.ownsLoader(true)
.saveOnShutdown(true)
.registerAndShutdownWith(MinecraftServer.getInstanceManager(),
MinecraftServer.getSchedulerManager());registerAndShutdownWith is the form worth preferring when the instance owns its loader.
ownsLoader(true) means the instance closes the loader on shutdown, saveOnShutdown(true) means it
writes its chunks first, and the shutdown task is registered for you — the three together are the
part that is easy to get wrong by hand, because a loader closed before the save loses the save.
If you register by hand instead, shutdown(InstanceManager) does the same work and
unregister(InstanceManager) unregisters without saving.
This is the combination that did not work before 1.0.0:
// The scheduler takes the light service, not the instance — it binds to an instance later,
// through the chunks its supplier produces.
ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService());
FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
.chunkLoader(loader)
.chunkSupplier(scheduler.supplier())
.autoChunkLoad(true)
.register(MinecraftServer.getInstanceManager());The supplier produces FalcoLightingChunks, which extend FalcoChunk. Each one reports its block
changes, loads and ticks to the scheduler, and the scheduler lights the touched regions and sends
them. You write no listener.
falco-light declares falco-instance as compileOnly, so adding the light module does not bring
this one with it — declare both, or use the BOM. See Installation.
Minestom's own events still fire, and for most application code they are the right thing.
FalcoInstance dispatches InstanceChunkLoadEvent and InstanceChunkUnloadEvent, and the block
writer dispatches PlayerBlockBreakEvent, so a listener registered on the GlobalEventHandler
works here exactly as it does on an InstanceContainer. Take those unless you have a reason not to.
ChunkLifecycleListener is the other thing, and the difference is when, not what:
ChunkLifecycleListener |
InstanceChunkLoadEvent |
|
|---|---|---|
| when | during the transition, inside the lock | after it, once the chunk is published and the future completed |
| may call back into the instance | mostly no — stated per method | yes |
| a throw | fails the chunk load and reaches the caller waiting on it | does not |
| per block written |
onBlockChange, no allocation |
no equivalent |
Take the listener when you must act before anybody else sees the chunk, or when you need per-block notification — which is what the light engine uses it for. Take the events for anything else.
Two ways into the listener, depending on how much you need.
The builder takes two consumers, which is enough for most things:
FalcoInstance.builder(DimensionType.OVERWORLD)
.chunkLifecycle(
chunk -> log.info("loaded {} {}", chunk.getChunkX(), chunk.getChunkZ()),
chunk -> log.info("unloaded {} {}", chunk.getChunkX(), chunk.getChunkZ()))
.register(manager);For the full set, implement ChunkLifecycleListener and register it on the lifecycle. Every method
has a default, so implement only what you need:
instance.lifecycle().addListener(new ChunkLifecycleListener() {
@Override
public void onPublish(ChunkLifecycleEvent event) { } // visible to the world, before onLoad
@Override
public void onLoad(ChunkLifecycleEvent event) { }
@Override
public void onTick(ChunkLifecycleEvent event) { }
@Override
public void onUnload(ChunkLifecycleEvent event) { }
@Override
public void onBlockChange(FalcoChunk chunk, int x, int y, int z, Block block) { }
});Two things to know before you write one:
-
onBlockChangeruns inside the chunk's write lock. Writing another block from it re-enters that lock, and calling into code that takes a different lock is how a deadlock is built. Keep it to recording the position. - A listener that throws fails the load rather than being swallowed, and the caller waiting on the chunk sees it. That is deliberate: a chunk whose listener failed is not a chunk anybody should be handed.
A FalcoInstance cannot back a shared instance, and this has not changed: SharedInstance takes
an InstanceContainer and nothing else, so the attempt does not compile. What FalcoSharedInstance
does is fix a container-backed view:
InstanceManager manager = MinecraftServer.getInstanceManager();
InstanceContainer world = manager.createInstanceContainer();
world.setChunkSupplier(FalcoChunk::new);
// Not manager.createSharedInstance(world): that factory always builds Minestom's own type.
FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world);
manager.registerSharedInstance(view);The order matters. createInstanceContainer registers the container, and the view's constructor
refuses one that is not registered — registerSharedInstance performs no such check, and an
unregistered container is ticked by nobody.
The view keeps its own generator, chunk supplier and auto-load setting, where Minestom's writes all
three through to the container and lets one view reconfigure another. saveInstance() writes the
view's own tags rather than the container's.
What it does not change is who owns the blocks: setBlock reaches the container, which serialises
every write on its own monitor. A world that needs Falco's write path uses FalcoInstance and gives
up sharing.
FalcoChunk extends Chunk and holds a BlockStorage. Sections are created on the first write into
them; every section that holds nothing shares one instance.
That has one consequence you can trip over. getSections() and getSection(int) materialise
every section they hand back, because Minestom's contract lets a caller write into the result. On a
lazy chunk that turns a read into 24 allocations. Heightmap#getHeight does the same for the
sections it descends through.
If you only want to look, BlockStorage has read-only counterparts:
| To read | Use | Not |
|---|---|---|
| one section | view(int) |
section(int) |
| all sections | views() |
sections() |
| how many exist | materialisedSections() |
— |
| whether one is still the shared empty | shared(int) |
— |
Reaching the storage of a chunk is chunk.storage(). The counts per operation — a setBlock at
y = 64 materialises ten sections, all of them the heightmap descent rather than the write — are in
Counted.
-
No
SharedInstanceonto aFalcoInstance. Structural, see above. -
Foreign chunk types are refused. A
ChunkSupplierproducing anything but aFalcoChunkis rejected with a message naming the cause, because such a chunk would be accepted everywhere except the unload path and would then report itself as loaded forever. -
FalcoChunk#tickwalks its tickable map without a lock. A concurrentsetBlockthat rehashes the map while the tick thread walks it can yield garbage or spin. Inherited rather than introduced —DynamicChunkhas the identical race — and Minestom'sChunk#tickcontract says the method "doesn't necessary have to be thread-safe". Open, and listed as such in Project Status. -
No throughput measurement exists. Benchmark classes for the instance and the chunk are in
falco-benchmarks, but none has been run as a baseline, so there is no timing to quote.
- Rationale: Instances and Chunks — why the module exists, the four places a foreign instance behaves differently, and what it refuses to attempt
- Counted — the object and byte figures quoted above, with what they do not say
- Research: Instance Container — the investigation that preceded it
- Project Status — the working record, including what is still open
The measured tables are collected on Measured Results, and a correction to one
is made there first; a page that carries one of its own says so where the table stands.
What the ± after a JMH mean covers is stated once, in Rationale: Measurement.
Wiki home · Repository · README and quick start · API documentation · Issues · Licence: AGPL-3.0
Start here
- Quick start (README)
- Installation
- Anvil Chunk Loader
- Light Engine
-
Instances and Chunks — the third module,
falco-instance
The measured record
- Measured Results — every measured table, with the run behind each one
- Benchmarking
- Project Status
- Rationale: Measurement
Why it is built this way
-
Rationale — the index for the five rationale pages
-
Research — the index for the investigations
the six
- Exception Hierarchy
- Instance Container
- Light Engine
- Shared Instances and Batches
- Instance Performance
- Fluent API — a record, not a reference
Working on the build
-
Contributing — what a change is built, tested and released with
-
Build Setup — the build wiring, and the index for the six pages behind it