Skip to content

Instance Performance Research

TheMeinerLP edited this page Aug 1, 2026 · 2 revisions

How to make falco-instance faster and more parallel than Minestom's InstanceContainer — and what stands in the way. Research carried out on 2026-08-01 against Minestom 2026.06.20-26.1.2 (the version mycelium-bom 1.7.2 pins) and Falco at 0.3.0.

Nothing in this document is measured. There is no instance benchmark in the repository. Every performance figure below is an estimate, and every estimate is marked as one. What has to be built first says so concretely. The repository itself claims no speed advantage today (README.md, docs/rationale/instances-and-chunks.md), and a report asserting one would be rejected by this project's own rules (docs/benchmarks.md).

Line references to Minestom sources are prefixed MS:. They are accurate for 26.1.2 and shift by a few lines on other versions.

The short answer

  1. On a default server, all player-driven block writes already run on a single thread. ServerFlag.DISPATCHER_THREADS defaults to 1 (MS:ServerFlag.java:20), and player packets are processed inside the entity tick (MS:entity/Player.java:378-380). Falco's finer lock granularity is invisible there.
  2. The four most expensive items in the block-write and chunk-streaming path lie outside what any Instance implementation controls — the per-packet viewer scan (MS:instance/EntityTrackerImpl.java:320-352), palette writes, chunk packet serialisation, and the absence of batching. Swapping the instance addresses none of them.
  3. What is in reach: bounding the unbounded thread fan-out on load and generate, pulling foreign code out of the chunk write lock, moving expensive work off the tick thread onto the load thread, and removing a few shared write cells.
  4. The one proposal with plausibly visible tick-time impact depends on (3c) and is currently blocked by a type check (FalcoInstance.java:690-695) — for which there is a way out that none of the source analyses found (B3).
  5. A result of "the advantage only exists in a configuration almost nobody runs" would not be a failure. It would be the first defensible statement on the question at all.

The framing findings

Three verified facts that determine where a gain can exist at all. Every measure below is prioritised against them.

The default server is single-threaded in the tick

MS:ServerFlag.java:20 defaults DISPATCHER_THREADS to 1. MS:ServerProcessImpl.java:307-318 ticks instances serially and then uses dispatcher().updateAndAwait(...) as a hard barrier. MS:entity/Player.java:378-380: Player.update calls interpretPacketQueue(), so player packets — and therefore every block placement — are processed in the entity tick, on the partition's TickThread. With one dispatcher thread, all player-driven block writes are serialised before FalcoInstance ever sees them.

The finer lock only pays off when either -Dminestom.dispatcher-threads is raised, or the application writes from its own threads (world generation, schematics, minigame arenas). Both belong in any claim of an advantage, and both must appear as parameters in a benchmark.

The most expensive item per block write is out of reach

MS:instance/EntityTrackerImpl.java:337-352: ChunkView.SetImpl calls references() on every iterator(), size() and forEach() (:320-330), which allocates an Int2ObjectOpenHashMap and walks nearbyEntitiesByChunkRange across (2·8+1)² = 289 chunks at the default view distance (MS:ServerFlag.java:17). There is no cache. Every chunk.sendPacketToViewers(...) (FalcoInstance.java:445, :449) triggers it. The EntityTracker is created privately in MS:instance/Instance.java:116 and cannot be replaced.

Any micro-allocation optimisation in the write path stands next to this item. Without measurement it cannot be claimed that one is even visible.

Two correctness findings that matter regardless of performance

A structurally reachable deadlock

FalcoInstance.java:429-439 holds the chunk write lock across two calls into foreign code, and both receive this:

