All Mods Resource Packs Data Packs Shaders Modpacks Plugins
Shinoyuki-BetterAutoSave

Shinoyuki-BetterAutoSave

by Shinoyuki_Miyako • Downloads: 0 • Followers: 0
Description
Gallery
Changelog
Versions

BetterAutoSave

简体中文 | English

BetterAutoSave

Make server autosaves stutter-free ~ Parts of this mod's code were generated by Claude Opus 4.8 / Claude Fable 5. If you run into any issue, please open an issue

Download: Modrinth · GitHub Releases

Project status: actively developed and in a fast pre-1.0 iteration phase, with frequent updates (including releases coordinated with BetterBackup). The core async save has been validated in production for a long time and is safe by default; async chunk loading is a newer, opt-in feature and is off by default. Watch Releases / Modrinth for updates

What problem does this mod solve

A vanilla Minecraft server autosaves every 5 minutes. During that save, the main thread has to serialize every modified chunk and write it to disk — and the whole server is frozen while it happens. On an empty server you will not notice, but on a server with many mods and players this pause is routinely 200 ms to several seconds, and every player lags at once.

Besides the periodic autosave, several other moments stutter the same way: players teleporting or large numbers of chunks being unloaded (a chunk must be saved before it leaves memory), entity-dense areas during a save (large farms / mob grinders), and global data such as villages and raids (vanilla SavedData) where a single large file hits the disk.

BAS makes the main thread do only the one thing that must happen in place — taking an independent snapshot of the data to be saved. Serialization and disk IO are handed to background threads. Because the background works on copies, it never interferes with the main thread, which lets go immediately after the snapshot. Chunks, entities and saved data all go through this pipeline. When the server is struggling BAS automatically slows down, but forces full speed as the next autosave cycle approaches so a backlog can never build up.

Requirements and installation

Both loaders are maintained from the same source. Server-side only on both; clients do not need to install it.

  • Forge 1.20.1: Forge 47.3.22 or newer (47.3 / 47.4 lines both work), Java 17 or newer
  • NeoForge 1.21.1: NeoForge 21.1 line, Java 21 or newer

Download the jar matching your loader from Modrinth or Releases and drop it into the server's mods/ folder:

  • For Forge, use the -all jar (named like shinoyuki_betterautosave-<version>-all.jar; it bundles MixinExtras and other dependencies). The plain thin jar crashes on load for missing dependencies.
  • For NeoForge, use shinoyuki_betterautosave-neoforge-<version>.jar.

After the first launch the config file is generated at config/Shinoyuki-Optimize/shinoyuki_betterautosave/common.toml. The defaults work out of the box; most servers do not need to change anything.

Will it lose world data?

No. BAS is designed on one premise: it must never be less safe than vanilla.

  • On shutdown it waits for every pending save to hit the disk before letting the server exit, and the final save goes through the vanilla synchronous path.
  • BAS never "holds saves for later" — a chunk enters background processing the moment it should be saved. There is no "nothing saved for minutes, crash loses it all" window (some similar mods have this problem).
  • If a background write fails it retries automatically and never pretends it succeeded: chunks and saved data fall back to the vanilla synchronous write once retries are exhausted; entities have no coordinate recovery queue and are already evicted from memory by vanilla, so an exhausted retry logs an ERROR and drops that chunk's latest entity increment — the same outcome as vanilla here (vanilla entity saving likewise has no retry and no synchronous fallback; BAS actually retries a few more times first).

Beyond that, the Forge build also fixes three vanilla paths that silently lose data (player data read failure, truncating writes for advancements and stats, and level.dat having only a single backup). Those fixes are on by default — see the configuration reference.

Common configuration

Key Default Description
general.enabled true Master switch; off means vanilla behavior, as if not installed
throttle.chunksPerTickBase 4 Max chunks snapshotted by the main thread per game tick
throttle.adaptiveEnabled true Slow down automatically when the server struggles; keep it on
workers.chunkWorkerThreads 2 Background threads for chunks
workers.entityWorkerThreads 2 Background threads for entities
workers.savedDataWorkerThreads 1 Background threads for saved data; raise to 2 with mods that write a lot of vanilla SavedData
compat.eventCompatMode PARTIAL Event compatibility level; leave it alone unless you know you need it

The Forge build has 43 settings, the NeoForge build 26. Every setting, why each default is what it is, and the recommended rollout path are documented in CONFIGURATION.en.md, covering player data protection, level.dat integrity, async chunk loading, Prometheus monitoring and working with backup tools.

Feature matrix across the two builds

The two builds are not identical. Some gaps exist because NeoForge fixed the problem upstream, so the corresponding option is unnecessary there; others are Forge-first and not yet ported symmetrically.

Feature Forge 1.20.1 NeoForge 1.21.1
Async saving (chunks / entities / SavedData) yes yes
Async chunk loading ([load] section) yes no (the section does not exist; the NeoForge build has no load-side mixins at all)
level.dat registry cache yes not needed (upstream moved the table out of level.dat)
level.dat startup check yes not needed (1.21 already falls back and quarantines on a read failure)
level.dat startup backup / post-write verify yes no (1.21 has no equivalent either; port pending)
playerdata read fallback yes not needed (fixed upstream in 1.21)
advancements / stats atomic write yes no (1.21 still truncates on write; port pending)
advancements dirty skip, staggered player saving yes no (performance only; port pending)
Sync chunk load / inter-tick gap diagnostics yes yes

In-game commands

Requires OP (permission level 2).

Command Effect
/betterautosave status One-line current status
/betterautosave metrics One-line metrics summary
/betterautosave debug Full diagnostics: queue depths, per-stage timings, counters
/betterautosave flush Drain every pending save to disk. The command returns immediately and polls in the background until it completes or times out (safety.shutdownTimeoutSeconds)
/betterautosave drain-unload Wait for all pending chunks to land; likewise polls in the background and returns immediately
/betterautosave hottest-chunks [count] List the slowest-saving chunks (default 10, accepts 1-50) to locate hotspots
/betterautosave force-async Force one background save pass over all chunks in the current dimension (diagnostic)
/betterautosave diagnose [count] List the sources of main-thread synchronous chunk loads and the inter-tick gap statistics (default 10, accepts 1-50)
/betterautosave diagnose reset Clear both tables above (cumulative counters are kept; see below)

