-
-
Notifications
You must be signed in to change notification settings - Fork 0
Benchmarks and Demo
The build wiring of the two modules that are never published: why they exist as separate modules, what depends on what, and which tasks they add. This page is about the build files only. What the benchmarks measure, under which conditions, and what the numbers do and do not license anyone to conclude is in Benchmarking and Rationale: Measurement — nothing on this page restates a measurement, and no number here is a result.
The two files are
falco-benchmarks/build.gradle.kts
and
falco-demo/build.gradle.kts.
This is a build fact, checkable in one place. The root build's publishedModules list names
falco-anvil, falco-light, falco-instance and falco-bom, and maven-publish is applied only
to that list (see Publishing). falco-benchmarks and falco-demo are absent from it,
so neither has a publish task at all and ./gradlew publish at the root cannot reach them —
no task exclusion, no enabled = false, nothing to keep in sync. Both modules say so in their own
description, which is the string that would appear in a POM if one were ever generated.
The same holds for what they pull in: the JMH Gradle plugin, jmh-core, the SLF4J binding and
Minestom-at-runtime all live in these two modules and in no published one.
The module exists because a benchmark that spans both libraries has nowhere else to live.
ScalingBenchmark imports BitPacker and PaletteData from falco-anvil and
ChunkLightPropagator, LightNibbles and SectionOpacity from falco-light in one class, and the
support classes BenchmarkConstants and SectionStates are used from both the benchmark.anvil and
the benchmark.light packages. With a jmh source set inside each library module, those would
either have to be duplicated or the two library modules would have to depend on each other just for
benchmarking. Keeping the benchmarks in their own module also keeps the jmh Gradle plugin, and
everything it pulls in, out of the published artefacts entirely.
dependencies {
jmhImplementation(platform(libs.mycelium.bom))
jmhImplementation(platform(libs.adventure.bom))
jmhImplementation(project(":falco-anvil"))
jmhImplementation(project(":falco-light"))
jmhImplementation(libs.adventure.nbt)
jmhImplementation(libs.annotations)
jmhImplementation(libs.jmh.core)
jmhImplementation(libs.minestom)
jmhImplementation(libs.fastutil)
}Every dependency is on the jmh configuration, so nothing here reaches the main or test
classpath of this module, let alone another one. Minestom is implementation-style rather than
compileOnly — the opposite of the library modules (see Dependency
Management) — because the comparison benchmarks execute Minestom's own code
rather than compiling against it. The Adventure platform is imported for the same reason: benchmark
code runs, so it needs Adventure on the runtime classpath, which the library modules never do.
Note that falco-instance is not a dependency here: no benchmark measures it.
No JMH annotation processor is declared, deliberately. The JMH Gradle plugin already generates the benchmark harness classes with its own bytecode generator; declaring the annotation processor as well would make both mechanisms emit the same classes, leaving the jar with two copies of every benchmark.
jmh {
jmhVersion.set(libs.versions.jmh)
includeTests.set(false)
resultFormat.set("JSON")
resultsFile.set(layout.buildDirectory.file("reports/jmh/results.json"))
humanOutputFile.set(layout.buildDirectory.file("reports/jmh/human.txt"))
val include = providers.gradleProperty("jmh.include").orNull
if (include != null) {
includes.set(listOf(include))
}
}jmhVersion is pinned from the catalog rather than left to the plugin's default, so the harness that
runs is the JMH version this repository names — 1.37 today (see Dependency
Management).
includeTests is off so that what the plugin scans and puts on the JMH classpath is exactly
src/jmh/java and its jmh configuration. This module has no src/test/java at all, so the setting
changes nothing today; it is there to keep a test source set added later out of a benchmark run
rather than to fix a problem that exists.
A full ./gradlew :falco-benchmarks:jmh run executes every @Benchmark method at every combination
of its @Param values — the inventory is in Benchmarking — which is far more than
anybody wants while working on one of them. -Pjmh.include='BitPackerBenchmark.pack' narrows the run
to a regular expression without editing this file. The two output files land under
falco-benchmarks/build/reports/jmh/; neither is committed to the repository, which is why
Rationale: Measurement lists the raw run records as a reproducibility gap
rather than pointing at a file here.
Finally, the benchmark sources are compiled by a normal build and executed by none:
tasks {
named("check") {
dependsOn(named("compileJmhJava"))
}
}A benchmark that stopped compiling after a refactoring elsewhere in the codebase should fail the
build like any other source set would. Nothing in the build depends on the jmh task itself, so
./gradlew build never runs a benchmark and a published artefact never contains one.
falco-demo is a runnable comparison of the Falco and the Minestom chunk loader on the user's own
world. It is deliberately absent from the root build's publish list, for the same reason as
falco-benchmarks: it is a tool for someone who wants to check the loader's behaviour on their own
hardware, not something a consumer of the library should ever resolve as a dependency. What a run of
it produces is not a JMH result and is not comparable with anything in
Benchmarking.
dependencies {
implementation(platform(libs.mycelium.bom))
implementation(project(":falco-anvil"))
implementation(project(":falco-light"))
implementation(project(":falco-instance"))
implementation(libs.minestom)
implementation(libs.slf4j.api)
compileOnly(libs.annotations)
runtimeOnly(libs.slf4j.simple)
testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.platform.launcher)
testRuntimeOnly(libs.junit.jupiter.engine)
}Unlike the library modules, where Minestom is compileOnly because the consumer picks the version
(see Dependency Management), here it is the opposite: this module starts a
real server and calls the loader Minestom ships with, so it needs Minestom on the runtime classpath,
which implementation provides. slf4j-simple is the binding the library modules deliberately do
not choose for anybody, taken here as runtimeOnly because the demo is the one thing in this
repository that is actually run by a user.
falco-light is included because the Falco server hands its instance a FalcoLightingChunk through
ChunkLightScheduler — the half of the stack a pure loading comparison cannot show, and whether the
light of a streamed-in chunk looks right is usually the first thing anybody notices while flying
around. falco-instance is included even though nothing in the demo runs it: ServerStack imports
FalcoInstance so it can name the type in its Javadoc and in the log line that explains why the
demo leaves it out, and a reader following that explanation should land on the real type rather than
on a string that was accurate only when it was written.
tasks.register<JavaExec>("runFalcoLoader") { configureDemo("falco") }
tasks.register<JavaExec>("runMinestomLoader") { configureDemo("minestom") }Both run the identical main class, net.onelitefeather.falco.demo.ChunkLoadDemo, with the identical
configuration; the --loader value is the only thing that differs. Anything else would make the
comparison meaningless, so the shared configuration lives in one place (configureDemo) and the two
task registrations only pass a different argument.
A few details inside configureDemo are worth calling out:
- The JVM used for the run is pinned explicitly through a
JavaToolchainServicelauncher at language version 25, rather than whatever JVM happens to run the Gradle daemon, so the run doesn't carry an unreported variable. - The world directory is passed as an explicit system property (
falco.demo.world) rather than relied upon via the working directory, so the demo doesn't silently depend on how it was launched (a run started from an IDE would otherwise look in a different place). - Options such as
threads,chunks,warmup,rounds, anddimensionare read as Gradle properties through aCommandLineArgumentProvider, so a run reads./gradlew :falco-demo:runFalcoLoader -Pthreads=8. A property that is not set contributes no argument at all, leaving the demo's own defaults in charge. -
isIgnoreExitValueis deliberately left at its default (false) for the measurement tasks.ChunkLoadDemo.mainends every case it handles itself — an unparseable command line, a missing world, a world whose region files mark no chunk as written — with a printed explanation and a plain return, which exits zero; the path that completes a measurement ends with an explicitSystem.exit(0), because the Minestom registries start threads that would otherwise keep the JVM alive after the report. A non-zero exit status therefore really is a defect and should fail the build rather than scroll past unnoticed.
tasks.register<JavaExec>("runFalcoServer") { configureServer("falco") }
tasks.register<JavaExec>("runMinestomServer") { configureServer("minestom") }This is the second half of the module: a joinable server somebody can judge by eye, sharing the world
directory, toolchain, and option style with the measurement tasks above — for the same reason those
two share theirs. The comparison is only meaningful if the two servers differ in the stack
(--stack=falco against --stack=minestom) and in nothing else, so everything except --stack is
configured once in configureServer.
The view distance is set as a system property, not a command-line option read by the demo's own argument parser:
val viewDistance = providers.gradleProperty("viewDistance").orElse("10")
...
systemProperty("minestom.chunk-view-distance", viewDistance.get())This is forced by Minestom itself. At 2026.06.20-26.1.2, server/ServerFlag.java:17 declares
public static final int CHUNK_VIEW_DISTANCE = intProperty("minestom.chunk-view-distance", 8) — a
static final field read once when the class is initialised, which happens before main() gets a
chance to apply anything. A -PviewDistance value that only reached the demo's own option parser
would be logged correctly while the server itself silently used the default of 8.
Unlike the measurement tasks, isIgnoreExitValue is set to true for the server tasks. The normal
way to stop a long-running server is a signal (Ctrl-C), and a signal leaves the JVM exit code at 130
or 143 regardless of how cleanly the process shut down. Without ignoring that exit value, every
session stopped with Ctrl-C would end in a red "BUILD FAILED" printed underneath the shutdown lines
that actually say the loader closed properly. A genuine crash still prints its stack trace to the
same console, which is the output anyone watching a running server would be reading anyway, so
nothing is lost by ignoring the exit code here.
Both server tasks also fix the SLF4J Simple output format — level info, timestamps as HH:mm:ss —
through three more system properties, so two sessions logged side by side are comparable line for
line.
falco-demo/build.gradle.kts accesses sourceSets and javaToolchains through
extensions.getByType<...>() rather than through Kotlin's generated type-safe accessors:
val mainSourceSet = extensions.getByType<SourceSetContainer>().named("main")
val toolchains = extensions.getByType<JavaToolchainService>()The java-library plugin is applied to this module from the root build's subprojects block (see
Build Setup) rather than from a plugins { } block in this file, and relying on
accessor generation across that boundary is exactly the kind of thing that silently breaks on a
Gradle upgrade.
Most of the measured tables are owned by Benchmarking and
Project Status; a page that carries one of its own says so where the table stands.
What the ± after a JMH mean covers is stated once, in Rationale: Measurement.
Wiki home · Repository · README and quick start · API documentation · Issues · Licence: AGPL-3.0
Start here
- Quick start (README)
- Installation
- Anvil Chunk Loader
- Light Engine
-
Rationale: Instances and Chunks — the third module,
falco-instance
The measured record
Why it is built this way
- Rationale — the index for the five rationale pages
- Research — the index for the five investigations
- Research: Fluent API — a proposal; describes no existing API
Working on the build