chunk.lockWriteLock();
try {
    this.lastBlockChangeTime = System.nanoTime();
    final BlockPlacementRule rule = MinecraftServer.getBlockManager().getBlockPlacementRule(placed);
    if (placement != null && rule != null && doBlockUpdates) {
        placed = Objects.requireNonNullElse(rule.blockPlace(placementState(placement, placed, blockPosition)), Block.AIR);
    }
    chunk.setBlock(x, y, z, placed, placement, destroy);
} finally { chunk.unlockWriteLock(); }
  • rule.blockPlace(...) — the PlacementState carries this (:467, :471).
  • chunk.setBlock(..., placement, destroy)MS:instance/DynamicChunk.java:115-129 calls handler.onDestroy and handler.onPlace, both with instance in the argument list.

Foreign code that then calls instance.getBlock(...) on a different chunk takes that chunk's read lock while the first chunk's write lock is held (MS:utils/chunk/ChunkCache.java:38-43). Two concurrent writers in two chunks whose rules or handlers read each other produce exactly the deadlock shape that FalcoInstance.java:441-442 explicitly describes as avoided.

Minestom has the same structure (MS:instance/InstanceContainer.java:159-215), but its instance monitor (:149) makes the constellation unreachable. This is the one place where giving up the global lock opens a new deadlock form — and the Javadoc claims the opposite at that very spot.

Not verified: whether any shipped vanilla BlockPlacementRule or BlockHandler actually reads the world. The shape is structurally reachable; its occurrence in a default configuration is not demonstrated.

loadingChunks must not be swapped for Long2ObjectSyncMap

This is the most dangerous proposal the research produced, and it looked cheap: a drop-in replacement, effort "S", because every operation used exists in primitive form, compute included.

Long2ObjectSyncMapImpl.compute (:251) delegates on the fast path to ExpungingEntryImpl.compute (:576-585), which is a CAS retry loop:

for(;;) {
  final Object previous = this.value;
  if(previous == EXPUNGED) return ...;
  if(UPDATER.compareAndSet(this, previous, next != null ? next : (next = remappingFunction.apply(key, (V) previous))))
    return ...;
}

The remapping function is therefore not mutually excluded and can run more than once. FalcoInstance does not use loadingChunks.compute as a computation, but as a per-position lock with side effects: chunks.put + dispatcher().createPartition (:640-646), chunks.remove + markUnloaded() + deletePartition (:730-737), and the double-check of the chunk map (:551-559). That is exactly the invariant FalcoInstance.java:165-172 describes and FalcoInstanceLoadRaceTest pins down.

The swap would break the module's core guarantee silently — no compiler error, no test failure at low concurrency. Only chunks (:156, used with get/put/remove only) would be exchangeable, and it is mutated inside the other map's compute.

Related: the Javadoc at FalcoInstance.java:150-155 justifies the ConcurrentHashMap choice by calling Long2ObjectSyncMap a "copy-on-write map". It is a port of Go's sync.Map — a volatile read map without locking plus a dirty map behind a monitor. Copy-on-write happens only on promotion of the dirty map, not on every write, and its read path is primitive and lock-free. The stated reason does not hold.

What has to be measured first

Without this section the rest is opinion. Four building blocks, in this order.

M0 — infrastructure

  • falco-benchmarks/build.gradle.kts needs jmhImplementation(project(":falco-instance")). It is absent today; the benchmark module knows falco-anvil and falco-light only.
  • benchmark/support/MemoryChunkLoader — a heap-backed ChunkLoader, so a load benchmark measures the instance and not the page cache (docs/benchmarks.md describes this trap for the region file benchmark). Model: the BarrierLoader in FalcoInstanceLoadRaceTest.
  • benchmark/support/InstanceFixtures — builds FalcoInstance and InstanceContainer with the same ChunkSupplier (FalcoChunk::new) and the same loader. Otherwise two chunk types are compared along with the instances. Model: ChunkSaveComparisonBenchmark.
  • benchmark/support/InstanceEquivalence — aborts the trial unless both sides produce the same block state and the same number of packets. Without it, "Falco does less" is indistinguishable from "Falco is faster". Model: LightEngineComparisonBenchmark.verifyBothEnginesAgree().