High-cost chunks usually sit where block entities are dense — large automated farms, mod shop panels, complex redstone.

The stalls ordinary monitoring cannot see

Two kinds of stall never show up on a normal dashboard. Since 0.20.0 BAS records both itself — observing only, never intervening, on by default.

Main-thread synchronous chunk loads. When a chunk is not in memory and something on the main thread asks for it directly, the main thread waits in place until the disk read — and if needed, terrain generation — has finished, and the whole server is frozen meanwhile. A production stress test with 74 players measured a single wait of 5.2 seconds. BAS records every wait over 50 ms (threshold configurable): how long it blocked, the chunk coordinates, the dimension, and the first non-vanilla class on the call stack.

Long pauses between ticks. MSPT only measures time spent inside a game tick; the wait between two ticks is not counted. The same stress test contained pauses of 17.1 and 14.8 seconds that were invisible on the TPS graph and on the dashboard — everything looked healthy while players were timing out. BAS takes one timestamp at the start and one at the end of every tick and records any gap over 1 second (configurable); an optional deep mode, off by default, attributes a gap to the individual task that caused it.

Cost: the sync-load probe sits on the branch taken after vanilla's four-slot chunk cache misses, so it is not reached at all on a cache hit; on a miss it only adds two nanosecond reads, and a stack is captured solely once the wait exceeds the threshold — never during normal operation. The tick gap check is two nanosecond reads per tick.

On attribution: the detection reports which call chain the block happened on, not who has a bug. Stalls reported by this feature usually reflect another mod's call pattern — fetching a chunk synchronously is perfectly reasonable in many situations, its cost simply scales with server size, view distance and disk speed — and are not by themselves evidence of a defect in that mod. Treat it as the starting point of an investigation, not its conclusion.

Read it with /betterautosave diagnose, or scrape the four new Prometheus metrics (bas_sync_load_stalls_total, bas_tick_gap_max_seconds and two more). The eight settings and a full sample of the command output are in section 8 of CONFIGURATION.en.md.

Design boundary

BAS has been evaluated to the end of what it can do here: under the extreme compatibility constraints it holds itself to, there is no meaningful async chunk optimization left to take. Where room does remain, taking it would break that compatibility — producing data-safety problems and conflicts between mods.

In a production stress test with 74 players online, sampled over 550 seconds, NbtIo writes accounted for 0.03% of main-thread time, ChunkSerializer serialization for 0.67%, and everything BAS itself does for 1.42% in total. That is not the same as "there is no optimization left" — the same sample still shows roughly 0.3 percentage points on the table (copySections unconditionally makes two PalettedContainer.copy calls even for empty sections, about 0.1 pp; batching the POI replay, about 0.1 to 0.2 pp). But that is already down in the noise, and what can be taken without changing the compatibility premise adds up to less than half a percentage point.

It is also not the same as "BAS makes chunk loading stop being the bottleneck". The opposite is true: the bottleneck sits in the half of the chunk system BAS cannot reach. In that same sample, DistanceManager distance-field propagation consumed 82% of the chunk system's main-thread budget, and the tasks actually driving loads forward only 18%.

What each further step would break is concrete. Moving ForgeCaps onto a worker thread belongs to the same family as the data loss in issue #8: mods that attach a chunk capability lose data silently. Moving ChunkDataEvent.Load onto a worker thread calls every listener off the main thread; it throws nothing and simply rots over time. Moving POI / SectionStorage onto a worker thread runs into SectionStorage not being thread-safe, and the outcome is silently corrupted villager AI data. Taking over DistanceManager means a state machine that is not thread-safe, and a head-on conflict with C2ME. Requiring installation on both sides, or forcing everything async, gives up single-side installation, opt-in and instant rollback — the largest differentiator BAS has.

Performance has reached the limit compatibility allows, so this release changes direction: instead of chasing those last fractions of a percent, BAS now tells server owners where the stalls actually come from. The full reasoning is in ROADMAP.md (Chinese).

Mod conflicts

  • Cannot be installed together (all take over the same save path): Fast Async World Save (fastasyncworldsave, BAS logs a WARN when it detects this one), Smooth Chunk Save, and other async / per-tick save mods.
  • C2ME / C2ME-Forge: split it by feature. The save side is pick-one; parallel loading is complementary under BAS's default config but becomes pick-one once BAS async loading is enabled; worldgen is always complementary.
  • Compatible: Starlight, Radium / Canary, Modernfix, FerriteCore and similar.

The reasoning, the full list of injection points and the data-integrity contract of each compatibility level are in COMPATIBILITY.en.md.

Quick recovery if something goes wrong

All three options keep world data intact:

  1. Disable temporarily: set general.enabled to false, restart or /reload. The mod stays installed but all logic is skipped — pure vanilla.
  2. Uninstall completely: move the jar out of mods/ and restart. World data remains protected by vanilla saving; uninstalling loses nothing.
  3. Tune instead of removing: if you suspect a performance setting, adjust chunksPerTickBase (1-64) or switch eventCompatMode to FULL first — no need to uninstall.

Building / development

./gradlew build                 # compile + run all tests (common / forge / neoforge)
./gradlew :forge:runServer      # start a 1.20.1 Forge dev server
./gradlew :neoforge:runServer   # start a 1.21.1 NeoForge dev server

Module layout: common/ (zero-Minecraft pure-algorithm core, source-merged into both loaders — the crown-jewel save state machine lives here once, never forked) + forge/ (1.20.1) + neoforge/ (1.21.1).

