-
-
Notifications
You must be signed in to change notification settings - Fork 0
Benchmarking
JMH benchmarks for the Anvil chunk loader and the
light engine. They live in their own source set, src/jmh/java, so nothing they
need ever reaches the classpath of the published library. This page states why the suite is shaped
the way it is, what each published table does and does not establish, and how a third party can
re-perform any of it. It does not restate the design of either subsystem — that is on the two pages
above — and it is written for someone deciding whether to believe a number, not for someone looking
for a summary.
Read this before quoting a number. Everything below is one measurement, of one machine, on one JVM, under one load, at one fork. What that licenses and what it does not is stated before the results, not after them. The definition of the
±figure lives in Rationale: Measurement and is deliberately not repeated anywhere else.
The full, unabridged tables behind every number here — including the ones that did not make the highlight reel — are in Project Status under Measured.
Three questions made this suite exist, and every benchmark in it answers one of them.
- Does the structural claim turn into a number? The loader's claim is about lock granularity and the light engine's is about preparation cost. Both are readable in the source; neither is worth anything until the part outside the lock, or the part that was removed, is weighed.
- Is it actually faster than what Minestom ships? Not faster than a reimplementation of it, and not faster than a description of it. The comparison benchmarks call the original types.
-
Should a design survive?
AreaVsPerChunkBenchmarkandIncrementalVsFullBenchmarkexist to kill an idea that does not pay for its complexity. Both were written before the answer was known, and the simpler design was the fallback in each case.
Every table on this page is labelled with one of these, and no number from one kind may be compared with a number from another.
| Kind | What runs | What a number from it means |
|---|---|---|
| Library | No Minestom server. The registry is replaced by a fake and the chunk is plain arrays. | What this code contributes to an operation. Not a chunk load time, not comparable with any comparison row. |
| Comparison | The real Minestom implementation, on the same inputs, in the same process. Three of the four start a server. | A head-to-head result. Absolute microseconds include registry time on both sides, so they are comparable only against their own counterpart. |
| Decision | Falco against Falco, on real Minestom chunks. | Which of two Falco designs is cheaper. Says nothing about Minestom and nothing about absolute cost. |
The distinction is not cosmetic. A library benchmark deliberately removes the most expensive thing a
real server does; a comparison benchmark deliberately keeps it. Reading a 44.5 µs light row as
"lighting a section costs 44.5 µs on a server" is the single most likely misreading of this page, and
it is wrong in both directions at once.
The figure after a ± is defined once, in
Rationale: Measurement — it is a confidence-interval half-width over the
measurement iterations of a single JVM, not a standard deviation and not a min–max range. Every table
here carries a provenance line naming the class, the parameters, the thread count, the run settings
and that limitation, directly beneath it.
Given that, each comparison in this suite falls into exactly one of four categories. The test is
mechanical: write the faster side as a ± ea and the slower as b ± eb.
| Label | Test | What may be written |
|---|---|---|
| Supported |
a + ea < b − eb — the intervals do not overlap |
The factor, together with the conservative bounds (b − eb) / (a + ea) and (b + eb) / (a − ea). Stated plainly; a disjoint pair at this interval width is strong. |
| No difference resolvable | The intervals overlap | The direction of the two means, marked as unresolved. Never "the two are equal", and never a factor. |
| Not usable |
ea / a or eb / b is 0.5 or more |
No factor at any precision. What such a row evidences is dispersion — that the time stopped being predictable — which is a real finding, stated in words. |
| Derived | The row is arithmetic over other rows | Labelled as derived, with the errors of its inputs added. If the inputs carry no ±, the derived row carries no factor either. |
Two consequences a hostile reader should hold us to. The interval width used here is generous, so a disjoint pair is a strong result and is stated without hedging — hedging one of those is as much a defect as printing a factor on a row that cannot carry one. And an overlapping pair is not a null result: it says the difference was not resolved at this precision, not that there is none. Both rules are applied to the rows where Falco loses exactly as they are to the rows where it wins.
- It is not portable. Core count, CPU frequency scaling, the JIT's compilation plan on the day, the page cache and the file system all move these numbers. A result from your laptop says nothing about a production host, and neither does ours.
- It is not a chunk load time. No benchmark here includes registry lookups, chunk allocation, packet assembly, the scheduler or anything else a server does around this code. They report what this library contributes to it.
-
It is not a tail latency. Every benchmark in the suite is
Mode.AverageTime; nothing here reports a percentile. A server's felt performance is dominated by the worst tick, and the mean is only a proxy for it. For the contention rows in particular, the tail is the interesting quantity. - It is not a property of the software. Every published table comes from a single JVM process. It is a property of that process on that machine — which is why the direction and the order of magnitude are what the project claims, and why every repeat that has been run is reported.
-
RegionFileBenchmarkis not a storage benchmark. It runs on a warm page cache and measures almost no device time. Realistic for a server saving the same chunks repeatedly; useless as a statement about a disk.
Expand a section for the chart, the table and its provenance. Every block is labelled with its kind in the summary line, every row is classified by the rules above, and the one block that does not come from a benchmark of this suite says so before it says anything else.
Anvil loader as threads compete — comparison benchmark: no difference resolvable on one thread, 1.9× faster on two in this run, and beyond that Minestom's read time stops being predictable
Reading one chunk, 200 distinct block states, µs/op:
| Threads | Falco | Minestom | |
|---|---|---|---|
| 1 | 1 203 ± 123 | 1 060 ± 55 | intervals overlap — no difference resolvable |
| 2 | 1 181 ± 31 | 2 200 ± 445 | 1.9× faster in this run (1.45×–2.30×) — not reproduced, see below |
| 4 | 1 378 ± 84 | 11 021 ± 16 470 | not usable as a factor — Minestom's ± is 1.5× its mean |
| 8 | 2 438 ± 252 | 530 905 ± 1 928 261 | not usable as a factor — Minestom's ± is 3.6× its mean |
Comparison benchmark. RegionFileComparisonBenchmark.falcoRead / .minestomRead,
distinctStates = 200, one thread count per row via -t so the four rows are four separate runs,
one fork, 3 warmup and 5 measurement iterations of 1 s per the class annotation, JMH 1.37, one
16-core machine recorded as not idle, no results.json committed, run date and commit not recorded.
One fork — the ± covers variance between iterations of one JVM, not between JVM launches, see
Rationale: Measurement. This table is reproduced from
Project Status, which owns it;
any correction is made there first.
On one thread nothing is resolved, in either direction. Falco's mean is the higher one — 1 203 against 1 060, which would be 14 % — but the intervals overlap at 1 080 to 1 115, so neither the 14 % nor a tie is established by this run. What the row does show is that the pipeline buys nothing without contention, which is the expected result: the claim is about lock granularity, and with one reader there is no lock to contend for.
At two threads the picture inverts and the row carries it. The intervals are disjoint, so the difference stands: Minestom's read takes between 1.45 and 2.30 times as long as Falco's, quoted to one decimal because the wider of the two relative errors is 20 %; the bounds are the honest form.
The two-thread row did not reproduce, and that must be said next to it. An independent run of
the identical configuration — same class, same methods, distinctStates = 200, -f 1 -wi 3 -i 5,
one -t per row — is published in Anvil Chunk Loader, and it measured
Minestom at 103 437 ± 856 306 µs/op at two threads: 47 times the mean printed above, and a
half-width 8.3 times its own mean, which under the rules above carries no factor at all. Falco's
two-thread figure reproduced, at 1 174 ± 71 against the 1 181 ± 31 here, and so did the other three:
all four pairs of Falco intervals overlap between the two runs. Minestom's do not behave that way at
any thread count above one — its mean moves by 47× at two threads, and its interval widens until it
constrains nothing. That asymmetry is itself the more durable result: Falco's read time is
reproducible across JVM launches and Minestom's is not, which is the same statement the wide
intervals make within a single run. What survives both runs is the direction and the loss of
predictability. The 1.9× is a property of this run and of nothing else.
At four and eight threads there is no factor to quote, and that is the finding. Minestom's error
bar exceeds its own mean on both rows — 16 470 on 11 021, and 1 928 261 on 530 905. An interval that
wide over a quantity that cannot be negative is not a measurement of a duration; it is a measurement
of a very long tail. The correct statement is qualitative and it is worse for a server than being
slow: under four and eight concurrent readers Minestom's read time stops being predictable, while
Falco's ± stays at or below about a tenth of its own mean at every thread count. The 8.00×
these rows used to be quoted at is withdrawn.
A second run exists at a higher fork count, and it does not reproduce the magnitude. The four-thread
comparison was re-run with two forks and ten iterations: Falco 1 325.6 ± 21.1 µs/op against
Minestom 359 690.8 ± 97 498.3 µs/op, the run recorded in
Anvil Chunk Loader. Falco's two figures agree — [1 294, 1 462] against
[1 304, 1 347], overlapping — while Minestom's do not: 11 021 against 359 690, on disjoint
intervals. That asymmetry is the finding. Falco's read time is reproducible across JVM launches
at four threads and Minestom's is not, which is the same statement the wide intervals make within a
run. What the two-fork run adds is that it samples run-to-run variance twice rather than never, and
its Minestom error is 27 % of its mean rather than 150 %. What neither run establishes is a factor,
and the magnitude of the collapse is still unexplained by any mechanism the project has
investigated.
Writing: no difference is resolvable at any thread count. All four write rows overlap (the full table is in Project Status). The reason to expect that is structural rather than statistical: both implementations take a lock to allocate sectors and update the header, so the part that differs between them is not on the write path.
Light engine against Minestom's — comparison benchmark: faster in all six scenarios, 1.11× to 1.71×, byte-identical output
| Sources | Solid | Falco | Minestom | |
|---|---|---|---|---|
| 1 | 0 % | 44.5 ± 0.6 | 49.4 ± 1.3 | 1.11× faster (1.07×–1.16×) |
| 1 | 30 % | 39.3 ± 0.8 | 62.0 ± 2.0 | 1.58× faster (1.50×–1.66×) |
| 8 | 0 % | 98.3 ± 2.4 | 121.1 ± 5.5 | 1.23× faster (1.15×–1.32×) |
| 8 | 30 % | 119.3 ± 3.5 | 204.2 ± 3.7 | 1.71× faster (1.63×–1.80×) |
| 64 | 0 % | 109.2 ± 1.6 | 126.5 ± 5.6 | 1.16× faster (1.09×–1.23×) |
| 64 | 30 % | 122.6 ± 1.3 | 206.6 ± 4.2 | 1.68× faster (1.63×–1.74×) |
Comparison benchmark. LightEngineComparisonBenchmark.falco / .minestom, one section,
emissionMix = UNIFORM, lightSources and occlusionPercent as tabulated, one thread, run at
-f 1 -wi 5 -i 10 rather than the class annotation's 3 and 5, JMH 1.37, one 16-core machine
recorded as not idle, no results.json committed, run date not recorded, measured after commit
69381af. One fork — the ± covers variance between iterations of one JVM, not between JVM
launches, see Rationale: Measurement. The bracketed ranges are the
conservative ratio bounds; the point factor is retained because the tables it is quoted in elsewhere
use it. This table is reproduced from
Project Status, which owns it; any
correction is made there first.
All six pairs of intervals are disjoint, so all six rows stand as differences. This is the strongest evidence in the project and it is stated without qualification about this run: on this machine, in this process, Falco's engine was ahead in every scenario measured.
The lead grows with occlusion: at one source it goes from 1.11× at 0 % solid to 1.58× at 30 %.
occlusionPercent is a benchmark parameter, not a survey of real worlds — the reason for measuring
at 30 % is that solid blocks end a search early, so an entirely open section is the upper bound of
the work rather than the typical case.
Two facts about the code carry more weight here than the timings do, because neither can be
dismissed as noise. The measured Minestom side is the real BlockLight.buildInternalQueue and
LightCompute.compute at version 2026.06.20-26.1.2, reached by placing the benchmark in
net.minestom.server.instance.light because those methods are package-private — not a
reimplementation. And both engines are verified byte-identical: the benchmark's @Setup runs both
paths over the section it just built and aborts the trial if the 2048 bytes differ, per trial and per
parameter combination, and LightEngineEquivalenceTest pins the same property on every build over 54
scenarios. A faster number here cannot have been bought by computing something else.
The same, with sources of mixed brightness — comparison benchmark: the honest case, where two of the four rows resolve nothing
| Sources | Solid | Falco | Minestom | |
|---|---|---|---|---|
| 8 | 0 % | 118.97 ± 8.89 | 126.54 ± 9.55 | intervals overlap — no difference resolvable |
| 8 | 30 % | 116.50 ± 7.63 | 201.46 ± 16.83 | 1.73× faster (1.49×–2.01×) |
| 64 | 0 % | 150.42 ± 32.73 | 162.20 ± 3.11 | intervals overlap — no difference resolvable |
| 64 | 30 % | 149.42 ± 12.64 | 252.26 ± 9.28 | 1.69× faster (1.50×–1.91×) |
Comparison benchmark. LightEngineComparisonBenchmark.falco / .minestom, one section,
emissionMix = MIXED, lightSources 8 and 64, occlusionPercent 0 and 30, one thread, run at
-f 1 -wi 3 -i 5 — the class annotation, and shorter than the -i 10 of the table above, so the
two tables are not directly comparable, JMH 1.37, one 16-core machine recorded as not idle, no
results.json committed, run date not recorded, emissionMix added in commit 0e8fbb5. One fork —
the ± covers variance between iterations of one JVM, not between JVM launches, see
Rationale: Measurement. This table is reproduced from
Project Status, which owns it; any correction is
made there first.
Sources of differing brightness make a position get queued more than once, and this is where that
costs something. In the two open-sky rows the difference does not resolve — the intervals overlap
in both, and at 64 sources Falco's own ± is 22 % of its mean, the widest dispersion of any light
row published. Those two rows say the engines were not distinguishable here, not that they are equal
and not that Falco leads by a few percent.
The two rows with solid blocks do resolve, and hold their factor. Both pairs are disjoint, and the lead is the same size as with uniform sources.
The regression against uniform sources is reported rather than left out, because a benchmark that
only shows its best case is worth nothing. Its size has been stated as about 33 %, and that
figure needs its base named: Rationale: Measurement records that it is
anchored against a UNIFORM re-run at these same short settings — 112.7 µs at 64 sources and 0 %
solid — and not against the 109.2 µs of the longer run in the table above. Re-deriving it from
the two published tables gives a different answer, and that answer would be wrong, because the two
tables were measured at different iteration counts. The re-run itself is published in no table on
either page, so the 33 % currently rests on a number a reader cannot look up.
Where a save spends its time — library benchmark: 81 µs of a save is inside a lock, 4 057 µs is outside one
| Stage | Time | Lock held | Source |
|---|---|---|---|
| Snapshot | 64 µs | the read lock of the chunk |
snapshot, measured |
| Codec, without compression | 1 356 µs | none |
codecWithoutCompression, measured |
| NBT serialisation and zlib compression | 2 701 µs | none |
codec − codecWithoutCompression, derived
|
| Transfer | 17 µs | the region lock |
transfer, measured |
Library benchmark — no server, no Minestom on either side of any row. ChunkSaveStageBenchmark,
a 24-section ChunkColumn
of plain arrays, distinctStates = 200, one thread, one fork, 5 warmup and 5 measurement iterations
of 1 s per the class annotation — the run's command line is not recorded, JMH 1.37, one 16-core
machine recorded as not idle, no results.json committed, run date and commit not recorded.
No ± was published for any row, so no rule in How to read a row can be
applied to this table and no ratio drawn from it is defensible as more than an order of magnitude.
One fork — see Rationale: Measurement.
The claim this table exists to weigh is structural, and the structure is verifiable without any
benchmark: in FalcoAnvilLoader the snapshot holds the chunk's read lock, the transfer holds the
region lock, and the codec between them holds neither. Minestom's RegionFile reports
supportsParallelLoading() == true but serialises reading, decompression and NBT parsing through
a single ReentrantLock (instance/anvil/RegionFile.java:42, parse at :88, version
2026.06.20-26.1.2, cited in full in Anvil Chunk Loader), so its parallelism
is largely nominal. Moving that work out of the lock is what the three-stage pipeline is for.
What the numbers add is how much the lock-free part weighs: 64 + 17 = 81 µs inside a lock against 1 356 + 2 701 = 4 057 µs outside one. That ratio is the kind of quantity a single machine measures reliably, and it is the reason compression became the optimisation target — but the four rows carry no uncertainty, so it is an order of magnitude, not a figure to three places.
The two percentages this table has been quoted at elsewhere do not follow from the four rows above. "About 97 % outside any lock" against 4 057 / 4 138 = 98.0 %, and "compression is 63 % of a save" against 2 701 / 4 138 = 65.3 %. Both published figures err on the conservative side, which is the right direction — but neither is derivable from what is printed here, and the second one rests on the derived row, so it is not about compression alone either.
Two cautions on the middle rows. The derived row is not zlib alone: codec() is
compress(serialize(encode(...))) while codecWithoutCompression() is encode(...) and returns a
CompoundBinaryTag it never serialises, so their difference contains NBT serialisation as well as
compression. And the compression level that came out of this — Falco writes at zlib level 2 where
Minestom writes at 6 — was recorded as 1.83× faster for about 3 % more bytes with no benchmark
named, no payload described, no settings and no interval. Treat that pair of figures as a design-time
note rather than a result of this suite; ChunkSaveComparisonBenchmark.compressFalcoLevel and
.compressMinestomLevel exist to replace it with a measured one and have never been published.
The uniform-section fast paths — not from a benchmark of this suite: a design-time measurement, in which a one-state section costs 51× and 76× less than a 200-state one
A section of pure air or pure stone carries a palette of one entry, and the majority of every world looks like that. Detecting the case up front rather than walking 4 096 blocks:
| Fast path for a uniform section | General section, 200 states | Uniform section, 1 state | |
|---|---|---|---|
| Palette encode | 27.9 µs | 0.54 µs | 51× cheaper |
| Opacity table | 40.8 µs | 0.54 µs | 76× cheaper, and no arrays allocated |
Not a table from this suite, and not a comparison against anything Minestom ships. These are
two inputs to the same Falco code, not two versions of it — the columns are distinctStates = 200
against distinctStates = 1, which is why they are not headed Before and After. No benchmark class,
no resolveCost, no thread count, no forks or iterations, no JMH version, no machine and no ± were
recorded for these two figures. The shape matches
PaletteDataBenchmark.encode and SectionOpacityBenchmark.of, but nothing ties them to a specific
configuration. Treat both as orders of magnitude taken while designing the code, not as results of a
run that can be re-performed.
Where an idea did not pay off, that is written down too — see the rejected optimisations in Project Status, including the bucket queue that loses 5–7 % at equal source brightness.
The charts are generated from the tables in Project Status by
docs/charts/generate.mjs, which lives in the main repository next to the SVGs themselves.
The list below is what a reviewer would raise, answered in the order the objections actually bite. Each says what the threat is and whether the harness does anything about it. Where the answer is "nothing", that is written as "nothing".
One fork, on every table above. Not mitigated. Seven of the sixteen benchmark classes run at
@Fork(2), and none of those seven produced a published number. All four comparison classes and both
decision classes are @Fork(1), so every table on this page describes dispersion inside one JVM
process. A different JIT compilation plan, a different GC ergonomic decision, a different code layout
between launches — none of that is sampled even once, and no ± on this page covers it. This is the
largest single limitation of the evidence base. Raising @Fork on the six classes that matter costs
wall clock and no code.
Four cross-run repeats exist, and they are worth more than any interval here. The register is
Project Status, which owns every measured table; the four are the four-thread read
comparison at two forks and ten iterations (above), the second single-fork run of the whole loader
table published in Anvil Chunk Loader, IncrementalVsFullBenchmark, where an
independent earlier run gave 1.96× / 6.40× / 3.95× against the published 2.07× / 5.60× / ≈ 3.7×, and
the AreaVsPerChunkBenchmark nine-chunk row, re-run at 5.37× against 5.47× with overlapping ratio
bounds. Three of the four teach the same lesson and it is the honest summary of this whole page:
ratios reproduce, third digits do not. The fourth teaches a sharper one. On the loader table all
four of Falco's intervals overlap between the two runs, while Minestom's means move by up to 47× and
its intervals widen until they constrain nothing — a finding about reproducibility rather than about
speed, and one the project did not set out to make.
Warmup is thin on the largest benchmarks. Not mitigated and not checked. Warmup is 3 × 1 s or
5 × 1 s. For a library benchmark running at tens of microseconds per operation that is tens of
thousands of invocations, comfortably past C2 compilation. For the decision benchmarks it is not:
AreaVsPerChunkBenchmark at chunkCount = 16 runs at roughly 11 600 µs/op, which is about 260
warmup invocations in total, and IncrementalVsFullBenchmark sky light at roughly 39 600 µs/op gets
about 75. Whether the JIT has reached a steady state on those paths in 75 invocations is genuinely
unknown, and nothing in the harness tests it. Raising -wi and showing the number does not move
would settle it.
Dead-code elimination. Mitigated, with one exception that is documented rather than hidden. 40 of
the 50 measured methods return their result, three consume every value through a Blackhole, and the
remaining seven perform file I/O that cannot be optimised away. The exception is
LightNibblesBenchmark.getUniform and .setUniformUnchanged, which are provably eliminated and are
published as lower bounds rather than per-access costs — the detail is in
The one place a sweep is still eliminated.
State carried across a trial. Not mitigated; every @Setup in the harness is Level.Trial.
There is no per-iteration or per-invocation setup anywhere. Three consequences worth naming.
RegionFileBenchmark.writeRaw and ChunkSaveStageBenchmark.transfer write the same chunk coordinate
for the whole trial, which the source argues cannot grow the file without bound because the allocator
frees the old sectors after reserving the new ones. ChunkSaveComparisonBenchmark writes through
both loaders for the whole trial, and both keep internal caches that are warm after the first
invocations — this one is not discussed anywhere. AreaVsPerChunkBenchmark re-lights the same chunks
for the whole trial; its javadoc claims a per-iteration rebuild that does not exist, though the risk
that claim was defending against is avoided anyway, because JMH runs each @Benchmark method as its
own trial with a fresh state instance and area therefore never sees perChunk's output.
IncrementalVsFullBenchmark toggles its block between two states so the world alternates rather than
drifting, which is the right mitigation and is stated in the source.
One machine, and it was not idle. Disclosed, not mitigated. A 16-core machine is the only hardware fact recorded anywhere. No CPU model, no frequency governor, no memory configuration, no file system, no kernel. Project Status records that the machine was not idle during the runs, which is preserved here rather than softened: background load is a live candidate explanation for the wide error bars on the contention rows.
One JVM, and it is not recorded. The toolchain is Java 25 with options.release = 25, but the
JVM that ran the published benchmarks — vendor, exact build, GC, flags beyond the class-level
-Xms/-Xmx — is written down nowhere. Two classes, AreaVsPerChunkBenchmark and
IncrementalVsFullBenchmark, set no heap flags at all, and they are the two with the largest live
set and the most GC sensitivity.
The Minestom side is not pinned in this repository. settings.gradle.kts declares
net.minestom:minestom with withoutVersion(); the version is resolved through mycelium-bom
1.7.2. A republication of that BOM changes the Minestom half of every comparison without a single
commit in Falco. The version the published numbers were measured against is 2026.06.20-26.1.2,
recorded in Project Status.
Synthetic payloads, not real worlds. Partly mitigated, and the mitigations are the interesting
part. SectionStates.distinct writes contiguous runs rather than random scatter, because a random
fill produces a palette access pattern no world shows; SectionStates.lit spreads sources evenly
rather than clustering them, because clustered sources do far less work. ChunkPayloads builds its
payloads through the real save path, so they compress like chunk data rather than like noise. The
residual threat is unavoidable and stated once: no benchmark here reads a real world, and the mapping
from distinctStates 1, 8, 64, 200 to air, underground, surface and heavily built sections is a
plausible assertion, not a survey.
resolveCost is an uncalibrated proxy. Not mitigated. FakeBlockLightSource burns
Blackhole.consumeCPU(resolveCost) tokens per resolution with resolveCost ∈ {0, 50}. The reasoning
for having it is sound — measuring a cache against a free fake makes the cache look like pure
overhead, which is the opposite of what a registry does — but nothing establishes that 50 tokens is
what a Minestom registry lookup costs. Any claim resting on resolveCost = 50 is conditional on that
number. This does not touch the light comparison tables above, which use the real
MinestomBlockLightSource on both sides.
Derived rows carry the errors of both inputs, and two published ones carry none. The
codec − codecWithoutCompression row above, and the 53.9 µs figure for Minestom's search in
Project Status, which is minestomFull − minestomQueue because Minestom's
search consumes the queue it is handed and cannot be measured on a prepared one. Both techniques are
legitimate; neither difference has a published uncertainty.
488 configurations, each with its own interval. At that count a handful of intervals will exclude the true mean by chance alone. Not a serious problem here — the published claims are few and the effects large — but a reader who wants to count is entitled to the point.
Allocation figures are the exception to all of the above. The figures quoted elsewhere from
-prof gc — the opacity table going from 74 040 to 8 664 bytes per call — are essentially
deterministic. They are immune to fork count, to machine load and to JIT variance, which makes them
the most robust quantitative evidence the project has. Where an allocation claim and a timing claim
support the same conclusion, the allocation is the one to lead with.
./gradlew jmh # every benchmark, full settingsThat is every measured method for every combination of the parameters its state class declares — 50
methods over 488 configurations at commit ca79507, at the forks and iterations the annotations ask
for. Budget well over an hour and do not touch the machine while it runs. The count is dominated by
ScalingBenchmark, whose fifteen section counts and five distinct-state counts share one state class
and therefore form a full cross product of 300 configurations on their own.
The full run is not what you want during development. Restrict it to what you are working on:
# One class
./gradlew jmh -Pjmh.include='BitPackerBenchmark'
# One method
./gradlew jmh -Pjmh.include='ChunkSaveStageBenchmark.codec'
# A regex over several
./gradlew jmh -Pjmh.include='light\..*Propagator.*'The Gradle task writes two files:
| File | Content |
|---|---|
build/reports/jmh/human.txt |
the console output |
build/reports/jmh/results.json |
machine readable, for JMH Visualizer |
Both land under build/, which is gitignored. No run's output is committed in this repository,
which is why every provenance line above says so.
The task builds a self-contained benchmark jar, which is the faster way to iterate because it skips Gradle entirely and accepts every JMH option:
./gradlew jmhJar
java -jar build/libs/falco-*-jmh.jar 'BitPackerBenchmark.pack' -f 1 -wi 1 -i 1
java -jar build/libs/falco-*-jmh.jar -l # list every benchmark
java -jar build/libs/falco-*-jmh.jar -h # every optionA quick smoke run — one fork, one warmup iteration, one measurement iteration — is -f 1 -wi 1 -i 1.
That is enough to prove a benchmark executes and produces a plausible number. It is not enough
to compare two versions of the code; for that, drop the overrides and let the annotations decide.
JMH allows one instance at a time. A crashed run leaves /tmp/jmh.lock behind and every later run
fails with "Another JMH instance might be running"; delete the file.
java -jar build/libs/falco-*-jmh.jar 'PaletteDataBenchmark.encode' -prof gc
java -jar build/libs/falco-*-jmh.jar 'PaletteDataBenchmark.encode' -prof perfasm # Linux, needs perf-prof gc is the one worth reaching for first. Several of the claims on the other pages are about
allocation, not about time, and gc.alloc.rate.norm answers those directly — and does so far more
reproducibly than any timing here.
Everything above can be re-performed. It cannot yet be reproduced exactly, because the environment of the original runs was not recorded — that gap is listed below rather than papered over. What follows is enough for a third party to run the same configurations and say whether their machine agrees.
Build the jar once:
./gradlew :falco-benchmarks:jmhJar
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar -l # list every benchmarkThen, one command per published table. These are the configurations the tables above state; where a table's run settings were not recorded, the class annotation is used and the command says so.
# Loader under contention. One run per thread count -- -t takes a single value, so this is four
# separate runs, and the four rows of that table were produced that way.
for t in 1 2 4 8; do
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar \
"RegionFileComparisonBenchmark.(falco|minestom)(Read|Write)" \
-p distinctStates=200 -f 1 -wi 3 -i 5 -t "$t" \
-rf json -rff "loader-contention-t$t.json"
done
# The two-fork control for the four-thread row. This is the run the loader's claim rests on.
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar \
"RegionFileComparisonBenchmark.(falco|minestom)Read" \
-p distinctStates=200 -f 2 -wi 5 -i 10 -t 4 -rf json -rff loader-contention-2fork.json
# Light engine, equal source brightness. The six-row table.
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar \
"LightEngineComparisonBenchmark.(falco|minestom)" \
-p emissionMix=UNIFORM -f 1 -wi 5 -i 10 -rf json -rff light-uniform.json
# Mixed source brightness. lightSources=1 is skipped because MIXED degenerates to UNIFORM there.
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar \
"LightEngineComparisonBenchmark.(falco|minestom)" \
-p emissionMix=MIXED -p lightSources=8,64 -f 1 -wi 3 -i 5 -rf json -rff light-mixed.json
# Where a save spends its time. Annotation settings; the original run's command line is unrecorded.
java -jar falco-benchmarks/build/libs/falco-benchmarks-*-jmh.jar \
ChunkSaveStageBenchmark -p distinctStates=200 -f 1 -wi 5 -i 5 \
-rf json -rff save-stages.json-rf json -rff <file> is what makes a run citable: JMH writes the JDK build, the VM name and version,
the JVM arguments, every parameter and every iteration into that file.
JMH Visualizer reads it directly. A ./gradlew jmh run writes the same
thing to build/reports/jmh/results.json instead.
If you want your numbers to be worth more than ours, raise the fork count. -f 5 on any comparison
benchmark costs five times the wall clock and produces the only kind of interval that covers
run-to-run variance at all. Add -prof gc to anything you intend to quote as an allocation.
None of this is filled in for the published tables, and no value below may be guessed at from the outside. A replication that records it is more useful than the original.
| CPU | model, stepping, physical cores, SMT on or off |
| Frequency policy | turbo on or off, governor or P-state driver |
| RAM | size and speed |
| Storage and file system | both region benchmarks touch it |
| OS and kernel | distribution and version |
| JVM | vendor, full version string, GC, all flags |
| Machine state | idle or not, and what else was running |
| Commit | the Falco commit, and the Minestom version mycelium-bom resolved |
| Run date | per table |
| Raw results | the results.json of every run above |
A replication is not expected to reproduce these numbers. It is expected to reproduce these conclusions, and the criteria are stated in advance so that neither side gets to decide after the fact.
- A row marked supported agrees if the direction is the same and your conservative ratio bounds overlap the ones printed in the table. It disagrees if the direction reverses, or if your bounds are disjoint from ours — which would mean one of the two machines is measuring something the other is not.
- A row marked no difference resolvable agrees if your intervals also overlap, and is a new result if yours are disjoint. Resolving a difference we could not is an improvement on this page, not a contradiction of it.
- A row marked not usable agrees if your slower side is also dispersed — an error bar that is a large fraction of its own mean. A run in which Minestom's read time stays tight under four or eight concurrent readers would be a genuine refutation of the loader's central claim, and it is the single most valuable measurement anyone outside this project could contribute.
- Absolute microseconds are not expected to agree at all. Do not report a mismatch in them as a failure to reproduce; report the ratio.
Ratios reproduce, third digits do not. Three of the four repeats this project has run behaved that way; on the fourth, the loader table, Falco's numbers reproduced and Minestom's did not. A fifth that behaves differently from any of them is worth hearing about.
The benchmarks do not run during ./gradlew build, check or test. jmh and jmhJar are only
reachable when asked for by name. A benchmark run takes long enough that wiring it into the normal
build would make every commit painful, and JMH numbers are too noisy on a shared CI runner to gate
anything on them anyway.
Verify it for yourself:
./gradlew clean build --dry-run | grep -i jmh # prints nothing except the compile taskcompileJmhJava is part of build, and that is on purpose: a benchmark that no longer compiles
after a refactoring should break the build like any other source set.
The three kinds are introduced at the top of this page; this is what each one actually does.
The library benchmarks — the large majority — start no server at all. They measure what this code contributes, with the registry replaced by a fake.
The decision benchmarks start one anyway: AreaVsPerChunkBenchmark and
IncrementalVsFullBenchmark compare two Falco paths against each other, so no foreign implementation
is involved, but what they measure is defined over chunks of a real instance and there is no fake to
put in its place. Both sides of each pay the same registry, so the comparison is unaffected; the
absolute microseconds are not comparable with the library benchmarks.
The comparison benchmarks measure this implementation against the one Minestom ships with, and there the point is to run the original rather than a stand-in. Three of the four need a server for it:
| Benchmark | Server | Why |
|---|---|---|
RegionFileComparisonBenchmark |
no | Minestom's RegionFile reads no registry, so it runs in a bare fork. The class sits in net.minestom.server.instance.anvil because that type is package-private. |
ChunkSaveComparisonBenchmark |
MinecraftServer.init() |
Minestom's AnvilLoader reads the biome registry and the block state count in static fields, so the class initialiser fails before any measurement unless the registries exist. |
LightEngineComparisonBenchmark |
MinecraftServer.init() |
Measures the original light engine, whose methods are package-private in net.minestom.server.instance.light. |
LightEngineStageBenchmark |
MinecraftServer.init() |
Same package and the same reason: it splits both engines into their stages, so it calls the same package-private methods. |
The comparison numbers therefore include what a real registry costs on both sides — which is correct, because both sides pay it. They are not comparable with the library benchmarks below, which deliberately exclude it.
The rest of this section applies to the library benchmarks.
Both packages already separate their algorithm from the registries of a running server —
PaletteEntryResolver for the codec and BlockLightSource for the light engine. The benchmarks
plug fakes into those two interfaces
(FakePaletteEntryResolver,
FakeBlockLightSource).
This is a deliberate trade. A registry lookup is expensive enough to dominate every one of these measurements, and a benchmark whose number is 90 % Minestom tells you nothing about the code in this repository. The consequence is that no benchmark here reports what a chunk load costs on a real server. They report what this library contributes to it.
FakeBlockLightSource takes a resolveCost in Blackhole.consumeCPU
tokens for exactly this reason. SectionOpacity exists to resolve each distinct block state once
instead of once per visit, and how much that is worth depends entirely on what a resolution costs.
Measuring it against a free fake would make the cache look like pure overhead, which is the opposite
of what happens against a registry. What a token is worth against a real registry has never been
calibrated, which is why resolveCost appears in the threats above.
| Benchmark | Kind | Parameters | What it answers |
|---|---|---|---|
BitPackerBenchmark .pack .unpack .roundTrip
|
library |
bitsPerEntry 4, 5, 8, 15 |
What the packing loop of one section costs. It runs once per section on every load and every save, so a full-height chunk pays it 24 times. The parameter is the only thing that changes the iteration count per long. |
PaletteDataBenchmark .encode .unpack .roundTrip
|
library |
distinctStates 1, 8, 64, 200 |
What collecting a palette costs as the section gets busier. 1 is a section of pure air or pure stone — the majority of every world — and is answered without packing anything. 200 already needs eight bits per entry. |
ChunkCompressionBenchmark .compress .decompress
|
library |
compression ZLIB, GZIP, NONE × distinctStates 8, 200 |
The most expensive single stage of a chunk transfer, and the one the loader deliberately performs outside every lock. The payload is real serialised chunk NBT built through the same codec the save path uses, not random bytes — random bytes do not compress and would make zlib look far worse than it is. |
RegionFileBenchmark .writeRaw .readRaw .roundTrip
|
library | – | The byte transfer alone, on an already compressed payload. readRaw uses positional channel reads and takes no lock; writeRaw takes the region lock for the sector allocation and the header update. Not a statement about a disk: the page cache is warm throughout. |
ChunkSaveStageBenchmark .snapshot .codec .codecWithoutCompression .transfer .full
|
library |
distinctStates 8, 200 |
The interesting one. See below. |
ChunkSaveStageBenchmark splits a whole chunk save into the three stages it consists of, because
the central claim of the loader is structural and this is what turns it into a number:
| Stage | Lock held |
|---|---|
snapshot — copy the section arrays |
the read lock of the chunk; a game thread waits here |
codec — palettes, packing, NBT, compression |
none |
transfer — hand the finished bytes to the region file |
the region lock, for the allocation and the header |
full — all three, so the sum can be checked against the whole |
– |
codecWithoutCompression splits the middle stage again — but not into a palette half and a zlib
half, as its javadoc says. It returns a CompoundBinaryTag and never serialises it, so the
difference against codec covers NBT serialisation as well as compression. full is what the
decomposition should be checked against, and it has never been published.
The chunk is a ChunkColumn
of plain arrays, not a Minestom chunk. A real Section needs a started server, and the registry time
would land inside the codec stage and hide the very thing the benchmark isolates.
| Benchmark | Kind | Parameters | What it answers |
|---|---|---|---|
LightNibblesBenchmark .getUniform .getAllocated .setUniformUnchanged .setAllocating .ofArray
|
library | – | What the uniform shortcut is worth. A section whose blocks all carry the same level keeps no array and answers from a field; every other section shifts a nibble out of 2048 bytes. Most sections of a world are completely dark or completely sky-lit, so the shortcut is the common path, not the exception. |
SectionOpacityBenchmark .of
|
library |
distinctStates 1, 8, 64, 200 × resolveCost 0, 50 |
What "resolve each distinct state once" is worth. distinctStates sets how often the cache misses; resolveCost sets what a miss costs. At resolveCost = 0 you are measuring the hash map and the two array writes per block, so the table looks like pure overhead. At resolveCost = 50 you are measuring what it saves: seven resolutions per distinct state instead of seven per block. What 50 tokens corresponds to on a real registry is not established. |
LightPropagatorBenchmark .propagate
|
library |
lightSources 0, 1, 8, 64 × occlusionPercent 0, 25 |
How the single-section search scales with the amount of queued positions. lightSources = 0 is answered without a search at all, which is the case for the overwhelming majority of the sections of a world. occlusionPercent = 25 is there because solid blocks stop the search early — measuring only an open section reports the worst case and calls it normal. |
ChunkLightPropagatorBenchmark .propagate .propagateSky
|
library |
sectionCount 4, 16, 24 × lightSourcesPerSection 1, 8 |
The same search across section borders, over a flat map (4), a shallow world (16) and a full-height overworld (24). Both searches are measured because the engine runs both per chunk and they behave very differently: block light is bounded by the amount of emitting blocks, sky light seeds nearly every block of an open column. |
AreaVsPerChunkBenchmark .area .perChunk
|
decision |
chunkCount 1, 4, 9, 16 |
Whether area forming earns its complexity: an area of n chunks against n separate calculateWithNeighbours calls. Written as a decision rather than a report — had it not held, the simpler per-chunk design was the better one and area forming was to be dropped rather than tuned. The chunkCount = 1 row is the control: a lone chunk has no loaded neighbours, so neither side has a ring to read and the two must come out level. |
IncrementalVsFullBenchmark .incremental .full
|
decision |
sky false, true |
Whether replaying a changed position earns the memory the kept light costs. Both sides toggle the same block, compute the same nine chunks plus the same ring and write into the same sections; only the origin of a chunk's light differs. The block is toggled rather than placed, so both directions of an incremental update are measured — adding brightness and taking it back — and the world alternates between two states instead of drifting. sky is a parameter because a tick pays for both kinds and they gain very differently. |
Both propagators keep their working buffers between runs, so the benchmarks reuse one instance for
the whole trial and warm the buffers in @Setup. A fresh instance per invocation would measure two
array allocations instead of the search. The residual cost is that the measured path never pays for
growing a buffer, which is realistic for a running server and not for the first chunk after startup.
The last two start a server, unlike everything else in this table, because an area and a scheduler
pass are defined over real chunks of a real instance. They are also the only two classes in the
harness that set no heap flags, so their numbers rest on a default, unrecorded heap configuration.
ChunkLightService has no benchmark of its own; what it costs is covered by those two and by the
comparison benchmarks below.
These are the ones that answer "is this actually better", and they are the reason the loader and light engine documents can state factors instead of intentions. Each measures the original, not a reimplementation of it — which is why three of them live in Minestom packages, where the measured types are package-private.
| Benchmark | Parameters | What it answers |
|---|---|---|
RegionFileComparisonBenchmark .falcoRead .minestomRead .falcoWrite .minestomWrite
|
distinctStates 8, 200, and the JMH thread count |
The central claim of the loader: what the lock granularity is worth. Run it with -t 1 and no difference is resolvable; the difference appears only under contention, which is why the thread count is the parameter that matters here. There is no @Threads annotation anywhere in the harness, so a plain ./gradlew jmh measures this at one thread and never exercises the claim at all. |
ChunkSaveComparisonBenchmark .falcoSave .minestomSave .compressFalcoLevel .compressMinestomLevel
|
distinctStates 1, 16, 64, 256, 1024 |
Whether the palette handling shows up in a whole chunk save. Both sides run the identical Adventure writer over byte-identical payloads. The compression level is deliberately not equalised, because the levels differ in what the two loaders actually ship — Minestom 6 against ChunkCompression.DEFAULT_LEVEL = 2 — and the two compress* methods exist so that difference can be subtracted out. Ten of its twenty configurations are published — four methods over five distinctStates levels make twenty, and the ten belonging to .falcoSave and .minestomSave appear in Anvil Chunk Loader under Measured: saving a chunk, as five table rows carrying both loaders side by side. Exactly one of those rows resolves a difference, and that row is confounded by the compression level. What is unpublished is the compress*Level pair that would remove the confound, without which the palette claim cannot be separated from the zlib level. |
LightEngineComparisonBenchmark .falco .minestom
|
lightSources 1, 8, 64 × occlusionPercent 0, 30 × emissionMix UNIFORM, MIXED |
By how much each light engine wins, and on what shape of section. All three parameters are needed: the margin moves with each of them. |
LightEngineStageBenchmark .falcoReadStates .falcoOpacity .falcoPropagate .falcoCollect .falcoFull .minestomQueue .minestomFull
|
lightSources 1, 8, 64 × occlusionPercent 0, 30 |
Why one of them wins, which the comparison never says. It splits the Falco path into reading the palette, building the opacity table, searching and packing, and the built-in path into building the seed queue and the rest. This is what identified the allocation the opacity table used to make, and it is the source of the stage table in Light Engine. |
emissionMix decides whether every source of the section emits level 15 (UNIFORM, glowstone
throughout) or whether the sources differ (MIXED: glowstone 15, lantern 15, torch 14, redstone
torch 7, magma block 3, at the same positions drawn from the same seed). The Falco search assumes its
queued positions are ordered by level, which only holds while every source starts at the same one, so
this parameter is what would show whether a bucket queue is worth adding. One cell of the cross
product measures nothing: with a single source MIXED places glowstone as well and is a duplicate of
UNIFORM. The recommended run therefore leaves it out — the commands are in
Reproducing a published table.
The comparison verifies the two engines agree before it measures them. Its @Setup runs both
paths over the section it just built and aborts the trial when the 2048 bytes differ, so a faster
number cannot come from computing something else. LightEngineEquivalenceTest pins the same property
down in the normal test run, over 54 scenarios. Both are recent: the byte identity was stated in
Light Engine long before anything in the build checked it.
Because three of them start a server, their absolute numbers include registry time and are not comparable with the library benchmarks above. Compare them only against their own counterpart.
ScalingBenchmark measures
this library against itself rather than against Minestom, along two axes that the other benchmarks
sample too coarsely to expose a bend in:
| Method | Parameter | What it answers |
|---|---|---|
blockLightBySectionCount skyLightBySectionCount
|
sectionCount 1 … 256, fifteen steps |
Whether cost per section stays flat as the world grows taller. Block light does across the whole range. Sky light does not: a least-squares fit over the vanilla range (≤ 24 sections) understates the measured cost at 256 sections by about 20 %, while the same method lands within 2 % for block light. |
paletteByDistinctStates packingByDistinctStates
|
distinctStates 1 … 1024 |
The same question for the codec as a section fills up. |
Fifteen steps rather than three, because the point of this benchmark is to find where a curve stops being straight — and that is exactly what a coarse parameter set hides.
Two things about this class a reader should weigh before quoting it. It is the only class in the harness that measures three iterations rather than five, which makes its intervals the widest in the suite for a given dispersion — and no table from it is published, so those intervals cannot be inspected. And the extrapolation claim is a fit with unpublished residuals extended ten times beyond its support: "about 20 %" is as precise as it can honestly be stated. The durable part of the finding is the mechanism rather than the number — sky light seeds a queue at every open cell, so its work grows with the volume it can see, and that is why the curve bends where block light's does not.
The class also contains an unclaimed repeat. sectionCount and distinctStates share one state
class, but blockLightBySectionCount and skyLightBySectionCount never read distinctStates, and
the two codec methods never read sectionCount. Every light measurement in the class is therefore
already performed five times over, and every codec measurement fifteen times, each as an independent
JMH trial with its own warmup. The spread across those repeats is a direct empirical estimate of
exactly the run-to-run variability the single-fork objection is about, and it costs one run and no
code change to recover.
The rules every benchmark in this source set follows. Where a rule has an exception, the exception is named here rather than left for a reader to find.
-
Inputs are built in
@Setup, never in the measured method. Otherwise the generator is what gets measured. Every input is built from a seededRandominto an instance field, so no measured method has a compile-time-constant input. -
The work cannot be deleted by the JIT. Of the 50 measured methods, 40 return their result,
which JMH consumes;
LightNibblesBenchmark.getUniform,.getAllocatedandScalingBenchmark.packingByDistinctStatesconsume every value through aBlackhole; and the remaining seven —RegionFileBenchmark.writeRaw,ChunkSaveStageBenchmark.transferand.full,RegionFileComparisonBenchmark.falcoWriteand.minestomWrite,ChunkSaveComparisonBenchmark.falcoSaveand.minestomSave— perform file I/O, which is a side effect no compiler may remove. One case survives all of this and is documented below. -
@Fork,@Warmupand@Measurementare explicit on every class, so a reader can see how much evidence a number rests on without consulting the JMH manual. They are not uniform: seven classes fork twice and nine fork once, warmup is 3 or 5 iterations, measurement is 3 or 5, and the iteration time is 1 s except inChunkSaveComparisonBenchmark, which uses 2 s. Copy the values from the class you are documenting. Several published tables were additionally produced with command-line overrides, which is why each provenance line above states the settings of the run rather than of the annotation. -
@BenchmarkModeand@OutputTimeUnitare set per class. All sixteen classes areAverageTimein microseconds, because every operation is a whole-section or whole-chunk unit of work and throughput would be the less natural framing. Nothing here reports a percentile. -
@State(Scope.Thread), except where a thread count is the parameter.RegionFileComparisonBenchmarkandChunkSaveComparisonBenchmarkare@State(Scope.Benchmark)with a nested@State(Scope.Thread)slot, because those two are run at several thread counts and the threads must share one region file while writing to different chunks. Everywhere else the measured code is single-thread confined —LightPropagatorandChunkLightPropagatorare documented as such — and the state classes hold mutable buffers. -
Sweeps, not single accesses.
LightNibblesBenchmarkwalks all 4096 blocks per invocation instead of reading one nibble. A single nibble read is a handful of instructions, which is below what a harness can separate from its own overhead. There is no@OperationsPerInvocationanywhere, so divide by 4096 yourself for the per-access cost — but see the caveat below. -
Deterministic inputs, from two seeds. The library benchmarks seed from
BenchmarkConstants.SEED; the comparison and decision classes use their ownSEED = 20260731. Inputs are byte-identical between two runs of the same benchmark, which is what makes a difference between them describe the change and not the input — but the two groups do not share a world, so no input is shared across the boundary either.
LightNibblesBenchmark.getUniform and .setUniformUnchanged report times far below one nanosecond
per access. That is not a measurement error and no Blackhole fixes it — verified by rerunning with
-Djmh.blackhole.autoDetect=false, which produces the same number.
A uniform section answers every read from a single field, whatever the coordinates. The loop body is
therefore loop-invariant, the JIT hoists it out, and the empty loop disappears. setUniformUnchanged
goes the same way: writing the level the section already carries returns immediately, the compiler
proves the loop has no effect, and removes it.
These two numbers are lower bounds, not per-access costs. What they legitimately say is that the
uniform path can collapse to a single field read — which is precisely the property the shortcut
exists for. Only getAllocated and setAllocating divide meaningfully by 4096. Both are kept
because a change that accidentally made the uniform path allocate would show up here instantly.
| Piece | Version | Why |
|---|---|---|
me.champeau.jmh |
0.7.3 |
Latest release. Gives the benchmarks their own source set so JMH never lands on the main or test classpath of a published library. |
org.openjdk.jmh:jmh-core |
1.37 |
Latest release. Not managed by mycelium-bom — the BOM covers adventure, minestom, cyano, junit and mockito only — so both versions are pinned explicitly in settings.gradle.kts. |
net.kyori:adventure-bom |
5.1.1 |
The main source set gets adventure through compileOnly(minestom), which never reaches a runtime classpath. The benchmarks run their code for real and need adventure at runtime, so they import the platform directly. Keep this in sync with the version Minestom resolves to — check with ./gradlew dependencyInsight --configuration compileClasspath --dependency adventure-nbt. |
net.minestom:minestom |
not pinned here | Declared withoutVersion() and resolved through mycelium-bom 1.7.2. The published comparisons were measured against 2026.06.20-26.1.2; a republication of the BOM moves the Minestom side without any commit in this repository. |
The plugin generates the harness classes with its bytecode generator. No jmhAnnotationProcessor is
declared on purpose: with both present, the two generators emit the same classes and the jar ends up
with two copies of every benchmark.
On Java 25, JMH 1.37 prints a warning about sun.misc.Unsafe::objectFieldOffset being terminally
deprecated. It is harmless and comes from JMH itself, not from this repository.
- The
±figure, the fork counts and the limits of this evidence base, stated once and in full: Rationale: Measurement. - The unabridged tables, including every row that did not reach this page: Project Status.
- What the loader and the light engine actually do with the results: Anvil Chunk Loader and Light Engine.
- The benchmark sources are canonical for every condition on this page —
falco-benchmarks/src/jmh/java, wired up byfalco-benchmarks/build.gradle.ktsandsettings.gradle.kts. Where this page and an annotation disagree, the annotation is right and this page is a defect.