No Minestom package is needed. The comparison path is Instance#setBlock and loadChunk, both public and overridden on both sides — the package trick used by the light and region-file benchmarks does not apply here.

M1 — ChunkMapComparisonBenchmark

ConcurrentHashMap<Long,Chunk> against Long2ObjectSyncMap<Chunk>, in isolation, no server. Cheapest first. @Param loadedChunks = {64, 256, 1024, 4096} (sync map promotion is O(n)), thread count via -t, @BenchmarkMode({AverageTime, SampleTime}), always with -prof gc. This settles the boxing question that docs/rationale/instances-and-chunks.md leaves open. It measures chunks only — for loadingChunks the question is already decided in the negative, see above.

M2 — BlockWriteComparisonBenchmark

The core claim. @State(Scope.Benchmark), @Param contention = {DISJOINT, CLUSTERED, SHARED}, @Param doBlockUpdates = {false, true}, a ThreadSlot as @State(Scope.Thread) for target coordinates (model: RegionFileComparisonBenchmark). Two traps, both already documented in the repo:

  • Blocks must alternate. FalcoInstance.java:425 returns immediately for an identical block at the same position within the same tick — a benchmark writing STONE twice measures a map lookup. IncrementalVsFullBenchmark already solves this with a toggle.
  • SHARED is the control, not the side case. Writing into one chunk makes both sides take the chunk write lock; they must come out level. If they do not, the DISJOINT difference comes from something other than lock granularity.

The framing condition must appear as an axis: the number is worthless without "dispatcher threads = n" and "cores = m" stated next to it.

M3 — ChunkLoadComparisonBenchmark and the scaling series

.falcoLoad / .minestomLoad plus a load-unload cycle over the MemoryChunkLoader. The cycle is the more important case because it produces the map writes. Then a documented run recipe -t 1,2,4,8 — the shape of the curve is the statement, not any single point. README.md is the lesson: the same code yields three entirely different conclusions at 1, 4 and 8 threads, and the 8-thread row has an error margin four times its mean, making it unusable.

M4 — percentiles instead of means

None of the 16 existing benchmark classes uses Mode.SampleTime; all 16 measure AverageTime. Under lock contention the mean is the least useful statistic. M2 and M3 need p50/p90/p99/p999. In the load test path, SampleWindow (in falco-demo) currently stops at percentile95.

M5 — load test, only after M1–M4

A deterministic Cyano test (InstanceContentionTest) with platform threads against a threshold, plus optionally a --stack=falco-instance mode in falco-demo. Today both demo stacks run on InstanceContainer, because FalcoInstance cannot hold a FalcoLightingChunk — see B3. A load test without prior micro figures shows only that something differs, not why.

Honest expected outcome, marked as an estimate: with one dispatcher thread M2 will probably show no delta; with four or more and DISJOINT, a clear one. README.md shows the same pattern for the loader — 1.14× slower single-threaded, inverting from two threads on.

Measures

Tier A — worth doing now, clear gain, manageable risk

A1 — bound chunk loading and generation

In FalcoInstance#retrieveChunk (:565-566) and #generateChunk (:880), introduce an instance-wide Semaphore (Math.max(availableProcessors, 2), as in FalcoAnvilLoader.java:160 and ChunkLightScheduler.java:169). The permit is acquired inside the virtual thread, not before starting it — otherwise the caller blocks, which is precisely why ChunkLightScheduler does it that way.

Gain (estimated): bounds the number of concurrently live staging arrays (one GenSection[] per run with up to 24 cloned palettes plus 24 Int2ObjectOpenHashMap, :925-929) and the number of blocked carriers during file I/O. Visible in: p99 chunk load time under a join burst at high view distance (M3), gc.alloc.rate.norm, peak thread count via JFR.

Effort: S. Risk: one real trap — generateChunk (:880-882) calls loadChunk(...).join() inside its virtual thread. Acquiring the permit before the join() makes a waiting thread hold a permit while waiting for another thread that needs one: self-deadlock under saturating fan-out. The permit must be acquired after the join().