The version roadmap and capability overview are in ROADMAP.md (Chinese); dual-version porting details are in archive/MULTIVERSION_PLAN.md (Chinese).

License

AGPL-3.0-or-later, with two section 7 additional permissions (LICENSE-EXCEPTION.md): a modpack distribution exception — unmodified official release jars may be included verbatim in modpacks and server packs with no obligation beyond keeping the project name and a repository link — and a Minecraft linking exception explicitly permitting combination with Minecraft itself and LGPL-licensed mod loaders. Modified versions of this mod remain under the full AGPL, including its section 13 network terms.

How to Install Shinoyuki-BetterAutoSave (Mod)

Shinoyuki-BetterAutoSave by Shinoyuki_Miyako is verified for Forge, Neoforge across Minecraft versions 1.20.1, 1.21.1 (latest release: 0.20.2-neoforge, file size: 207 KB). Follow the steps below to install it safely from ModCraft.tv:

  1. Click the Download button above and select your Minecraft version (1.20.1, 1.21.1) and platform (Forge, Neoforge).
  2. Make sure you have the compatible loader or environment installed for Forge, Neoforge.
  3. Move the downloaded Shinoyuki-BetterAutoSave file into your Minecraft .minecraft/mods folder and launch the game.

All Versions

+ Add Version
Name Game version Platforms Published Downloads Actions
1.21.1
Neoforge
1mo ago 1
1.20.1
Forge
1mo ago 1
1.21.1
Neoforge
2mo ago 1
1.20.1
Forge
2mo ago 1
1.21.1
Neoforge
2mo ago 1
1.20.1
Forge
2mo ago 1
1.21.1
Neoforge
3mo ago 1
1.20.1
Forge
3mo ago 1
1.21.1
Neoforge
3mo ago 1
1.20.1
Forge
3mo ago 1
1.21.1
Neoforge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
4mo ago 1
1.20.1
Forge
5mo ago 1
1.20.1
Forge
5mo ago 1
1.20.1
Forge
5mo ago 1
## v0.20.2 — Fixes two race windows in async chunk saving

In two very narrow timing windows, async chunk saving exposed a false "this chunk has no write in flight" signal to the main thread. When hit, the same chunk ends up with two concurrent writes, or a save request is wrongly treated as "already being handled" and skipped. The worst outcomes are the chunk rolling back to an older state after a restart, or the last edits before an unload never reaching disk

Each window is only a few instructions wide and requires the main thread to process a save of that same chunk at exactly that moment, so the real-world probability is low. They are data-safety issues nonetheless, and all 0.20.x users are advised to upgrade. This release only touches the chunk saving path

### 1. Problem

In ChunkMap.save the main thread picks one of two branches purely from the chunk's save state: if it is "in flight" (a write is already under way), the latest content is registered as a relay snapshot to be written once the in-flight write finishes; if it is "dirty", a new save cycle is started. Switching between these states has to happen in a single step, with no other value visible to the main thread in between

Two paths split that switch into two steps, briefly exposing "dirty" in the middle:

1. **In-place retry after a failed write**. When a write fails and maxRetries is not yet exhausted, the callback resubmits the already-serialized old data in place. It first set the state to "dirty" and only in the next step back to "in flight". A main thread caught in between concludes nothing is in flight: if it wins the race it opens a second save cycle that writes the same chunk concurrently with the retry; if it loses, it treats this save as "already being handled" and lets it through without registering a relay. If the latter happens during a chunk unload, the last edits before the unload have no copy left anywhere
2. **Relay write**. When the in-flight write finishes and a relay snapshot is waiting, the callback first set the state to "dirty" and took the snapshot, and only in the next step entered the relay cycle. Registering a relay does not mark the chunk as saved, so a main thread processing the same chunk during this gap likewise opens a second cycle. If the chunk was edited again after the relay snapshot was taken, both cycles consider their data stale when they land and neither marks the chunk clean; if the older data happens to finish writing last, the region file keeps the old content while the chunk is already marked as saved and will not be written again

The second window does not require a disk error — it only needs the same chunk to be edited and saved again while its previous write is in flight (typically on unload, or during autosave under load)

Under some interleavings both windows also leave the internal pending-drain counter permanently one too high: /betterautosave drain-unload never sees it reach zero, and the Prometheus mustDrain metric kee
...
## v0.20.2 — Fixes two race windows in async chunk saving

In two very narrow timing windows, async chunk saving exposed a false "this chunk has no write in flight" signal to the main thread. When hit, the same chunk ends up with two concurrent writes, or a save request is wrongly treated as "already being handled" and skipped. The worst outcomes are the chunk rolling back to an older state after a restart, or the last edits before an unload never reaching disk

Each window is only a few instructions wide and requires the main thread to process a save of that same chunk at exactly that moment, so the real-world probability is low. They are data-safety issues nonetheless, and all 0.20.x users are advised to upgrade. This release only touches the chunk saving path

### 1. Problem

In ChunkMap.save the main thread picks one of two branches purely from the chunk's save state: if it is "in flight" (a write is already under way), the latest content is registered as a relay snapshot to be written once the in-flight write finishes; if it is "dirty", a new save cycle is started. Switching between these states has to happen in a single step, with no other value visible to the main thread in between

Two paths split that switch into two steps, briefly exposing "dirty" in the middle:

1. **In-place retry after a failed write**. When a write fails and maxRetries is not yet exhausted, the callback resubmits the already-serialized old data in place. It first set the state to "dirty" and only in the next step back to "in flight". A main thread caught in between concludes nothing is in flight: if it wins the race it opens a second save cycle that writes the same chunk concurrently with the retry; if it loses, it treats this save as "already being handled" and lets it through without registering a relay. If the latter happens during a chunk unload, the last edits before the unload have no copy left anywhere
2. **Relay write**. When the in-flight write finishes and a relay snapshot is waiting, the callback first set the state to "dirty" and took the snapshot, and only in the next step entered the relay cycle. Registering a relay does not mark the chunk as saved, so a main thread processing the same chunk during this gap likewise opens a second cycle. If the chunk was edited again after the relay snapshot was taken, both cycles consider their data stale when they land and neither marks the chunk clean; if the older data happens to finish writing last, the region file keeps the old content while the chunk is already marked as saved and will not be written again

