diff --git a/.github/ISSUE_TEMPLATE/performance-report.yml b/.github/ISSUE_TEMPLATE/performance-report.yml index d126b20..0ad0a7c 100644 --- a/.github/ISSUE_TEMPLATE/performance-report.yml +++ b/.github/ISSUE_TEMPLATE/performance-report.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - **Read [What a measurement here means](https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/explanation/what-a-measurement-here-means.md) + **Read [What a measurement here means](https://docs.onelitefeather.net/falco/explanation/what-a-measurement-here-means) before filing this.** Every number this project publishes comes from a JMH benchmark in `falco-benchmarks`, and the conditions are published next to it. A number without its methodology cannot be compared with one that has it, so this form asks for the methodology @@ -126,7 +126,7 @@ body: attributes: label: "Before submitting" options: - - label: "I read [What a measurement here means](https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/explanation/what-a-measurement-here-means.md)." + - label: "I read [What a measurement here means](https://docs.onelitefeather.net/falco/explanation/what-a-measurement-here-means)." required: true - label: "My run used the configured forks and iterations, not a single smoke run." required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0eaf48d..e7fb393 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -53,7 +53,7 @@ What is shared, and how is it guarded? _Delete this section if the change makes no claim about performance._ Numbers without their methodology are not accepted; see -[Benchmarking](../docs/explanation/what-the-benchmarks-establish.md). +[Benchmarking](https://docs.onelitefeather.net/falco/explanation/what-the-benchmarks-establish). - [ ] The figures below come from JMH with the configured forks and iterations, not a smoke run - [ ] I have quoted the error margins alongside the means, and named the machine, the JVM and the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 095899b..8efe18c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Falco is an Anvil chunk loader, a light engine and an `Instance` implementation all three modules are experimental: every public type carries `@ApiStatus.Experimental`, and signatures and behaviour may still change in a minor release. This file is the entry point and nothing more — what the build is made of, which conventions it enforces and what a push to `main` -publishes is [Contributing](docs/contributing/README.md) in the +publishes is [Contributing](https://docs.onelitefeather.net/falco/contributing) in the documentation. ## Building @@ -22,7 +22,7 @@ fails the build, so a green `./gradlew build` is also the documentation check. the internal `mycelium-bom` through `https://repo.onelitefeather.dev/onelitefeather`, which answers `401` to an anonymous request, so `./gradlew build` fails without them. This affects only work on Falco itself. The names the credentials are read under are in -[Installation](docs/how-to/build-falco-from-source.md). +[Installation](https://docs.onelitefeather.net/falco/how-to/build-falco-from-source). **Consuming Falco needs no credentials from this release onwards, and did need them before it.** The jars were always served without authentication, but a POM is resolved as well as a jar, and up to @@ -36,7 +36,7 @@ and the public `releases` endpoint declared, the 1.8.5 import resolves and bring Running a single test class, building the benchmark jar and starting the two demo servers are further commands, listed under -[Working on this](docs/contributing/README.md#working-on-this). +[Working on this](https://docs.onelitefeather.net/falco/contributing#working-on-this). ## Commits @@ -62,7 +62,7 @@ Opening one fills `.github/pull_request_template.md`, whose checklist is the sho what review looks for: the pull-request title is the Conventional Commit, a test was written first and watched fail, the concurrency section is filled in or deleted, the measurements section is filled in or deleted. What each of those four asks for, and why, is -[Opening a pull request](docs/contributing/README.md#opening-a-pull-request). +[Opening a pull request](https://docs.onelitefeather.net/falco/contributing#opening-a-pull-request). `.github/workflows/build-pr.yml` then runs the same build on the branch that `./gradlew build` runs locally. diff --git a/README.md b/README.md index 043ab19..9385264 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,29 @@ # Falco -[![API status: experimental](https://img.shields.io/badge/API-experimental-yellow)](docs/project-record/project-status.md) -[![Release](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Frepo.onelitefeather.dev%2Freleases%2Fnet%2Fonelitefeather%2Ffalco-anvil%2Fmaven-metadata.xml&label=release&color=blue)](docs/how-to/add-falco-to-your-build.md) -[![Java 25](https://img.shields.io/badge/Java-25-orange)](docs/how-to/build-falco-from-source.md) -[![Documentation](https://img.shields.io/badge/docs-gitbook-lightgrey)](docs/README.md) +[![API status: experimental](https://img.shields.io/badge/API-experimental-yellow)](https://docs.onelitefeather.net/falco/project-record/project-status) +[![Release](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Frepo.onelitefeather.dev%2Freleases%2Fnet%2Fonelitefeather%2Ffalco-anvil%2Fmaven-metadata.xml&label=release&color=blue)](https://docs.onelitefeather.net/falco/how-to/add-falco-to-your-build) +[![Java 25](https://img.shields.io/badge/Java-25-orange)](https://docs.onelitefeather.net/falco/how-to/build-falco-from-source) +[![Documentation](https://img.shields.io/badge/docs-gitbook-lightgrey)](https://docs.onelitefeather.net/falco) [![Licence: AGPL-3.0](https://img.shields.io/badge/licence-AGPL--3.0-lightgrey)](LICENSE) -A high-performance Anvil chunk loader and light engine for -[Minestom](https://github.com/Minestom/Minestom), plus an `Instance` implementation that cleans up -after itself. +Falco is a set of modules for [Minestom](https://github.com/Minestom/Minestom) servers that load +worlds saved in the Anvil format. It provides a chunk loader, a block and sky light engine, and an +`Instance` that unloads everything it loaded. It is for server developers who want a loader that +fails loudly on a bad read instead of quietly generating over real data. -The first two replace something the platform already ships, and both exist for the same reason: the -versions that come with the server serialise work that does not have to be serialised, and lose -information that should not be lost. The third replaces something that works, for a reason that has -nothing to do with speed — and it claims none. +- [Anvil chunk loader](https://docs.onelitefeather.net/falco/explanation/how-the-anvil-loader-is-built) — reads and writes region files in parallel, and throws on a read failure. +- [Light engine](https://docs.onelitefeather.net/falco/explanation/how-the-light-engine-works) — block and sky light, thread-safe per call, usable with any chunk type. +- [Automatic relighting](https://docs.onelitefeather.net/falco/how-to/keep-chunk-light-up-to-date-automatically) — chunks keep their light current as blocks change. +- [FalcoInstance](https://docs.onelitefeather.net/falco/explanation/why-falco-instance-exists) — unregistering it unloads every chunk it loaded. +- [Shared worlds](https://docs.onelitefeather.net/falco/how-to/use-falcoinstance-instead-of-instancecontainer) — `FalcoSharedInstance` on a plain container. +- [Version guard](https://docs.onelitefeather.net/falco/explanation/the-chunk-version-guard) — refuses worlds older than snapshot 21w43a instead of reading them as air. +- [World migration](https://docs.onelitefeather.net/falco/explanation/how-world-migration-works) — `falco-migration` moves older worlds forward. It has no release yet. -| Module | What it is | -| --- | --- | -| [`falco-anvil`](docs/explanation/how-the-anvil-loader-is-built.md) | A `ChunkLoader` for the Anvil region format. Genuinely parallel: reading, decompression and NBT parsing do not share one lock. A read failure throws instead of reporting the chunk as absent, so the server cannot overwrite real data with a freshly generated chunk. It refuses worlds older than snapshot 21w43a instead of reading them as air; a world at or above that floor is read with the current schema regardless of how old its DataVersion actually is, with no DataFixer involved. | -| [`falco-light`](docs/explanation/how-the-light-engine-works.md) | A block and sky light engine. Thread-safe per call and tied to no chunk implementation, so it works with chunk types Minestom's own engine ignores. Call it yourself, or let a chunk keep its own light up to date. | -| [`falco-instance`](docs/explanation/why-falco-instance-exists.md) | An `Instance` and its `Chunk`. **No speed gain is claimed and none is measured** — ticking lives in the server's global `ThreadDispatcher`, not in the instance. What it buys is an unload path of its own, where `InstanceManager.unregisterInstance` leaks every chunk a foreign instance ever loaded. It cannot back a `SharedInstance`; shared worlds are served by `FalcoSharedInstance` on a plain container instead. | - -All three modules are **experimental**. Every public type carries `@ApiStatus.Experimental`; -signatures and behaviour may still change in a minor release. +All published modules are **experimental**. See +[API surface and stability](https://docs.onelitefeather.net/falco/reference/api-surface-and-stability). ## Quick start -From nothing to a server that serves a stored world, in five steps. Step 4 needs no client, and step -5 replaces the hand-written parts of step 2 with the third module. - ### 1. Declare the dependency @@ -49,21 +44,29 @@ dependencies { // Minestom is compileOnly in Falco, so it does not arrive with these // artefacts. Falco declares no version for it, on purpose. You pick it. // The version Falco compiles and measures against is recorded in the documentation: - // https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/contributing/README.md#environment + // https://docs.onelitefeather.net/falco/contributing#environment implementation("net.minestom:minestom:") } ``` -Take only the modules you need — a platform constrains a version for each, it does not pull one in. -Steps 2 to 4 below use the first two; step 5 uses all three. Individual coordinates, Maven and -snapshots are in -[Installation](docs/how-to/add-falco-to-your-build.md). +Take only the modules you need. The platform constrains a version for each module; it does not pull +any in. Individual coordinates, Maven and snapshots are in +[Installation](https://docs.onelitefeather.net/falco/how-to/add-falco-to-your-build). -### 2. Write the server +### 2. Serve a stored world ```java -public final class Bootstrap { +import net.kyori.adventure.key.Key; +import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.world.DimensionType; +import net.onelitefeather.falco.anvil.FalcoAnvilLoader; + +import java.io.IOException; +import java.nio.file.Path; + +public final class FirstServer { public static void main(String[] args) { MinecraftServer server = MinecraftServer.init(); @@ -71,411 +74,50 @@ public final class Bootstrap { InstanceContainer instance = MinecraftServer.getInstanceManager() .createInstanceContainer(DimensionType.OVERWORLD); - // The world root, not worlds/lobby/region. The instance keeps the loader for as long as it - // lives, so it is not closed here — that happens on shutdown, at the bottom. - FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + Key dimension = DimensionType.OVERWORLD.key(); + FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), dimension); + instance.setChunkLoader(loader); instance.enableAutoChunkLoad(true); - // One service is enough. It keeps nothing between calls and may be used from any number of - // threads, which is what lets the chunks around several players be lit at the same time. - ChunkLightService lighting = new ChunkLightService(); - MinecraftServer.getGlobalEventHandler().addListener(InstanceChunkLoadEvent.class, event -> - lighting.calculateWithNeighbours(event.getInstance(), event.getChunkX(), event.getChunkZ())); - - MinecraftServer.getGlobalEventHandler().addListener(AsyncPlayerConfigurationEvent.class, event -> { - event.setSpawningInstance(instance); - event.getPlayer().setRespawnPoint(new Pos(0, 64, 0)); - }); - - MinecraftServer.getSchedulerManager().buildShutdownTask(() -> { - instance.saveChunksToStorage().join(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { loader.close(); } catch (IOException exception) { - throw new UncheckedIOException(exception); + exception.printStackTrace(); } - }); + })); server.start("0.0.0.0", 25565); } } ``` -The listener is the explicit route: you decide which chunks are lit and when. Step 5 shows the -shorter one, where the chunks keep their own light and no listener is needed. Everything in -`falco-light` that this step uses works without `falco-instance` on the classpath. - -### 3. Put a world where the loader looks - -`worlds/lobby/` is the **world root** — the directory that contains `region/`, or -`dimensions///region/` in the 26.1 layout. The loader resolves the dimension -directory first and falls back to the older one. Point it at `region/` itself and it will find -nothing. `level.dat` is not read, so a directory holding only region files is enough. - -Run the class and connect to `localhost:25565`. Chunks are read as you walk, `close()` on shutdown -flushes every open region file and writes the summary line. - -### 4. Check it without a client - -The same two operations without a listener around them, on a chunk of your choosing: - -```java -Chunk chunk = instance.loadChunk(0, 0).join(); -lighting.calculate(chunk); - -int level = lighting.blockLightAt(chunk, 8, 40, 8); -System.out.println("block light at 8/40/8 is " + level); -``` - -A non-zero level for a lit position means the loader read the chunk and the engine lit it. - -One honest note about the lighting in both steps. If the region files already carry light, it -recomputes what is already stored, because loading applies the stored arrays and clears the update -flag — for a pre-lit world the engine is doing work nobody asked for. It earns its keep on worlds -without stored light and after blocks change at runtime. Which case is which is spelled out in -[Light Engine](docs/explanation/when-light-computation-actually-runs.md). - -### 5. All three modules together - -Steps 2 to 4 use an `InstanceContainer` and drive the light yourself. The third module replaces both -of those decisions: - -```java -FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); - -// The scheduler takes the light service, not an instance. It reaches the world through -// the chunks its supplier builds. -ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); - -FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD) - .chunkLoader(loader) - .chunkSupplier(scheduler.supplier()) - .autoChunkLoad(true) - .ownsLoader(true) // close the loader on shutdown - .saveOnShutdown(true) // and write the chunks first - .registerAndShutdownWith(MinecraftServer.getInstanceManager(), - MinecraftServer.getSchedulerManager()); -``` - -That is the whole server: no light listener, and no shutdown task written by hand. Three things -changed compared with step 2. - -**The light keeps itself up to date.** The supplier builds `FalcoLightingChunk`s, which report their -own block changes, loads and ticks to the scheduler. It lights the touched region and sends it, one -tick later, including the ring around it. Before `1.0.0` this combination did not exist: the lighting -chunk and the Falco chunk both extended Minestom's `DynamicChunk`, a class has one superclass, and a -server had to choose one of the two. - -**Unregistering the world actually unloads it.** `InstanceManager#unregisterInstance` unloads chunks -only for an `InstanceContainer`; for anything else it leaves every chunk, tick partition and entity -behind. `FalcoInstance` cleans up after itself, and that leak is the reason the module exists at all. - -**A chunk allocates what it uses.** Sections are created on the first write into them and every empty -one shares a single instance, which takes a fresh chunk from 192 objects and 6 848 bytes to 25 and -840. It is a count, not a timing — see -[What "high-performance" means here](#what-high-performance-means-here). - -Two things to know before building on it. `getSections()` materialises all 24 sections, because a -caller may write into what it gets — use `chunk.storage().views()` to only look. And a chunk supplier -producing anything but a `FalcoChunk` is refused, because such a chunk would be accepted everywhere -except the unload path. Lifecycle listeners, the storage accessors and the rest are in -[Instances and Chunks](docs/how-to/use-falcoinstance-instead-of-instancecontainer.md). - -Minestom's own events are unaffected: `InstanceChunkLoadEvent`, `InstanceChunkUnloadEvent` and -`PlayerBlockBreakEvent` are dispatched here exactly as they are by a container, so listeners on the -`GlobalEventHandler` keep working. - -## Everything the three modules offer - -The five steps above are one path through Falco. This is the rest of it, so that what exists is -visible without reading three documentation pages first. Every snippet here is compiled by -`DocumentationSnippets` in `falco-demo`. - -### falco-anvil — reading and writing Anvil worlds - -The two-argument constructor is the whole of it for most servers. The builder is there when the -defaults do not fit: - -```java -FalcoAnvilLoader loader = FalcoAnvilLoader.builder() - .openRegionLimit(64) // region files kept open at once - .compressionLevel(2) // 1..9, the trade between write time and file size - .saveParallelism(4) // threads a saveChunks call may use - .dataVersion(4189) // what a written chunk claims to be - .diagnostics(new AnvilDiagnostics()) - .exceptionHandler(throwable -> log.warn("chunk load failed", throwable)) - .build(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); -``` - -**`diagnostics()` is the one to know about for a live server.** A world written by a different -version, or by a mod, contains blocks and biomes this loader cannot resolve — it substitutes and -counts rather than failing, and the counters are how you find out: - -```java -AnvilDiagnostics diagnostics = loader.diagnostics(); -diagnostics.reportUnknownBlock("mod:strange_block"); // true the first time, false after -``` - -`regionDirectory()` says which directory was resolved, `legacyLayout()` whether it fell back to the -pre-26.1 layout, and `openRegionCount()` how many files are open right now. `close()` flushes every -one of them and is what `ownsLoader(true)` calls for you. - -The version guard and the unknown-entry fallback above are both policies, not fixed behaviour: -`ChunkVersionPolicy` decides whether a chunk is readable at all, `UnknownEntryPolicy` decides what an -unknown block or biome becomes, and `falco-anvil` ships a default for each — `DefaultChunkVersionPolicy` -is the 21w43a guard, `DefaultUnknownEntryPolicy` is the air/plains substitution — discovered from the -classpath via `ServiceLoader` unless the builder's `versionPolicy()`/`unknownEntryPolicy()` slots are -used instead. The guard can be removed: `versionPolicy(null)` turns the check off, and nothing stands -in for it — a loader with no guard reads a pre-21w43a world as air again, with no error and no log -line. - -### falco-light — block and sky light - -Three entry points, in order of how much they do: - -```java -ChunkLightService lighting = new ChunkLightService(); - -lighting.calculate(chunk); // block light, this chunk -lighting.calculateSky(chunk); // sky light, this chunk -lighting.calculateWithNeighbours(instance, 0, 0); // both, and the ring around it - -int level = lighting.blockLightAt(chunk, 8, 40, 8); // read one position back -``` +Copy a saved world into the project as `worlds/lobby`, so that its region files sit in +`worlds/lobby/dimensions/minecraft/overworld/region`. Run the class and connect to `localhost:25565`. +You are standing in the stored world, not in generated terrain. -`calculateWithNeighbours` is the one to use when a chunk arrives from disk, because light crosses -chunk borders and a chunk lit alone has a dark seam. - -For a world that keeps itself lit, the scheduler does the bookkeeping. Its builder carries the knobs -that matter under load: - -```java -ChunkLightScheduler scheduler = ChunkLightScheduler.builder(lighting) - .executor(ChunkLightScheduler.defaultExecutor()) - .maxAreaSize(4) // chunks per side of one lighting area - .maxCachedChunks(256) // opacity tables kept between passes - .skyLight(ChunkLightScheduler.SkyLight.FROM_DIMENSION) - .onFailure(throwable -> log.error("lighting failed", throwable)) - .build(); -``` - -Two ways to drive it. On a `FalcoInstance`, `scheduler.supplier()` as in step 5 and nothing else. On -an `InstanceContainer`, hang a `ChunkLightListener` on the chunks and tick it yourself: - -```java -container.setChunkSupplier((instance, x, z) -> { - FalcoChunk chunk = new FalcoChunk(instance, x, z); - chunk.addLifecycleListener(new ChunkLightListener(scheduler)); - return chunk; -}); -MinecraftServer.getSchedulerManager().buildTask(() -> scheduler.onTick(container, System.currentTimeMillis())) - .repeat(TaskSchedule.tick(1)) - .schedule(); -``` - -And when something outside Falco changed the world, tell it: - -```java -scheduler.markChanged(instance, 0, 0); // this chunk needs relighting -scheduler.markChanged(instance, 0, 0, 8, 40, 8); // this position did -scheduler.markDirty(instance, 0, 0); // relight without an incremental path -``` - -### falco-instance — the instance, the chunk, shared views - -The builder is in step 5. Beyond it, the instance exposes its four parts, and the chunk exposes its -storage: - -```java -instance.registry(); // which chunks are loaded, by position -instance.lifecycle(); // loading, publishing, unloading, and the listeners -instance.blockWriter(); // the write path, including placement and destruction - -chunk.storage().views(); // read the sections without materialising them -chunk.storage().materialisedSections(); // how many actually exist -chunk.storage().shared(0); // is section 0 still the shared empty one -``` - -Lifecycle listeners are the extension point that replaced subclassing. Every method has a default: - -```java -instance.lifecycle().addListener(new ChunkLifecycleListener() { - @Override - public void onLoad(ChunkLifecycleEvent event) { - log.info("loaded {} {}", event.chunk().getChunkX(), event.chunk().getChunkZ()); - } -}); -``` - -They run **inside** the transition, before anybody else sees the chunk, which is what the light -engine needs — and why a throw from one fails the chunk load. For ordinary application code the -Minestom events named above are the right tool. - -Generation is the usual Minestom API, with one difference worth knowing: the generator is handed -copies of the section palettes and they are moved over only when it returns, so a generator that -fails halfway leaves the chunk exactly as it was rather than half built and published. - -```java -instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); -``` - -## Shared worlds - -Shared worlds are the one case `FalcoInstance` cannot serve, because `SharedInstance` takes an -`InstanceContainer` and nothing else. `FalcoSharedInstance` accepts that and builds on the container -instead: - -```java -InstanceManager manager = MinecraftServer.getInstanceManager(); -InstanceContainer world = manager.createInstanceContainer(); -world.setChunkSupplier(FalcoChunk::new); - -// Not manager.createSharedInstance(world): that factory always builds Minestom's own type. -FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world); -manager.registerSharedInstance(view); -``` - -The order in that example is not decoration. `createInstanceContainer` registers the container, and -the view's constructor refuses one that is not registered: `registerSharedInstance` performs no such -check, whereas the `createSharedInstance` this class cannot be built by does, and an unregistered -container is ticked by nobody. - -The view keeps its own generator, chunk supplier and auto-load setting, where Minestom's writes all -three through to the container and lets one view reconfigure another. All three are read once, in the -constructor, which cuts the other way too: turning auto chunk loading off on the container no longer -stops a view that already exists, so that has to be said to each view. Its tags were always its own — -`Instance` gives every instance a `TagHandler` and `SharedInstance` does not override it — but -`saveInstance()` handed the loader the container, so they were never written; here the view's own -data is what the loader is given. - -What none of that changes is who owns the blocks: `setBlock` reaches the container, and the container -serialises every write on its own monitor, because the method that performs the write is -`private synchronized` and is reached from three further places that an override cannot follow. A -world built this way keeps what the Falco chunk saves and keeps the container's write path; a world -that needs the write path uses `FalcoInstance` and gives up sharing. - -The per-view generator and chunk supplier are a repair and not a capability: they stop one view from -reconfiguring another, and nothing inside Minestom reads them, because the container creates every -chunk from its own. The reasoning is in -[Rationale: Instances and Chunks](docs/explanation/why-falco-instance-exists.md). - -## What "high-performance" means here - -Measured, not asserted. Every timing below comes from a JMH benchmark in this repository and is -quoted with the condition it was measured under. - -One condition is missing from all of them and is worth naming here rather than leaving implied: the -Minestom version. The rows that quote a factor compare Falco against the loader Minestom ships, so -the Minestom under test is part of the measurement, and no provenance line below records it — the -run commit is not recorded either, so it cannot be recovered after the fact. Every figure below was -taken before this repository moved to Minestom `2026.08.28-26.2`, and none has been re-run against -it. Read the comparison rows as unverified on 26.2 until a baseline is taken there; the jol count is -unaffected, being a count of objects on a heap rather than a comparison. - -One claim is not a timing and is marked as such where it appears: **a chunk allocates its sections -when something writes into them**, which takes a fresh chunk from 192 objects and 6 848 bytes to 25 -and 840. That comes from jol rather than from JMH — it is a count of objects on a heap, it has no -spread, and it is unaffected by what else the machine was doing. It also says nothing about speed. -Whether a smaller chunk makes anything faster depends on allocation pressure and on the collector, -and nobody here has measured that. The instance benchmarks exist; they have never been run as a -baseline, and until they have, no timing about the instance or the chunk appears in this file. - -**The Anvil loader is 1.9× faster on two threads** — 1 181 ± 31 against 2 200 ± 445 µs/op, reading -one chunk of 200 distinct block states. On one thread the intervals overlap and nothing is resolved -in either direction, which is the expected result: the claim is about lock granularity, and with one -reader there is no lock to contend for. At four and eight threads no factor can be quoted at all, -because Minestom's half-width there exceeds its own mean. What those rows establish is qualitative -and, for a server, the worse finding: under that load its read time stops being predictable, while -Falco's stays at or below about a tenth of its mean at every thread count. Writing shows no -resolvable difference anywhere. - -`RegionFileComparisonBenchmark.falcoRead` / `.minestomRead`, `distinctStates = 200`, -`@State(Scope.Benchmark)` with a per-thread chunk slot, two threads (`-t 2`; JMH takes one `-t` per -run, so each thread count is a run of its own), one fork, `-Xms1g -Xmx1g`, 3 warmup and 5 -measurement iterations, both annotated at 1 s; earlier documentation of this table said 2 s and no -`-r` was recorded on any command line, so the run's iteration time is unrecorded and the annotation -value stands here. JMH 1.37, one 16-core machine recorded as not idle, run commit and run date not -recorded, no `results.json` committed. The conservative bounds on this row are 1.45× to 2.30× -faster, the wider of the two relative half-widths being 20 %, which holds the factor to one decimal. -One fork — the `±` covers variance between iterations of one JVM, not between JVM launches, defined -once in [Rationale: Measurement](docs/explanation/what-a-measurement-here-means.md#the-interval-after-a-number). -The full four-row read and write table is owned by -[Measured Results](docs/reference/measured-results.md#the-region-file-against-the-one-minestom-ships-with). - -The two-thread figure did not reproduce, and that belongs next to it: an independent run of the same -configuration put Minestom at 103 437 ± 856 306 µs/op there, which carries no factor at all. What -survives both runs is the direction and the loss of predictability, not the 1.9×. - -An independent second run of the same configuration, and not an erratum for the run above. -`RegionFileComparisonBenchmark.falcoRead` / `.minestomRead`, `distinctStates = 200`, two threads -(`-t 2`, one thread count per run), one fork, `-Xms1g -Xmx1g`, recorded as `-wi 3 -i 5`, the class -annotating 1 s for both and no `-r` appearing on the recorded command line, so the run's iteration -time is unrecorded. JMH 1.37, one 16-core machine recorded as not idle, run commit and run date not -recorded, no `results.json` committed. Minestom's half-width is 8.3 times its own mean here, which -carries no factor at any precision; Falco reproduced at 1 174 ± 71 µs/op against the 1 181 ± 31 of -the first run. One fork — the `±` covers variance between iterations of one JVM, not between JVM -launches, defined once in -[Rationale: Measurement](docs/explanation/what-a-measurement-here-means.md#the-interval-after-a-number). -All four rows of this run are in -[Anvil Chunk Loader](docs/explanation/how-the-anvil-loader-is-built.md#measured-concurrent-readers-of-one-region-file). - -**The light engine is 1.11× to 1.71× faster** over six scenarios, every pair of intervals disjoint, -with byte-identical output asserted on every build. - -`LightEngineComparisonBenchmark.falco` / `.minestom`, one section, `emissionMix = UNIFORM`, -`lightSources` 1, 8 and 64 against `occlusionPercent` 0 and 30 — the six scenarios — one thread, -one fork, `-Xms512m -Xmx512m`, run as `-f 1 -wi 5 -i 10`, which overrides the class annotation of -3 warmup and 5 measurement iterations; no `-r` was recorded and the class annotates 1 s per -iteration. JMH 1.37, one 16-core machine recorded as not idle, measured at commit `69381af`, run -date not recorded, no `results.json` committed. All six pairs of intervals are disjoint and the -wider relative half-width of each pair is under 5 %, which is what admits two decimals; the -conservative bounds are 1.07× to 1.16× faster on the narrowest row and 1.63× to 1.80× faster on the -widest. One fork — the `±` covers variance between iterations of one JVM, not between JVM launches, -defined once in -[Rationale: Measurement](docs/explanation/what-a-measurement-here-means.md#the-interval-after-a-number). -The six rows and their bounds are owned by -[Measured Results](docs/reference/measured-results.md#against-the-engine-minestom-ships-with). - -The figure after a `±` is [the half-width of a confidence interval over the measurement iterations of -one JVM launch](docs/explanation/what-a-measurement-here-means.md); where two -intervals overlap, no factor is printed. The charts and the methodology are in -[Benchmarking](docs/explanation/what-the-benchmarks-establish.md), the full tables in -[Measured Results](docs/reference/measured-results.md), and the -optimisations that did **not** pay off in -[Project Status](docs/project-record/project-status.md). +The [tutorial](https://docs.onelitefeather.net/falco/tutorials/load-your-first-world-with-falco) goes on +to light the world with `falco-light` and to check a clean shutdown. Worlds saved before Minecraft 26.1 +use a different directory layout; see +[Load an Anvil world](https://docs.onelitefeather.net/falco/how-to/load-an-anvil-world). ## Documentation -Everything past the quick start lives in the -[documentation](docs/README.md): +The full documentation is on GitBook at [docs.onelitefeather.net/falco](https://docs.onelitefeather.net/falco). +It is organised in four parts: + +- [Tutorials](https://docs.onelitefeather.net/falco/tutorials/load-your-first-world-with-falco) — a guided first project, start to finish. +- [How-to guides](https://docs.onelitefeather.net/falco/how-to/add-falco-to-your-build) — steps for a specific task, such as migrating a world or reproducing a measurement. +- [Reference](https://docs.onelitefeather.net/falco/reference/modules-and-coordinates) — modules, supported versions, the API surface, policies, exceptions and measured results. +- [Explanation](https://docs.onelitefeather.net/falco/explanation/choosing-between-falco-and-the-built-in-loader) — why the modules are built the way they are, and what they deliberately do not do. -- [Installation](docs/how-to/add-falco-to-your-build.md) — all three modules, - the BOM, Maven, snapshots, the rendered Javadoc, and building from source -- [Anvil Chunk Loader](docs/explanation/how-the-anvil-loader-is-built.md) — what the - loader does, how to use it standalone, and what it deliberately does not do -- [Light Engine](docs/explanation/how-the-light-engine-works.md) — the engine, calling - it by hand versus letting a chunk maintain its own light, its guarantees and its limits -- [Measured Results](docs/reference/measured-results.md) — the - measurement environment and every published table with its provenance: the benchmark class and - methods, the parameter values and the run configuration behind each row -- [Benchmarking](docs/explanation/what-the-benchmarks-establish.md) — what each published - table does and does not establish, and how a third party can re-perform any of it -- [Project Status](docs/project-record/project-status.md) — the decisions - that shape the design, what is in the branch, defects found and fixed, and what is open -- [Rationale](docs/README.md) — why it is built this way: - what each decision was weighed against, and where the argument is weaker than the figures suggest -- [Research](docs/project-record/research/README.md) — the investigations run - before writing any code, kept because each answers a question worth asking again -- [Contributing](docs/contributing/README.md) — what a change is - built, tested and released with: the toolchain and library versions, the Gradle commands, the - conventions the build enforces, what a push to `main` publishes, and what review looks for -- [Build Setup](docs/contributing/build-setup.md) — how the Gradle build - is put together, and where the rest of it is documented: dependency management, publishing, - versioning and releases, tests and Javadoc, the benchmark and demo modules, the architecture rules +## Contributing The shortest path from a clone to a green build, and the one credential problem that stops it, are in -[`CONTRIBUTING.md`](CONTRIBUTING.md) in this repository. +[`CONTRIBUTING.md`](CONTRIBUTING.md). The +[contributing guide](https://docs.onelitefeather.net/falco/contributing) covers conventions, testing, +releases and what review looks for. ## Licence diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 62df9cd..20dfd58 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -9,70 +9,70 @@ ## How-to guides * [How-to guides](how-to/README.md) - * [Add Falco to your build](how-to/add-falco-to-your-build.md) - * [Load an Anvil world](how-to/load-an-anvil-world.md) - * [Compute light for a loaded world](how-to/compute-light-for-a-loaded-world.md) - * [Keep chunk light up to date automatically](how-to/keep-chunk-light-up-to-date-automatically.md) - * [Use FalcoInstance instead of InstanceContainer](how-to/use-falcoinstance-instead-of-instancecontainer.md) - * [Migrate a world from an older version](how-to/migrate-a-world-from-an-older-version.md) - * [Replace the version and unknown-entry policies](how-to/replace-the-version-and-unknown-entry-policies.md) - * [Build Falco from source](how-to/build-falco-from-source.md) - * [Run the JMH benchmark suite](how-to/run-the-jmh-benchmark-suite.md) - * [Reproduce a published measurement](how-to/reproduce-a-published-measurement.md) +* [Add Falco to your build](how-to/add-falco-to-your-build.md) +* [Load an Anvil world](how-to/load-an-anvil-world.md) +* [Compute light for a loaded world](how-to/compute-light-for-a-loaded-world.md) +* [Keep chunk light up to date automatically](how-to/keep-chunk-light-up-to-date-automatically.md) +* [Use FalcoInstance instead of InstanceContainer](how-to/use-falcoinstance-instead-of-instancecontainer.md) +* [Migrate a world from an older version](how-to/migrate-a-world-from-an-older-version.md) +* [Replace the version and unknown-entry policies](how-to/replace-the-version-and-unknown-entry-policies.md) +* [Build Falco from source](how-to/build-falco-from-source.md) +* [Run the JMH benchmark suite](how-to/run-the-jmh-benchmark-suite.md) +* [Reproduce a published measurement](how-to/reproduce-a-published-measurement.md) ## Reference * [Reference](reference/README.md) - * [Modules and coordinates](reference/modules-and-coordinates.md) - * [Supported versions](reference/supported-versions.md) - * [Measured results](reference/measured-results.md) - * [API surface and stability](reference/api-surface-and-stability.md) - * [Replaceable policies](reference/replaceable-policies.md) - * [Migration modes](reference/migration-modes.md) - * [Exceptions and faults](reference/exceptions-and-faults.md) - * [Logging](reference/logging.md) - * [Benchmark catalogue](reference/benchmark-catalogue.md) +* [Modules and coordinates](reference/modules-and-coordinates.md) +* [Supported versions](reference/supported-versions.md) +* [Measured results](reference/measured-results.md) +* [API surface and stability](reference/api-surface-and-stability.md) +* [Replaceable policies](reference/replaceable-policies.md) +* [Migration modes](reference/migration-modes.md) +* [Exceptions and faults](reference/exceptions-and-faults.md) +* [Logging](reference/logging.md) +* [Benchmark catalogue](reference/benchmark-catalogue.md) ## Explanation * [Explanation](explanation/README.md) - * [Choosing between Falco and the built-in loader](explanation/choosing-between-falco-and-the-built-in-loader.md) - * [Scope and non-goals](explanation/scope-and-non-goals.md) - * [When light computation actually runs](explanation/when-light-computation-actually-runs.md) - * [Measurement](explanation/what-a-measurement-here-means.md) - * [Choosing between FalcoInstance and InstanceContainer](explanation/choosing-between-falcoinstance-and-instancecontainer.md) - * [How the Anvil loader is built](explanation/how-the-anvil-loader-is-built.md) - * [How the light engine works](explanation/how-the-light-engine-works.md) - * [Why the concurrency design is what it is](explanation/how-the-concurrency-design-works.md) - * [How world migration works](explanation/how-world-migration-works.md) - * [The chunk version guard](explanation/the-chunk-version-guard.md) - * [Why Falco has its own Anvil chunk loader](explanation/why-a-second-anvil-loader.md) - * [Why Falco has its own light engine](explanation/why-a-custom-light-engine.md) - * [Why falco-instance exists](explanation/why-falco-instance-exists.md) - * [Comparing the light engine with Minestom's](explanation/comparing-the-light-engine-with-minestoms.md) - * [What the benchmarks establish](explanation/what-the-benchmarks-establish.md) +* [Choosing between Falco and the built-in loader](explanation/choosing-between-falco-and-the-built-in-loader.md) +* [Scope and non-goals](explanation/scope-and-non-goals.md) +* [When light computation actually runs](explanation/when-light-computation-actually-runs.md) +* [Measurement](explanation/what-a-measurement-here-means.md) +* [Choosing between FalcoInstance and InstanceContainer](explanation/choosing-between-falcoinstance-and-instancecontainer.md) +* [How the Anvil loader is built](explanation/how-the-anvil-loader-is-built.md) +* [How the light engine works](explanation/how-the-light-engine-works.md) +* [Why the concurrency design is what it is](explanation/how-the-concurrency-design-works.md) +* [How world migration works](explanation/how-world-migration-works.md) +* [The chunk version guard](explanation/the-chunk-version-guard.md) +* [Why Falco has its own Anvil chunk loader](explanation/why-a-second-anvil-loader.md) +* [Why Falco has its own light engine](explanation/why-a-custom-light-engine.md) +* [Why falco-instance exists](explanation/why-falco-instance-exists.md) +* [Comparing the light engine with Minestom's](explanation/comparing-the-light-engine-with-minestoms.md) +* [What the benchmarks establish](explanation/what-the-benchmarks-establish.md) ## Project record * [Project record](project-record/README.md) - * [Status — Anvil chunk loader and light engine](project-record/project-status.md) - * [Research notes](project-record/research/README.md) - * [Research: could `falco-instance` be faster, and how would anyone know?](project-record/research/instance-performance-research.md) - * [Research: exception hierarchy for the Anvil package](project-record/research/exception-hierarchy.md) - * [Research: a fluent construction API for the three modules](project-record/research/fluent-api.md) - * [Research: a multithreaded `InstanceContainer` replacement](project-record/research/instance-container.md) - * [Research: a faster, lower-memory light engine](project-record/research/light-engine.md) - * [Research: shared instances and batch integration for a foreign instance](project-record/research/shared-instances-and-batches.md) +* [Status — Anvil chunk loader and light engine](project-record/project-status.md) +* [Research notes](project-record/research/README.md) +* [Research: could `falco-instance` be faster, and how would anyone know?](project-record/research/instance-performance-research.md) +* [Research: exception hierarchy for the Anvil package](project-record/research/exception-hierarchy.md) +* [Research: a fluent construction API for the three modules](project-record/research/fluent-api.md) +* [Research: a multithreaded `InstanceContainer` replacement](project-record/research/instance-container.md) +* [Research: a faster, lower-memory light engine](project-record/research/light-engine.md) +* [Research: shared instances and batch integration for a foreign instance](project-record/research/shared-instances-and-batches.md) ## Contributing * [Contributing](contributing/README.md) - * [Build setup](contributing/build-setup.md) - * [Dependency management](contributing/dependency-management.md) - * [Versioning and releases](contributing/versioning-and-releases.md) - * [Publishing](contributing/publishing.md) - * [Testing and Javadoc](contributing/testing-and-javadoc.md) - * [Benchmarks and demo](contributing/benchmarks-and-demo.md) - * [Architecture rules](contributing/architecture-rules.md) - * [Documentation conventions](contributing/documentation-conventions.md) - * [Benchmark baselines](benchmarks/README.md) +* [Build setup](contributing/build-setup.md) +* [Dependency management](contributing/dependency-management.md) +* [Versioning and releases](contributing/versioning-and-releases.md) +* [Publishing](contributing/publishing.md) +* [Testing and Javadoc](contributing/testing-and-javadoc.md) +* [Benchmarks and demo](contributing/benchmarks-and-demo.md) +* [Architecture rules](contributing/architecture-rules.md) +* [Documentation conventions](contributing/documentation-conventions.md) +* [Benchmark baselines](benchmarks/README.md) diff --git a/docs/explanation/why-falco-instance-exists.md b/docs/explanation/why-falco-instance-exists.md index 30d00b8..3f09477 100644 --- a/docs/explanation/why-falco-instance-exists.md +++ b/docs/explanation/why-falco-instance-exists.md @@ -356,6 +356,9 @@ call legal. the setter `public` itself and holds its blocks in a `BlockStorage` of its own, so nothing about the storage is inherited any more. The barrier is unchanged — it is answered one class lower down. +Before `1.0.0` this was a real choice for a server. The lighting chunk and the Falco chunk both extended +`DynamicChunk`, a class has one superclass, and a server had to pick one of the two. + The sentence that used to stand here said that `FalcoChunk` adds nothing beyond re-exposing those members, and that Minestom's block storage was never the part that needed replacing. Both were true of the first version and neither is true now. What changed the answer was a measurement rather than diff --git a/docs/how-to/keep-chunk-light-up-to-date-automatically.md b/docs/how-to/keep-chunk-light-up-to-date-automatically.md index c90442c..d61f7fb 100644 --- a/docs/how-to/keep-chunk-light-up-to-date-automatically.md +++ b/docs/how-to/keep-chunk-light-up-to-date-automatically.md @@ -68,6 +68,55 @@ that already has a pool can hand that one over instead. source is retracted before the light is spread again. How this works, and why the update is identical to a full recalculation, is explained in [Explanation How the light engine works](../explanation/how-the-light-engine-works.md#incremental-updates-and-what-they-keep). +## Tune the scheduler + +The builder carries the settings that matter under load. Every one has a default; the values below +are the ones you would most often set: + +```java +ChunkLightScheduler scheduler = ChunkLightScheduler.builder(lighting) + .executor(ChunkLightScheduler.defaultExecutor()) + .maxAreaSize(4) // chunks per side of one lighting area + .maxCachedChunks(256) // opacity tables kept between passes + .skyLight(ChunkLightScheduler.SkyLight.FROM_DIMENSION) + .onFailure(throwable -> log.error("lighting failed", throwable)) + .build(); +``` + +`maxAreaSize` decides where one lighting area ends: a larger value means fewer seams between areas, and +a smaller one means less work per pass. `SkyLight.FROM_DIMENSION` computes sky light only when the +dimension has skylight. `onFailure` receives what a failed pass threw, so a lighting failure is logged +instead of disappearing. + +## Tell the scheduler about changes Falco did not make + +When something outside Falco changes the world, say so, and the chunk is relit on the next tick: + +```java +scheduler.markChanged(instance, 0, 0); // this chunk needs relighting +scheduler.markChanged(instance, 0, 0, 8, 40, 8); // this position did +scheduler.markDirty(instance, 0, 0); // relight without an incremental path +``` + +## Use it on an InstanceContainer + +On a `FalcoInstance` the supplier is all you need. On an `InstanceContainer`, build the chunks yourself, +hang a `ChunkLightListener` on each one, and tick the scheduler from a repeating task: + +```java +container.setChunkSupplier((instance, x, z) -> { + FalcoChunk chunk = new FalcoChunk(instance, x, z); + chunk.addLifecycleListener(new ChunkLightListener(scheduler)); + return chunk; +}); +MinecraftServer.getSchedulerManager().buildTask(() -> scheduler.onTick(container, System.currentTimeMillis())) + .repeat(TaskSchedule.tick(1)) + .schedule(); +``` + +Use `FalcoInstance` unless you need the container's own write path; see +[Use FalcoInstance instead of InstanceContainer](use-falcoinstance-instead-of-instancecontainer.md). + ## Check it worked Place a block, let one tick pass, and the surrounding light is correct without any call of your own. diff --git a/docs/how-to/load-an-anvil-world.md b/docs/how-to/load-an-anvil-world.md index e5a5ee0..d580fe5 100644 --- a/docs/how-to/load-an-anvil-world.md +++ b/docs/how-to/load-an-anvil-world.md @@ -96,6 +96,41 @@ The three-argument constructor sets how many region files stay open. The default FalcoAnvilLoader loader = new FalcoAnvilLoader(worldRoot, dimension, 256); ``` +## Tune the builder + +The two-argument constructor is enough for most servers. The builder is there when the defaults do not +fit: + +```java +FalcoAnvilLoader loader = FalcoAnvilLoader.builder() + .openRegionLimit(64) // region files kept open at once + .compressionLevel(2) // 1..9, the trade between write time and file size + .saveParallelism(4) // threads a saveChunks call may use + .dataVersion(4189) // what a written chunk claims to be + .diagnostics(new AnvilDiagnostics()) + .exceptionHandler(throwable -> log.warn("chunk load failed", throwable)) + .build(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); +``` + +`diagnostics()` matters on a live server. A world written by another version, or by a mod, can contain +blocks and biomes this loader does not know. The loader substitutes them and counts the substitutions +rather than failing: + +```java +AnvilDiagnostics diagnostics = loader.diagnostics(); +diagnostics.reportUnknownBlock("mod:strange_block"); // true the first time, false after +``` + +## See what the loader resolved + +Three accessors tell you what the loader is doing right now: + +- `regionDirectory()` gives the directory it resolved. +- `legacyLayout()` is true when it fell back to the pre-26.1 layout. +- `openRegionCount()` is how many region files are open at this moment. + +`close()` flushes every open region file. `ownsLoader(true)` on a `FalcoInstance` calls it for you. + ## Check it worked The loader writes an opening log line naming the resolved region directory and the policies it diff --git a/docs/how-to/use-falcoinstance-instead-of-instancecontainer.md b/docs/how-to/use-falcoinstance-instead-of-instancecontainer.md index eac7119..9909bfd 100644 --- a/docs/how-to/use-falcoinstance-instead-of-instancecontainer.md +++ b/docs/how-to/use-falcoinstance-instead-of-instancecontainer.md @@ -151,6 +151,17 @@ of the four Minestom sites that branch on `instanceof InstanceContainer` — see The view keeps its own generator, chunk supplier and auto-load setting. The order of the calls and the limits of sharing are explained in [Explanation Why falco-instance exists](../explanation/why-falco-instance-exists.md#shared-instances-refused-by-the-compiler-which-is-the-good-outcome). +## Set a generator + +Generation uses the usual Minestom API: + +```java +instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); +``` + +The generator is handed copies of the section palettes, and they are moved over only when it returns. +A generator that fails halfway therefore leaves the chunk as it was, instead of half built and published. + ## Read a chunk without materialising its sections 8. Read through `chunk.storage()` and not through `getSections()`, `getSection(int)` or `sections()`. The diff --git a/docs/reference/measured-results.md b/docs/reference/measured-results.md index e4f5dcb..bdb1ff9 100644 --- a/docs/reference/measured-results.md +++ b/docs/reference/measured-results.md @@ -20,6 +20,13 @@ defects found and fixed, and what is still open — is [Project Status](../proje > the `±` covers is defined once in [What a measurement here means](../explanation/what-a-measurement-here-means.md); how to re-run > any of it is in [What the benchmarks establish](../explanation/what-the-benchmarks-establish.md). > +> **The Minestom version is part of every timing, and it is not recorded for any of them.** The rows +> that quote a factor compare Falco with the loader and engine Minestom ships, so the Minestom under test +> is part of the measurement. The run commit is not recorded either, so it cannot be recovered later. +> Every figure on this page was taken before this repository moved to Minestom `2026.08.28-26.2`, and none +> has been re-run against it. Read the comparison rows as unverified on 26.2 until a baseline is taken +> there. The object counts in [Counted](#counted) are not affected, because they count objects on a heap. +> > [Counted](#counted) is the exception and is separated for it: object and byte counts are > deterministic, carry no `±`, and are unaffected by what else the machine was doing. They are the > only figures on this page that survive being taken on a busy machine. diff --git a/falco-demo/README.md b/falco-demo/README.md index 0955c8e..efbd926 100644 --- a/falco-demo/README.md +++ b/falco-demo/README.md @@ -2,7 +2,7 @@ Put your own world in here and find out, on your own machine, whether the Falco stack does better than the one Minestom ships with. Nothing in this module is published; it exists so the claims in -[Project Status](https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/project-record/project-status.md) can be checked +[Project Status](https://docs.onelitefeather.net/falco/project-record/project-status) can be checked rather than believed. There are two ways to check, and they answer different questions: @@ -123,7 +123,7 @@ counted; timing those would report the speed of a header lookup as the speed of - **It is not a benchmark and cannot be quoted.** One session, one machine, one route flown by hand, no repetition, no isolation, no statistical model. Anything you intend to publish belongs in the JMH benchmarks of [`falco-benchmarks`](https://github.com/OneLiteFeatherNET/Falco/tree/main/falco-benchmarks) — see - [What a measurement here means](https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/explanation/what-a-measurement-here-means.md) — with the machine, the + [What a measurement here means](https://docs.onelitefeather.net/falco/explanation/what-a-measurement-here-means) — with the machine, the jvm and the JMH configuration next to the number. - **Your route is a variable.** Two flights over the same world load different chunks in a different order. Two impressions that differ by a little are not a result. @@ -220,7 +220,7 @@ then lists the warm-up rounds, then the measured ones, then the summary: - **It is not a benchmark.** No forks, no statistical model, no isolation. For anything you intend to quote, use the JMH benchmarks in [`falco-benchmarks`](https://github.com/OneLiteFeatherNET/Falco/tree/main/falco-benchmarks) — see - [What a measurement here means](https://github.com/OneLiteFeatherNET/Falco/blob/main/docs/explanation/what-a-measurement-here-means.md) — and report the + [What a measurement here means](https://docs.onelitefeather.net/falco/explanation/what-a-measurement-here-means) — and report the machine, the jvm and the JMH configuration alongside the number, as that document requires. - **A number without its thread count means nothing here.** The Falco loader wins on lock granularity, which by definition cannot show itself without contention. Single threaded it is if