The contradiction this resolves is stated in the repository itself: STATUS.md:253 warns against exactly this pattern, and FalcoInstance.java:566 is the only place in the repository that does not follow the advice.

A2 — pull the placement rule out of the chunk write lock

Evaluate getBlockPlacementRule and rule.blockPlace(placementState(...)) before chunk.lockWriteLock() in #writeBlock (:429-439). Only chunk.setBlock(...) — and, until A5, the timestamp — stay inside.

Gain: eliminates one of the two deadlock shapes above and shortens write-lock hold time by a registry lookup plus a foreign call. Visible in: no number, but a test that hangs today — two threads, two chunks, each with a rule reading the other chunk via instance.getBlock. That test is the actual value here and should be written before the change.

Effort: S. Risk: behavioural change — a rule sees the state immediately before the commit rather than during it. That is already true for the neighbour updates (:441-443), so the change is consistent with a decision already taken. No Minestom contract breaks: MS:instance/Chunk.java:95 requires the write lock for setBlock, not for rule evaluation.

What this does not fix, and that belongs in the docs: chunk.setBlock calls handler.onDestroy/onPlace under the write lock (MS:instance/DynamicChunk.java:115-129). That lives in Minestom's chunk and cannot be removed from here. The handler-borne deadlock shape remains. FalcoInstance.java:441-442 should say so instead of promising a freedom that only covers neighbours.

A3 — close visibility gaps

Make chunkSupplier (:211) and chunkLoader (:213) volatile. Neither is final nor volatile today, both are written from arbitrary threads via setChunkSupplier (:803) and setChunkLoader (:831), and read from virtual loader threads (:564, :663, :742, :757, :763, :769). The Javadoc explicitly permits the runtime swap (:821-829). No speed gain — a correctness gap closes. Whether it ever bit: unverified. Effort: S. Risk: negligible.

A4 — override getBlock in FalcoInstance

Instance#getBlock (MS:instance/Instance.java:714-717) goes through a single shared ChunkCache field (:119) whose chunk field is mutable and non-volatile. Override it with identical semantics and no shared cache, exactly as Instance#getBiome (:722-729) already does: getChunk + lockReadLock + chunk.getBlock + unlockReadLock, NPE on an unloaded chunk.

Gain (estimated): removes a process-wide shared cache line, written on every getBlock by every thread, from the read-dominated path that Falco's parallel write model creates in the first place. With two or more threads in different chunks that cache practically never hits and still writes. What is proven is only that a JMM data race disappears — it does not produce wrong results, because MS:utils/chunk/ChunkCache.java:29-45 reads the field into a local and validates it against coordinates afterwards.

Visible in: M2 with doBlockUpdates=true at ≥2 threads, and -prof perfnorm (cache misses). Effort: S. Risk: low while semantics stay identical.

A5 — defuse lastBlockChangeTime

:431 writes an instance-wide volatile long (:217) on every block write, inside the chunk write lock where it protects nothing. Two steps: pull the store out of the lock, then either switch to a VarHandle opaque store or — cleaner — keep the timestamp per chunk and compute getLastBlockChangeTime() (:1083) as a maximum.

Gain (estimated): removes a shared cache line written by every writer plus a System.nanoTime() call from the hottest write path. The field's value is small: only the getter reads it, and Minestom's batches do not refresh it for a foreign instance anyway (:86-88, :1077-1080). Visible in: M2 with DISJOINT at ≥2 threads. Effort: S to M. Risk: the documented semantics ("only as a delta against another reading of the same clock", :1075-1076) survive both variants.

A6 — allocate event objects behind hasListener()

:451 (every block write), :616 (every chunk load) and :740 (every unload) allocate the event before checking for listeners. MS:event/ListenerHandle.java:24-30 offers hasListener() for exactly this. Gain (estimated): one allocation and one map lookup less per block write on a server without listeners — and it stands next to the 289-chunk viewer scan, so without M2 it cannot be claimed to be visible. Effort: S. Risk: low; the handle binds to static process state, so it must be per-instance and lazy, not a static final loaded before server init.