The second window does not require a disk error — it only needs the same chunk to be edited and saved again while its previous write is in flight (typically on unload, or during autosave under load)

Under some interleavings both windows also leave the internal pending-drain counter permanently one too high: /betterautosave drain-unload never sees it reach zero, and the Prometheus mustDrain metric kee
...
## v0.20.1 — Fixes a startup crash when installed alongside Lithium-family optimisation mods

0.20.0 crashes the server at startup when installed together with Harium, Radium, Radium Re-Reforged, Canary or another Lithium port. If you run any of those, upgrade to 0.20.1 or roll back to 0.19.0. Only 0.20.0 is affected

### 1. Symptom

Mixin application fails at startup and the entire config is rejected:

``
Mixin apply failed shinoyuki_betterautosave.mixins.json:ServerChunkCacheSyncLoadMixin
  • > net.minecraft.server.level.ServerChunkCache
InvalidInjectionException: @At("INVOKE") on ...betterautosave$measureSyncChunkLoad with priority 1000
cannot inject into ServerChunkCache::m_7587_ merged by
me.jellysquid.mods.lithium.mixin.world.chunk_access.ServerChunkManagerMixin with priority 1000
`

What fails is the sync-load detector added in 0.20.0, but the whole of
shinoyuki_betterautosave.mixins.json goes down with it — async saving and async loading stop working too, and the server does not start

### 2. Cause

The 0.20.0 detector wraps the "wait until the chunk is ready" call inside
ServerChunkCache.getChunk with @WrapOperation and declares require = 0, intending "if the injection point is gone, skip silently rather than let a pure observation feature take the server down"

That declaration does not cover the failure mode that actually occurred. Lithium ports
@Overwrite getChunk outright, and Mixin has an earlier check in Injector.findTargetNodes: if the injector's priority is not higher than that of the mixin which already merged the method, it throws InvalidInjectionException immediately. Both sides sit at the default priority of 1000, so the condition fails — and because this check runs *before* the require hit-count check, require = 0 is never consulted

The 0.20.0 release notes said "the detector silently does nothing rather than failing server startup". The intent was right; the implementation did not deliver it. This release does

### 3. Fix

The detector's injection now passes a gate at class-load time. If either condition holds, the mixin is not applied at all:

1.
diagnostics.syncLoadDetection is false in the on-disk common.toml (a missing file, section or key defaults to true)
2. A mod known to
@Overwrite getChunk is present: harium, radium, canary, lithium

When the gate closes, a WARN line names the mod responsible:

`
[BetterAutoSave] sync chunk load detector disabled: mod 'harium' overwrites
ServerChunkCache.getChunk, which Mixin will not let us wrap. Async saving and loading are unaffected
`

That line is necessary: without it, "I installed 0.20 but never see any sync-load reports" has no explanation. **Saving, async loading and every other feature are unaffected by this gate**

Raising the mixin's priority was considered and rejected. BAS would then apply first, and the other mod's
@Overwrite` would replace the whole method body along with the injection — moving the crash risk onto them
...
## v0.20.1 — Fixes a startup crash when installed alongside Lithium-family optimisation mods

0.20.0 crashes the server at startup when installed together with Harium, Radium, Radium Re-Reforged, Canary or another Lithium port. If you run any of those, upgrade to 0.20.1 or roll back to 0.19.0. Only 0.20.0 is affected

### 1. Symptom

Mixin application fails at startup and the entire config is rejected:

``
Mixin apply failed shinoyuki_betterautosave.mixins.json:ServerChunkCacheSyncLoadMixin
  • > net.minecraft.server.level.ServerChunkCache
InvalidInjectionException: @At("INVOKE") on ...betterautosave$measureSyncChunkLoad with priority 1000
cannot inject into ServerChunkCache::m_7587_ merged by
me.jellysquid.mods.lithium.mixin.world.chunk_access.ServerChunkManagerMixin with priority 1000
`

What fails is the sync-load detector added in 0.20.0, but the whole of
shinoyuki_betterautosave.mixins.json goes down with it — async saving and async loading stop working too, and the server does not start

### 2. Cause

The 0.20.0 detector wraps the "wait until the chunk is ready" call inside
ServerChunkCache.getChunk with @WrapOperation and declares require = 0, intending "if the injection point is gone, skip silently rather than let a pure observation feature take the server down"

That declaration does not cover the failure mode that actually occurred. Lithium ports
@Overwrite getChunk outright, and Mixin has an earlier check in Injector.findTargetNodes: if the injector's priority is not higher than that of the mixin which already merged the method, it throws InvalidInjectionException immediately. Both sides sit at the default priority of 1000, so the condition fails — and because this check runs *before* the require hit-count check, require = 0 is never consulted

The 0.20.0 release notes said "the detector silently does nothing rather than failing server startup". The intent was right; the implementation did not deliver it. This release does

### 3. Fix

The detector's injection now passes a gate at class-load time. If either condition holds, the mixin is not applied at all:

1.
diagnostics.syncLoadDetection is false in the on-disk common.toml (a missing file, section or key defaults to true)
2. A mod known to
@Overwrite getChunk is present: harium, radium, canary, lithium

When the gate closes, a WARN line names the mod responsible:

`
[BetterAutoSave] sync chunk load detector disabled: mod 'harium' overwrites
ServerChunkCache.getChunk, which Mixin will not let us wrap. Async saving and loading are unaffected
`

That line is necessary: without it, "I installed 0.20 but never see any sync-load reports" has no explanation. **Saving, async loading and every other feature are unaffected by this gate**

Raising the mixin's priority was considered and rejected. BAS would then apply first, and the other mod's
@Overwrite` would replace the whole method body along with the injection — moving the crash risk onto them
...
## v0.20.0 — Making main-thread sync chunk loads and inter-tick pauses observable

