Links
Tags
Creators
Details
0.17.0-forge
Compatibility
Changes
v0.17.0 — Main-thread save capture cost roughly halved
0.17.0 is a performance release. The main-thread cost of capturing a chunk for saving drops by roughly half, by removing a full-chunk scan that ran on every snapshot but whose result was never used. This build also closes a data-safety gap where a third-party biome container could be referenced directly into an async snapshot
Removing the dead block recount during capture (issue #24)
Main-thread capture previously wrapped each section's two paletted containers in a LevelChunkSection. That constructor unconditionally calls recalcBlockCounts(), which walks all 4096 cells of every section whose palette holds more than one entry, feeding each into a hash map — about 98,000 hash writes for a 24-section chunk
The three counts it produces (non-empty blocks, randomly ticking blocks, randomly ticking fluids) only serve live-chunk tick scheduling and network sync. Vanilla reads none of them when serializing a section — a copy with completely wrong counts serializes byte-for-byte identically to a correct one. In other words, the result of that scan was never used anywhere on the save path
This build carries only the two containers the worker actually needs for encoding. The on-disk NBT is unchanged, and no mixin or vanilla behavior is involved
Before/after on a real production server (Forge 1.20.1, 137 mods, AMD Ryzen 9 9950X3D2, ZGC, 60-second sample, eventCompatMode = PARTIAL):
| Main-thread frame | Before | After |
|---|---|---|
Capture entry captureWithGeneration |
6692ms (19.1% of busy time) | 2948ms (8.2%) |
Section capture copySections within it |
4096ms | 1024ms |
Block recount recalcBlockCounts |
3152ms | 0 (call no longer exists) |
| BetterAutoSave total main-thread footprint | 25.8% of busy time | 15.2% |
The number of chunks saved in each sample was cross-checked against frames unaffected by this change; the "after" run actually processed about 15% more, so the reductions above are not inflated by a lighter workload
A side effect: with the per-section temporary hash map gone, ZGC relocation read barriers (forwarding_find) fell 32%. The paletted container deep copy itself became 20% faster as a result, even though that code was not touched
To be clear, this does not raise TPS on an already healthy server — both samples ran at TPS 20. What it buys is main-thread headroom: the same save throughput now occupies less main-thread time, making it less likely to crowd the tick budget under heavy load or with large worlds
No more duplicate capture in FULL compatibility mode
Under eventCompatMode = FULL, snapshot capture (section copies, light layer clones, heightmap clones, block entity NBT, structure data copies) previously ran unconditionally before the mode branch — even though that mode actually uses the complete NBT produced by vanilla ChunkSerializer.write, which gathers the same data itself. That gather-then-discard work is now skipped
Users who switched to FULL to work around issue #8 no longer pay double the main-thread capture cost
Data safety
If a live section's biome container is not vanilla's PalettedContainer implementation — which vanilla never produces, and only occurs when a third-party mod supplies a read-only container — it was previously referenced directly into the async snapshot. The read-only interface has no copy method, so it could not be detached, meaning the main thread could still mutate the same object while a worker encoded it. This build encodes it to NBT on the main thread instead, fully separating it from the live container
Upgrading
- Drop-in jar replacement, no config changes, no save format changes
- The on-disk NBT is byte-for-byte identical to before, so rolling back to 0.16.3 is safe at any time
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:WAXR3mM5:LIzqe94f"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:WAXR3mM5:LIzqe94f"
}