A7 — documentation that actively misleads

No runtime gain, but the precondition for the next optimisation decision not resting on a false premise again. Two of the four source analyses went wrong on exactly these lines.

Location What is wrong
FalcoInstance.java:150-155 "copy-on-write map" — it is a sync.Map port with a lock-free, unboxed read path. The stated reason for choosing ConcurrentHashMap does not hold.
FalcoInstance.java:441-442 promises freedom from the two-chunk lock shape but delivers it only for neighbour updates; rule and handler run under the lock.
docs/rationale/instances-and-chunks.md:336-337 "generateChunk always throws" — stale; the generator path is implemented (:840-858, :875-895, :922-953) and tested.
FalcoChunk.java:126 copies tickableMap, which MS:instance/DynamicChunk.java:236-241 does not — a silent correction of a Minestom defect, documented nowhere.
FalcoInstance.java:174 should record that loadingChunks is not interchangeable with a map whose compute is CAS-based.

Tier B — worth doing, but needs a measurement or a rebuild first

B1 — unbox the chunk map (chunks only)

Precondition: M1. :156 to a primitive long structure; :174 must stay a ConcurrentHashMap. Risk: highchunks is mutated inside loadingChunks.compute (:553, :642, :731). A structure that locks internally introduces a new lock order. The current order is globally consistent loadingChunks → chunks and therefore deadlock-free; that must not tip.

B2 — pre-warm the chunk packet on the load thread

In #completeLoad (:590-617), materialise the packet once: if (falcoChunk.getFullDataPacket() instanceof CachedPacket cached) cached.body(ConnectionState.PLAY);

Gain (estimated): moves heightmap computation, section serialisation and compression off the requesting player's tick thread (MS:entity/Player.java:794getFullDataPacket()MS:network/packet/server/CachedPacket.java:50-58) onto the already-parallel load thread. The write lock createChunkPacket takes (MS:instance/DynamicChunk.java:258-263) is uncontended there because the chunk is not published yet. Of all proposals, the most plausible candidate for visible tick-time impact — and it is not measured. Feasibility is verified: CachedPacket.body(ConnectionState) is public (:41-44) and ServerFlag.CACHED_PACKET defaults to on (MS:ServerFlag.java:50).

Risks: CachedPacket is @ApiStatus.Internal, so the instanceof guard is a deliberate coupling and belongs in the Javadoc; the cache hangs off a SoftReference; and chunks that are never sent pay for it, so the pre-warm must be switchable or a bulk preload turns it into a regression. Without a light solution the first light pass discards the work immediately (FalcoLightingChunk.java:157).

B3 — dissolve the FalcoInstance / FalcoLightingChunk exclusivity

#requireFalcoChunk (:690-695) demands the class FalcoChunk; FalcoLightingChunk.java:80 is extends DynamicChunk implements LightUpdateAware. The combination fails on the first chunk load. STATUS.md:144-152 records this and offers only "a chunk that is both", which means a module dependency falco-light → falco-instance and gives up the documented independence of the modules.

The way out none of the source analyses found: Chunk#onLoad() and Chunk#unload() are protected (MS:instance/Chunk.java:273, :308) — any subclass in any package may call them on this. FalcoLightingChunk already overrides onLoad (:145). So it suffices to publish an interface in falco-instance (working name ManagedChunk, with markLoaded()/markUnloaded()), have FalcoChunk implement it, and have requireFalcoChunk check for it. Since writeBlock also needs the six-argument setBlock, first public on DynamicChunk (MS:instance/DynamicChunk.java:75), the check becomes "is a DynamicChunk and a ManagedChunk".

A consumer wanting both then writes ten lines in their own package:

public final class LightingFalcoChunk extends FalcoLightingChunk implements ManagedChunk {
    public void markLoaded()   { onLoad(); }
    public void markUnloaded() { unload(); }
}