0.20.0 adds two diagnostic features that observe and never intervene: detection of main-thread synchronous chunk loads, and monitoring of long pauses between ticks. Both are on by default, and both builds have them

### 1. Why these two stalls were invisible

A production server with 74 players online froze. In the spark profile, BetterAutoSave's own save path on the main thread (NbtIo writes) accounted for 0.03% — saving was not the cause. The two actual sources were:

  • One main-thread synchronous chunk load, triggered by third-party code, blocking for 5.2 seconds
  • Two pauses that happened between ticks, 17.1 seconds and 14.8 seconds

The second kind is the harder one. MSPT only measures time spent inside a tick; the wait between two ticks is not counted, so those 17 seconds are invisible on the TPS graph and on every ordinary monitoring dashboard — the dashboard looks healthy while players are timing out. Digging both out of spark's inclusive call tree took hours of manual work. 0.20.0 makes the server report them by itself

### 2. Main-thread synchronous chunk loads

When a chunk is not in memory and something on the main thread asks for it directly, the main thread waits in place until the disk read — and if needed, terrain generation — has finished. The whole server is frozen for that duration. Vanilla rarely takes this path during normal play; what usually triggers it is third-party logic fetching a chunk by coordinates inside an event callback, a scheduled task or a command

BAS times the "wait until the chunk is ready" call inside vanilla's ServerChunkCache.getChunk. A wait of at least diagnostics.syncLoadThresholdMs (default 50 ms, one game tick) is recorded with its duration, chunk coordinates, dimension, and the first non-vanilla class on the call stack:

``
[BetterAutoSave] main-thread sync chunk load: 5188ms at (120,-340) in minecraft:overworld, called from com.example.protection.RegionScanner
`

Each distinct source prints one line the first time it appears; after that it only accumulates into the table, so a player exploring new terrain cannot flood the log

The instrumented call sits on the branch taken after vanilla's four-slot chunk cache misses: when the requested chunk is already cached, execution never reaches it. On a miss it costs two extra
System.nanoTime()` calls (tens of nanoseconds), and a stack is captured only once the wait actually exceeds the threshold — during normal operation none is ever captured

### 3. Inter-tick pauses

Between the end of one tick and the start of the next, the server waits for the next tick while draining its task queue. That time is not part of MSPT. If something in the queue stalls — a task submitted by another mod, a chunk system callback, a command — MSPT can stay perfectly healthy while players have been frozen for ten seconds or more

The default mode takes one timestamp at the start and one a
...
## v0.20.0 — Making main-thread sync chunk loads and inter-tick pauses observable

0.20.0 adds two diagnostic features that observe and never intervene: detection of main-thread synchronous chunk loads, and monitoring of long pauses between ticks. Both are on by default, and both builds have them

### 1. Why these two stalls were invisible

A production server with 74 players online froze. In the spark profile, BetterAutoSave's own save path on the main thread (NbtIo writes) accounted for 0.03% — saving was not the cause. The two actual sources were:

  • One main-thread synchronous chunk load, triggered by third-party code, blocking for 5.2 seconds
  • Two pauses that happened between ticks, 17.1 seconds and 14.8 seconds

The second kind is the harder one. MSPT only measures time spent inside a tick; the wait between two ticks is not counted, so those 17 seconds are invisible on the TPS graph and on every ordinary monitoring dashboard — the dashboard looks healthy while players are timing out. Digging both out of spark's inclusive call tree took hours of manual work. 0.20.0 makes the server report them by itself

### 2. Main-thread synchronous chunk loads

When a chunk is not in memory and something on the main thread asks for it directly, the main thread waits in place until the disk read — and if needed, terrain generation — has finished. The whole server is frozen for that duration. Vanilla rarely takes this path during normal play; what usually triggers it is third-party logic fetching a chunk by coordinates inside an event callback, a scheduled task or a command

BAS times the "wait until the chunk is ready" call inside vanilla's ServerChunkCache.getChunk. A wait of at least diagnostics.syncLoadThresholdMs (default 50 ms, one game tick) is recorded with its duration, chunk coordinates, dimension, and the first non-vanilla class on the call stack:

``
[BetterAutoSave] main-thread sync chunk load: 5188ms at (120,-340) in minecraft:overworld, called from com.example.protection.RegionScanner
`

Each distinct source prints one line the first time it appears; after that it only accumulates into the table, so a player exploring new terrain cannot flood the log

The instrumented call sits on the branch taken after vanilla's four-slot chunk cache misses: when the requested chunk is already cached, execution never reaches it. On a miss it costs two extra
System.nanoTime()` calls (tens of nanoseconds), and a stack is captured only once the wait actually exceeds the threshold — during normal operation none is ever captured

### 3. Inter-tick pauses

Between the end of one tick and the start of the next, the server waits for the next tick while draining its task queue. That time is not part of MSPT. If something in the queue stalls — a task submitted by another mod, a chunk system callback, a command — MSPT can stay perfectly healthy while players have been frozen for ten seconds or more

The default mode takes one timestamp at the start and one a
...
## v0.19.0 — Hardening player data and level.dat, plus the autosave main-thread spike

0.19.0 closes three vanilla paths that lose data silently, builds a full read/write verification loop around level.dat, and adds two optional performance optimizations. The data-safety fixes are on by default; the performance optimizations are off by default

(0.18.0 was never published; its content is folded into this release)

### 1. Three paths that lose data silently

All three are vanilla behavior. None of them raises an error, interrupts anything, or is necessarily noticed by the player right away

  • A failed player-data read is treated as a brand-new player.** When reading playerdata/<uuid>.dat throws, vanilla logs one line and proceeds as if the player had never joined — inventory, position and experience all reset — while <uuid>.dat_old sits untouched in the same directory. Power loss mid-write, a bad sector, or an external tool truncating the file all land here

