-
-
Notifications
You must be signed in to change notification settings - Fork 0
Instance Performance Research
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.
-
On a default server, all player-driven block writes already run on a single thread.
ServerFlag.DISPATCHER_THREADSdefaults 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. -
The four most expensive items in the block-write and chunk-streaming path lie outside what any
Instanceimplementation 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. - 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.
- 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). - 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.
Three verified facts that determine where a gain can exist at all. Every measure below is prioritised against them.
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.
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.
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(...)— thePlacementStatecarriesthis(:467,:471). -
chunk.setBlock(..., placement, destroy)→MS:instance/DynamicChunk.java:115-129callshandler.onDestroyandhandler.onPlace, both withinstancein 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.
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.
Without this section the rest is opinion. Four building blocks, in this order.
-
falco-benchmarks/build.gradle.ktsneedsjmhImplementation(project(":falco-instance")). It is absent today; the benchmark module knowsfalco-anvilandfalco-lightonly. -
benchmark/support/MemoryChunkLoader— a heap-backedChunkLoader, so a load benchmark measures the instance and not the page cache (docs/benchmarks.mddescribes this trap for the region file benchmark). Model: theBarrierLoaderinFalcoInstanceLoadRaceTest. -
benchmark/support/InstanceFixtures— buildsFalcoInstanceandInstanceContainerwith the sameChunkSupplier(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.
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.
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:425returns immediately for an identical block at the same position within the same tick — a benchmark writingSTONEtwice measures a map lookup.IncrementalVsFullBenchmarkalready solves this with a toggle. -
SHAREDis 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, theDISJOINTdifference 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.
.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.
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.
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.
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.
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.
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.
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.
: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.
: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.
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. |
Precondition: M1. :156 to a primitive long structure; :174 must stay a ConcurrentHashMap.
Risk: high — chunks 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.
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:794 → getFullDataPacket() →
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).
#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.
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.
: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.
: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.
| # | 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. |
Ideas that sound plausible but do not hold here.
-
Swapping
loadingChunksforLong2ObjectSyncMap. See above — the most dangerous proposal the research produced, and it was labelled effort "S", risk "low". -
Rebuilding the chunk map before measuring it. Three of four analyses ranked
Longboxing first or second.MS:utils/chunk/ChunkCache.java:31-36bypasses the lookup entirely under locality, and escape analysis may eliminate the boxing.docs/rationale/instances-and-chunks.md:325discards the only number ever quoted as non-reproducible. → M1 first. -
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. - 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.
-
Treating off-tick
Entity#removeon unload as a defect.MS:instance/InstanceContainer.java:272-286does the same, andsynchronizedbinds nothing to the tick thread. The real open risk is the concurrency of two unloads — a test assignment, not a measure. -
"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
computeinstead ofget+putwould add a bin lock on the hottest path to solve a problem that does not exist. -
FalcoLightingChunk extends FalcoChunkas the route to light integration. The obvious solution and the most expensive: it costs afalco-light → falco-instancemodule 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).
Not verifiable from the code, marked as estimates:
- Every performance gain in this document. There is no instance benchmark.
- Whether a shipped vanilla
BlockPlacementRuleorBlockHandlerreads the world, making the deadlock shape reachable. - Whether the
Longboxing inchunks.get(index)survives escape analysis. - Whether
Entity#removetolerates 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.
- A7 (docs) and A3 (volatile) — about an hour, removes two false premises and a visibility gap.
-
A1 (bound) and A5 (
lastBlockChangeTime) — each under a day, both pure improvements with no contract risk. - A2 (rule out of the lock), with the deadlock test written first. The test is the actual value.
- M0 + M1 + M2 — the measurement infrastructure. Nothing beyond this point is decidable without it.
- A4 and A6 alongside 4; M2 measures their effect.
- B3 → B2 → B4 as one project. Individually each delivers less than all three together, and B4 only with the neighbourhood safeguard.
- B1 and B5 only if M1 and M2 justify them.
- B6 when convenient, together with the missing test.