without falco-light depending on falco-instance and without the module boundary falling.

Gain: no direct runtime gain — but B4 and the full value of B2 only become reachable through it, and a documented runtime trap disappears. Effort: M — five call sites of requireFalcoChunk (:349, :357, :379, :598, :725) and three signatures must carry two types instead of one. Risk: low; the guarantee that must not break is that Chunk#isLoaded() keeps telling the truth (MS:instance/Chunk.java:266-268). The interface checks the same capability, just not via class identity — it gets weaker if a consumer implements it without actually calling the hooks, which cannot be enforced and belongs in the Javadoc.

B4 — move lighting into the load path

A Consumer<Chunk> hook in completeLoad between the load step (:593-598) and publishChunk (:604), which a ChunkLightScheduler can occupy. Today a freshly loaded chunk registers via onLoad, waits up to a tick for ChunkLightScheduler#onTick, is computed, and then calls invalidate(), which discards the packet.

Gain (estimated): removes the one-tick delay and the duplicated serialisation work. Effort: M. Depends on: B3 mandatory, B2 sensibly together.

The highest risk in this catalogue, and the repository already names it. docs/rationale/concurrency.md:327-336 records as an open issue that two threads lighting overlapping 3×3 neighbourhoods can commit a result from an already-stale reading — "showing up as a seam, not as an error". Lighting an unpublished chunk while its neighbours load in parallel sharpens exactly that case. Without a position reservation through the scheduler's inFlight set this measure produces visible light seams. Do not implement it without that safeguard.

B5 — currentlyChangingBlocks per chunk instead of per instance

:209 is an instance-wide ConcurrentHashMap<BlockVec, Block>; every block write allocates a BlockVec (:423) plus a get and a put (:425-426), cleared each tick (:1113). As a per-chunk guard over chunkBlockIndex it would be allocation-free. Why B and not A: it stands next to the 289-chunk viewer scan, so measurability is unproven — and it is a correctness mechanism (recursion guard), so it is test-bound. Effort: M. Risk: medium; a handler destroying its own block must still be aborted.

B6 — applyGenerator stages without a read lock

:923-929 clones the live palettes without lockReadLock(). Correct for the createChunk path (:669) where the chunk is unpublished — but on the generateChunk path (:887) the chunk is published and writable by other threads. A concurrent setBlock during the clone races on PaletteImpl, which has no synchronisation at all. Fix: lockReadLock() around the staging loop on the generateChunk path. Why B and not A: it touches the core promise at :914-917 ("the write lock is held only for the commit"), needs the Javadoc pulled along, and needs a test running generateChunk concurrently with setBlock — which does not exist today. Gain: correctness, not speed.

Tier C — interesting, but speculative or only on demonstrated need

# Measure Why C
C1 Upstream PR to Minestom: make ThreadProvider<Chunk> externally configurable MS:ServerProcessImpl.java:73/:101 hard-wires ThreadProvider.counter(); the interface is already @Experimental and a @FunctionalInterface. A region-based mapping would then be trivial and bring locality. That is a Minestom ticket, not a feature here, and the gain is unproven.
C2 A Falco batch entry point (several blocks, one MultiBlockChangePacket) Instance#setBlock is void and per block, so batching needs an additional API. It would trigger the viewer scan per section instead of per block — the only way to reduce the largest cost item at all. But: new API, new semantics, and AbsoluteBlockBatch already exists as a separate path.
C3 Ease the viewer scan Not reachable: the EntityTracker is created privately in MS:instance/Instance.java:116. Upstream only.
C4 Cheaper FalcoChunk#copy (:123-126) Cold path. MS:instance/DynamicChunk.java:237 does the same.
C5 unregister without a List.copyOf per pass (:298-307) Shutdown path. The copy is also the reason the pass count stays finite.
C6 Guard unregister against in-flight block writes Minestom serialises via synchronized (instance) (MS:instance/InstanceManager.java:116), Falco does not. Today it warns instead of preventing (:309-310). Shutdown path, tolerable.