playerData.loadFallback (on by default) changes this to: quarantine the damaged primary as <uuid>_corrupted_<timestamp>.dat to preserve the evidence, then try .dat_old, and on a successful read run it through the data fixer and return it. Only when neither file is readable does it fall back to vanilla behavior. A missing primary file — an actual new player — behaves exactly as before

  • Advancement and stats files are truncating writes.** Vanilla writes advancements/<uuid>.json and stats/<uuid>.json straight over the live file, with no temp file and no backup. Lose power halfway through and what remains is a length-truncated JSON — the next read fails to parse, and that player's advancements and statistics are gone

playerData.atomicSidecarWrite (on by default) switches to temp file + atomic rename, keeping the previous copy as .bak immediately before the rename. It falls back automatically on filesystems that do not support atomic rename

Closing the truncation window is the job of the temp file and the atomic replace themselves. The additional fsync sits behind its own switch, playerData.sidecarFsync, which is **off by default**: PlayerList.save runs on the server thread and writes both files for every online player on every autosave, so turning it on pins two synchronous device flushes per player onto a single tick — 120 of them at 60 players. Vanilla performs no fsync anywhere on this path, not even for playerdata/<uuid>.dat, and ext4 in its default data=ordered mode already flushes the new data before committing a rename over an existing file. Turn it on only if the host has no battery-backed write cache and unclean power loss is a real concern, and pair it with the stagger setting below

  • A crash shutdown skips the save teardown.** BetterAutoSave's four shutdown guards were all hung off ServerStoppingEvent. That event is never fired when the server exits abnormally, and there is a second case where another mod throws inside it and breaks the whole event
...
## v0.19.0 — Hardening player data and level.dat, plus the autosave main-thread spike

0.19.0 closes three vanilla paths that lose data silently, builds a full read/write verification loop around level.dat, and adds two optional performance optimizations. The data-safety fixes are on by default; the performance optimizations are off by default

(0.18.0 was never published; its content is folded into this release)

### 1. Three paths that lose data silently

All three are vanilla behavior. None of them raises an error, interrupts anything, or is necessarily noticed by the player right away

  • A failed player-data read is treated as a brand-new player.** When reading playerdata/<uuid>.dat throws, vanilla logs one line and proceeds as if the player had never joined — inventory, position and experience all reset — while <uuid>.dat_old sits untouched in the same directory. Power loss mid-write, a bad sector, or an external tool truncating the file all land here

playerData.loadFallback (on by default) changes this to: quarantine the damaged primary as <uuid>_corrupted_<timestamp>.dat to preserve the evidence, then try .dat_old, and on a successful read run it through the data fixer and return it. Only when neither file is readable does it fall back to vanilla behavior. A missing primary file — an actual new player — behaves exactly as before

  • Advancement and stats files are truncating writes.** Vanilla writes advancements/<uuid>.json and stats/<uuid>.json straight over the live file, with no temp file and no backup. Lose power halfway through and what remains is a length-truncated JSON — the next read fails to parse, and that player's advancements and statistics are gone

playerData.atomicSidecarWrite (on by default) switches to temp file + atomic rename, keeping the previous copy as .bak immediately before the rename. It falls back automatically on filesystems that do not support atomic rename

Closing the truncation window is the job of the temp file and the atomic replace themselves. The additional fsync sits behind its own switch, playerData.sidecarFsync, which is **off by default**: PlayerList.save runs on the server thread and writes both files for every online player on every autosave, so turning it on pins two synchronous device flushes per player onto a single tick — 120 of them at 60 players. Vanilla performs no fsync anywhere on this path, not even for playerdata/<uuid>.dat, and ext4 in its default data=ordered mode already flushes the new data before committing a rename over an existing file. Turn it on only if the host has no battery-backed write cache and unclean power loss is a real concern, and pair it with the stagger setting below

  • A crash shutdown skips the save teardown.** BetterAutoSave's four shutdown guards were all hung off ServerStoppingEvent. That event is never fired when the server exits abnormally, and there is a second case where another mod throws inside it and breaks the whole event
...
# 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 swi
...
# 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 swi
...
# v0.16.3 — Fix NeoForge users getting the Forge jar on Modrinth + rollup of fixes since 0.16.2

0.16.3 primarily fixes a distribution problem: on Modrinth, NeoForge 1.21.1 users who hit the default download got the Forge 1.20.1 jar, which FML then rejects with "requires forge / 1.20.1" — surfacing to users as "incompatible with neoforge". The mod itself was never at fault — the 0.16.2 NeoForge jar loads and runs correctly on real NeoForge 21.1.233 and 21.1.235 servers. This build also rolls up a batch of data-safety and config fixes accumulated since 0.16.2

## Distribution fix (main reason for this release)

Every release previously bundled both the Forge and NeoForge jars into a single Modrinth version, tagged with the union of both loaders (forge, neoforge) and both game versions (1.20.1, 1.21.1). Modrinth applies those tags to the whole version, and the default download is the version's first file (Forge's self-contained -all.jar) — so this one version showed up under "NeoForge 1.21.1", "Forge 1.20.1", and even the non-existent "Forge 1.21.1" / "NeoForge 1.20.1" combinations, and a NeoForge user hitting the default download always got the Forge jar

This build changes the release pipeline to publish one Modrinth version per loader (+forge / +neoforge), each carrying only its own jar as the default download, each appearing only under the correct loader and game version. The GitHub Release side already ships the two jars under distinct names and is unaffected

