-
-
Notifications
You must be signed in to change notification settings - Fork 0
How to Run the JMH benchmark suite
Execute the benchmarks in falco-benchmarks on your own machine. A normal build never runs them.
Before you start: a working build (How-to Build Falco from source). What the results can and cannot mean is Explanation What the benchmarks establish — read it before quoting a number.
./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 Benchmark workflow
(.github/workflows/benchmark.yml)
runs the same jar on demand and uploads four things: results.json, the readable output, the exact
command line as executed, and a filled-in provenance line. Two profiles:
| What it does | |
|---|---|
custom |
one JMH filter in one job — for re-running a single table |
full |
the whole suite across sixteen shards, about half an hour where a sequential run is five |
Its purpose is provenance, not precision. Many of the gaps listed on this page are of the form
no run record was kept rather than the number is wrong, and those close with a run that documents
itself even from a machine that measures worse. Leave forks empty to keep each class annotation,
which is what re-running a published table needs.
What its numbers may not be used for. A shared runner is not the machine the published tables came from, and the difference is not only in absolute terms: the same light comparison measures 1.35× there against 1.13× here. A ratio is no more portable than an absolute figure, only more stable when the hardware is fixed — see What a measurement here means. CI figures therefore belong in their own table with their own provenance line, never merged into one measured elsewhere.
One thing the workflow enforces rather than advises: a shard is a whole class. Shards run on
separate runners, so splitting a comparison class would put falcoRead and minestomRead on
different CPUs and the resulting ratio would measure the hardware. That happened once, at 2.13×
against the 1.13× one machine gives.
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.
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.
JMH prints its own summary table at the end of the run, with a mean and a half-width per row. A run
that printed no ± column was not a measurement.
On Java 25, JMH 1.37 warns about sun.misc.Unsafe::objectFieldOffset being terminally deprecated.
It is harmless and comes from JMH itself.
See also: How-to Reproduce a published measurement to re-run a specific published table · Reference Benchmark catalogue for every class and its parameters
Every published table lives on Reference Measured results, which owns them; a correction is made
there and nowhere else. What the ± after a JMH mean covers is defined once, in
Explanation What a measurement here means.
Wiki home · Repository · README and quick start · API documentation · Issues · Licence: AGPL-3.0
Getting started
How-to guides
- How-to Add Falco to your build
- How-to Load an Anvil world
- How-to Compute light for a loaded world
- How-to Keep chunk light up to date automatically
- How-to Use FalcoInstance instead of InstanceContainer
- How-to Migrate a world from an older version
four more
Reference
six more
Background
- Explanation Choosing between Falco and the built-in loader
- Explanation Scope and non-goals
- Explanation When light computation actually runs
- Explanation What a measurement here means
nine more
- Explanation Choosing between FalcoInstance and InstanceContainer
- Explanation How the Anvil loader is built
- Explanation How the light engine works
- Explanation How the concurrency design works
- Explanation How world migration works
- Explanation The chunk version guard
- Explanation Why a second Anvil loader
- Explanation Why a custom light engine
- Explanation Why falco-instance exists
- Explanation Comparing the light engine with Minestoms
- Explanation What the benchmarks establish
Project record
Working on Falco