What we advise against

Ideas that sound plausible but do not hold here.

  1. Swapping loadingChunks for Long2ObjectSyncMap. See above — the most dangerous proposal the research produced, and it was labelled effort "S", risk "low".
  2. Rebuilding the chunk map before measuring it. Three of four analyses ranked Long boxing first or second. MS:utils/chunk/ChunkCache.java:31-36 bypasses the lookup entirely under locality, and escape analysis may eliminate the boxing. docs/rationale/instances-and-chunks.md:325 discards the only number ever quoted as non-reproducible. → M1 first.
  3. Hoping for more parallelism in the tick. Ticking does not happen in the instance, and the tick barrier (MS:ServerProcessImpl.java:318) prevents any overlap. Tick time is a control metric for this module ("does not get worse"), not one where an advantage is expected. Presenting it as evidence would be dishonest.
  4. Promising an advantage on a default configuration. With one dispatcher thread, player-driven block writes are serialised anyway. A number without the dispatcher thread count beside it misleads.
  5. Treating off-tick Entity#remove on unload as a defect. MS:instance/InstanceContainer.java:272-286 does the same, and synchronized binds nothing to the tick thread. The real open risk is the concurrency of two unloads — a test assignment, not a measure.
  6. "Fixing" the recursion guard because it is not atomic. The non-atomicity makes it more permeable, not stricter, and its purpose (intra-thread recursion) is untouched. A compute instead of get+put would add a bin lock on the hottest path to solve a problem that does not exist.
  7. FalcoLightingChunk extends FalcoChunk as the route to light integration. The obvious solution and the most expensive: it costs a falco-light → falco-instance module dependency and inverts the documented independence. B3 achieves the same through an interface plus ten lines at the consumer.

Also struck as not applicable to this code: StampedLock for the chunk lookup (there is no lock there — getChunk is a ConcurrentHashMap#get); StampedLock inside the chunk (MS:instance/Chunk.java:49 is private final and the lock accessors are final); additional striping (over an already striped structure); Folia-style regionalisation (the dispatcher is final in the process); StructuredTaskScope and the Vector API (preview/incubator, would force a JVM flag on every consumer of the published artefacts); ScopedValue (there is no ThreadLocal to replace — zero hits); a custom palette (Palette is sealed); false sharing between chunk states (object graph, not array-of-structs).

Open questions

Not verifiable from the code, marked as estimates:

  • Every performance gain in this document. There is no instance benchmark.
  • Whether a shipped vanilla BlockPlacementRule or BlockHandler reads the world, making the deadlock shape reachable.
  • Whether the Long boxing in chunks.get(index) survives escape analysis.
  • Whether Entity#remove tolerates two concurrent runs.
  • The virtual thread scheduler's carrier ceiling — JDK general knowledge, not checkable here.
  • The actual effect of B2 on tick time. There is no benchmark for the send path.

Not examined: falco-light beyond FalcoLightingChunk and the scheduler's semaphore lines; falco-anvil beyond its semaphore and supportsParallel* lines; the falco-instance test classes (taken from the source analyses); docs/benchmarks.md and STATUS.md only by targeted grep.

Suggested order

  1. A7 (docs) and A3 (volatile) — about an hour, removes two false premises and a visibility gap.
  2. A1 (bound) and A5 (lastBlockChangeTime) — each under a day, both pure improvements with no contract risk.
  3. A2 (rule out of the lock), with the deadlock test written first. The test is the actual value.
  4. M0 + M1 + M2 — the measurement infrastructure. Nothing beyond this point is decidable without it.
  5. A4 and A6 alongside 4; M2 measures their effect.
  6. B3 → B2 → B4 as one project. Individually each delivers less than all three together, and B4 only with the neighbourhood safeguard.
  7. B1 and B5 only if M1 and M2 justify them.
  8. B6 when convenient, together with the missing test.

Clone this wiki locally