## Data safety and correctness

  • The in-flight dedup key for same-named SavedData across dimensions now uses the file path, fixing a case where identically named data in different dimensions could be treated as one
  • Online single-chunk restore now performs a real transactional rollback on install; the async lighting/resend tail of a restore gained exceptionallyAsync so failures are no longer swallowed silently
  • Fixed a case where a task already past the gate could be silently dropped in the window around a save-degradation flip
  • Async loading (opt-in, Forge only) now handles replay failure and worker parse failure separately, and its misleading log lines are corrected
  • Async-loading mixins now apply gated on load.enabled, with zero bytecode involvement when the feature is off
  • The SavedData large-file guard now keys off the uncompressed in-memory footprint, matching real spikes more closely

## Config changes

  • Removed the ineffective entityChunksPerTickBase knob (it had no effect)
  • Raised the deadlineGuardSeconds lower bound to 5, so setting it to 0 can no longer disable the save deadline backstop

## Performance and compatibility

  • The NeoForge SavedData path adopts serialize-once, removing the main-thread tag.copy deep-copy spike
  • On startup, detecting another async-save mod that also takes over ChunkMap.save (such as fastasyncworldsave) now logs a WARN advising you to run only one

## Upgrading

  • Drop-in jar replacement
  • NeoForge 1.21.1 users: Modrinth n
...
# v0.16.3 — Fix NeoForge users getting the Forge jar on Modrinth + rollup of fixes since 0.16.2

0.16.3 primarily fixes a distribution problem: on Modrinth, NeoForge 1.21.1 users who hit the default download got the Forge 1.20.1 jar, which FML then rejects with "requires forge / 1.20.1" — surfacing to users as "incompatible with neoforge". The mod itself was never at fault — the 0.16.2 NeoForge jar loads and runs correctly on real NeoForge 21.1.233 and 21.1.235 servers. This build also rolls up a batch of data-safety and config fixes accumulated since 0.16.2

## Distribution fix (main reason for this release)

Every release previously bundled both the Forge and NeoForge jars into a single Modrinth version, tagged with the union of both loaders (forge, neoforge) and both game versions (1.20.1, 1.21.1). Modrinth applies those tags to the whole version, and the default download is the version's first file (Forge's self-contained -all.jar) — so this one version showed up under "NeoForge 1.21.1", "Forge 1.20.1", and even the non-existent "Forge 1.21.1" / "NeoForge 1.20.1" combinations, and a NeoForge user hitting the default download always got the Forge jar

This build changes the release pipeline to publish one Modrinth version per loader (+forge / +neoforge), each carrying only its own jar as the default download, each appearing only under the correct loader and game version. The GitHub Release side already ships the two jars under distinct names and is unaffected

## Data safety and correctness

  • The in-flight dedup key for same-named SavedData across dimensions now uses the file path, fixing a case where identically named data in different dimensions could be treated as one
  • Online single-chunk restore now performs a real transactional rollback on install; the async lighting/resend tail of a restore gained exceptionallyAsync so failures are no longer swallowed silently
  • Fixed a case where a task already past the gate could be silently dropped in the window around a save-degradation flip
  • Async loading (opt-in, Forge only) now handles replay failure and worker parse failure separately, and its misleading log lines are corrected
  • Async-loading mixins now apply gated on load.enabled, with zero bytecode involvement when the feature is off
  • The SavedData large-file guard now keys off the uncompressed in-memory footprint, matching real spikes more closely

## Config changes

  • Removed the ineffective entityChunksPerTickBase knob (it had no effect)
  • Raised the deadlineGuardSeconds lower bound to 5, so setting it to 0 can no longer disable the save deadline backstop

## Performance and compatibility

  • The NeoForge SavedData path adopts serialize-once, removing the main-thread tag.copy deep-copy spike
  • On startup, detecting another async-save mod that also takes over ChunkMap.save (such as fastasyncworldsave) now logs a WARN advising you to run only one

## Upgrading

  • Drop-in jar replacement
  • NeoForge 1.21.1 users: Modrinth n
...
[0.16.0] BetterAutoSave v0.16.0 by Shinoyuki_Miyako on Jun 29, 2026
Download
# v0.16.0 — Async chunk load (stable)

0.16.0 brings the async chunk load introduced in v0.15 to a stable release: load-side POI reads moved off-thread, a thread-safety symmetry fix for the capability subsystem, and hardened shutdown paths. Async load is still off by default and opt-in (load.enabled), aimed at server owners chasing load performance who are willing to opt in. Forge 1.20.1 build only

## What this version brings

Async load (PARTIAL mode) moves chunk deserialization from the main thread to a background load worker. The 0.16 cycle did three things around this path:

  • Tier A async POI prefetch**: the load worker reads that column's POI region bytes off-thread during deserialization, the main thread populates the cache before replay, eliminating the synchronous tryRead().join() disk wait that used to sit on the main thread. Real production server (137 mods, 8 load workers, fast flight): main-thread POI getOrLoad dropped from 15.0% to 0.1%, load-side main-thread cost down ~13.7%
  • Capability subsystem thread-safety symmetry fix**: during chunk load, LevelChunk construction dispatches the global AttachCapabilitiesEvent and readCapsFromNBT triggers third-party capability deserialization — both are the same kind of main-thread-assuming work as the already-deferred ChunkDataEvent.Load. This build defers them to the main-thread replay as well, fixing a potential thread-safety hazard for mods that attach persistent capabilities to chunks under async load. The replay executes faithfully, capability data is read back as usual, no data loss
  • Hardened shutdown**: load / save workers are now daemon threads with a JVM shutdown fallback hook, preventing another mod that throws on the shutdown event chain from hanging the whole server while BAS finishes its teardown; in-flight load tasks are also included in the shutdown drain barrier, symmetric with the save side

## Verification

  • Pre-release multi-angle adversarial review of the capability off-thread path + 155 unit tests (including byte-faithful replay assertions for the capability defer)
  • Test-server reobf jar end-to-end: the async load worker loads spawn chunks through the capability defer + main-thread replay, zero exceptions, zero data delta

## Safety and compatibility

  • Lossless: capability gather and deserialization are replayed faithfully on the main thread in original order, byte-for-byte equivalent to vanilla synchronous load
  • Failure fallback: a worker parse failure falls back to a main-thread vanilla read, reads disk bytes only, zero data loss
  • Users without async load enabled (the default): no behavior change in the load path

## Configuration ([load] section)

  • load.enabled (default false): master switch for async chunk load, opt-in
  • load.asyncPoiPrefetch (default true): with async load on and PARTIAL mode, prefetch the POI region on the load worker; set false to fall back to a main-thread synchronous read if a POI-storage mod misbehaves

## Upgra
...
# v0.16.0-beta.1 — Async POI prefetch (Tier A, load-side performance)

This build extends v0.15's async load by moving the POI region disk read off the main thread onto the background load worker. With async load on, this POI read wait was the single largest remaining main-thread cost; this build effectively eliminates it. The feature kicks in automatically with async load, Forge 1.20.1 build only. Async load itself is still off by default and opt-in

## What it does

  • Async load (PARTIAL mode) moves chunk deserialization to a background worker, but PoiManager.checkConsistencyWithBlocks (the POI consistency check) still replays on the main thread, and the POI region read it triggers blocks the main thread synchronously on tryRead().join() waiting for disk
  • spark profiling: under fast movement / high view-distance, this POI read wait is the top remaining main-thread cost once async load is on
  • This build has the load worker read that column's POI region bytes off-thread during deserialization; the main thread populates the POI cache from those prefetched bytes before replay, so the consistency check hits the cache instead of reading disk

## Measured impact

  • Real production server (137-mod heavyweight pack, 8 load workers, fast flight): main-thread POI getOrLoad dropped from **15.0% to 0.1%**, the synchronous read wait gone, main-thread chunk-load cost down ~**13.7%**
  • TPS at cap, zero parse fallbacks, zero POI data delta

## Configuration ([load] section)

  • load.asyncPoiPrefetch (default true, hot-reloadable): whether to prefetch the POI region on the load worker. Only takes effect when async load is on (load.enabled=true) and mode is PARTIAL. Set false to fall back to a main-thread synchronous POI read (vanilla behavior) if a POI-storage mod misbehaves

## Safety and compatibility

  • Thread safety: the worker only touches the underlying IOWorker (thread-safe), never the non-concurrent POI cache; the POI cache parse and writes all stay on the main-thread replay, in line with every other POI access
  • No data loss: the prefetched bytes are that column's already-saved POI data, and the main-thread parse-and-populate is byte-for-byte equivalent to vanilla readColumn; if the column was already loaded before replay, the prefetched bytes are skipped in favor of the live data
  • Failure fallback: a failed POI prefetch does not affect chunk loading — it falls back to a main-thread vanilla POI read
  • The online chunk-restore path does not prefetch (it restores an already-loaded chunk whose POI is usually in memory, so a prefetch would be skipped by the guard and wasted)

## Why it is still a beta

  • Tier A is a load-side enhancement to async load, which itself is still in public testing and off by default; this build is marked pre-release alongside it
  • Main-thread cost elimination and zero-data-loss signals are real-server verified, but sustained concurrent multiplayer steady state is still under observation

## Upgrading

  • Drop-in ja
...
# v0.15.0-beta.1 — Async chunk load (working build, off by default)

0.15.0-beta.1 is the first async-chunk-load prerelease that runs on a real production server. The previous public prerelease v0.13.0-beta.1 shipped a jar that omitted the MixinExtras dependency, so enabling async loading on a real server crashed it on a missing class; this build fixes the packaging, ships a self-contained jar, and rewrites the async-load implementation to be non-blocking. The feature is still off by default and opt-in, Forge 1.20.1 build only. Back up your entire world folder before enabling it

### Key fix: MixinExtras packaging

  • Async load's instruction injection is built on MixinExtras @WrapOperation. The v0.13.0-beta.1 jar did not bundle that dependency, so a real server threw ClassNotFoundException when loading the async-load mixins (the dev environment ships the dependency, so this was only ever validated in dev and the problem stayed hidden)
  • This build ships a self-contained jar (MixinExtras embedded) — install and go, no manual dependency to add. This is the first async-load build that can actually be validated in production

### What it does

  • Vanilla deserializes a loading chunk's on-disk bytes into game objects (ChunkSerializer.read) on the main thread; with a high view-distance and players moving or teleporting fast, this becomes a main-thread burden
  • When enabled, the pure deserialization runs on a background load worker; the main thread keeps only what must run there: POI consistency, light sections, and ChunkDataEvent.Load replay
  • The real payoff is on production servers (view-distance 10-12 + many players): the freed main-thread time becomes TPS headroom, smoother loading, room for more players
  • It is not a flight-speed silver bullet: single-player flying at max view-distance is bound by vanilla's single-threaded chunk pipeline, which off-thread deserialization cannot help — that part cannot be parallelized without breaking mod compatibility

### What it does not touch

  • This optimizes the disk serialization boundary of **existing** chunks (read from disk, written to disk), not **worldgen**. Generating never-before-seen terrain is untouched: brand-new chunks never pass through ChunkSerializer.read, so async load only kicks in for chunks that were previously generated and saved

### Changes since v0.13.0-beta.1

  • Non-blocking future chain: async load's internals were rewritten from "@WrapOperation + join block-and-wait" into a non-blocking future chain, so the main thread no longer stalls waiting on a background parse
  • In-flight throttle (LoadInFlightLimiter): caps deserializations in flight so a batch finishing at once cannot land all its main-thread replay in a single tick; maxInFlight default 32 → 128, and hot-adjustable
  • Fine-grained Codec lock: the parse path's concurrency guard is narrowed to the Codec dispatch cache alone, cutting contention between workers
  • Now includes the v0.12.1 SavedData deep-copy spike f
...
Shinoyuki-BetterAutoSave Icon
Download Shinoyuki-BetterAutoSave
Select game version
Select platform