Parse regions on worker threads, and warm the cache before anyone looks - #165
Conversation
Closes #160 and #161. They belong together: warming a world on the main thread would be the freeze it exists to avoid, at a larger scale. **Off the main thread.** A region is about 1.4 s after #157 and it was being spent on the thread that answers every IPC call, serves the web panel and reads the console. Slicing made that interruptible, not absent — four cold regions still measured six seconds, three quarters of a second of which was the politeness gap between them, and that gap exists only because the parse was in the way. A pool of half the cores, capped at four, parses several at once and blocks nothing. `regionParse.ts` is the parse with no Electron in it, split out of worldTiles so a worker can import it. The worker hands back the ENCODED, gzipped region — which is byte for byte what the disk cache stores, so a result costs one decode and nothing rebuilds 1024 objects across a thread boundary. **The colour table is the whole risk.** `blockColour` reads module-level state that clientAssets fills at runtime from the operator's client jar, and a worker starts with it EMPTY. A worker that parsed without it would render the fallback palette and the caller would write those colours into the cache, where they would outlive the process and keep serving a wrong map with a cache version that still matched. So: an epoch on the table, the pool sends it whenever a thread's copy is stale, and the worker REFUSES to parse if it was never given one. Both halves are proved failable below. **Warming.** Nothing parsed a region until somebody looked at it, so the map was cold the first time it was opened on a world MSMS had not seen — 1.4 s a region against 13 ms from the cache, over four to nine regions. The warmer walks the region directories outward from the origin (spawn is where a map is first pointed) and parses into the cache when nobody is waiting. It stands aside whenever the pool has interactive work, is bounded per pass and resumable, does nothing at all when the operator has turned the cache off, and remembers a finished world so a warmed one costs a directory listing. Started AFTER the colour table loads, never before, for the reason above. Verification (MSMS_SMOKE_WORLDS, by exit code): - the worker's region is compared to this thread's ENCODED byte for byte — every column of every chunk, not a sample. Measured 820 ms on a worker against 871 ms on-thread for the same 1024 chunks. - the colour table is set to an unmistakable value before the worker runs, and the tile has to carry it. Never sending the table fails with "the worker refused the region: stale-colours"; removing the worker's guard so it parses anyway fails with "the worker did not use the colour table it was given". - warming with the cache off must do nothing, with it on must parse, and a second pass must find everything current and parse nothing. - the sliced on-thread parse is still asserted, with workers switched off for that one gate — with them on the main thread never blocks, and the gate would have been measuring nothing, the way its fixture used to (#157). `MSMS_NO_TILE_WORKERS` is the same switch for a machine that needs it. Gates green: MSMS_SMOKE, _WORLDS, _WEB, _MODUPDATE, _ANALYSIS, _AUDIT, _ALERTS.
The warmer writes NEAREST TO SPAWN FIRST and sweepCache evicts OLDEST FIRST, which are opposite orders. On a world bigger than the cache limit the warmer would therefore have spent every pass deleting the area around spawn — the part a map is actually pointed at — and keeping whichever far corner it happened to write last. Warming would have made the map worse, and only on the big worlds it exists for. It now stops when the cache is at its limit and says so once. An interactive parse may still evict; that one is answering somebody. A limit of zero is no room rather than no limit — the first version read it as "unlimited" and warmed straight past it, which the new gate caught. Proved failable: removing the guard fails with "warming ran past a full cache: 1".
Self reviewOne real defect, and it is the kind that only shows up on the worlds this feature exists for. The warmer would have eaten the area around spawnTwo policies pointing in opposite directions:
So on any world larger than A full cache now stops the warmer and says so once, naming the setting to raise. An interactive parse may still evict — that one is answering somebody. Writing the test then found a second bug in the fix: I had guarded with Proved failable: removing the guard fails with the same message. Checked and deliberate
Disclosed
Gates green by exit code: |
Closes #160 and #161. They belong together: warming a world on the main thread would be the freeze it exists to avoid, at a larger scale.
Off the main thread (#160)
A region is ~1.4 s after #157, spent on the thread that answers every IPC call, serves the web panel and reads the console. Slicing made that interruptible, not absent — four cold regions still measured six seconds, of which three quarters of a second was the deliberate gap between them, and that gap exists only because the parse was in the way.
regionParse.tsis the parse with no Electron in it, split out ofworldTiles.tsso a worker can import it. The worker hands back the encoded, gzipped region — byte for byte what the disk cache stores — so a result costs one decode and nothing rebuilds 1024 objects across a thread boundary.Pool is half the cores, capped at four. This is a server manager; the machines cores belong to the Minecraft server it is running.
The colour table is the whole risk
blockColourreads module-level state thatclientAssets.tsfills at runtime from the operators client jar. A worker starts with it empty, so a worker that parsed without it would render the fallback palette — and the caller would write those colours into the on-disk cache, where they outlive the process and keep serving a wrong map with a cache version that still matches.Three things stop that:
setTextureColoursThe warmer is started after the colour table loads, never before.
Warming (#161)
Nothing parsed a region until somebody looked at it, so the map was cold the first time it was opened on a world MSMS had not seen: 1.4 s a region against ~13 ms from the cache, over four to nine regions.
The warmer walks the region directories outward from the origin — spawn is where a map is first pointed, and a world accumulates far corners from one player who once walked a long way. It:
cache: false— there would be nothing to warm intoVerification
MSMS_SMOKE_WORLDS, by exit code. Measured on the fixture: 820 ms on a worker against 871 ms on-thread for the same 1024 chunks.the worker refused the region: stale-coloursthe worker did not use the colour table it was givenThe workers region is compared to this threads encoded, byte for byte — every column of every chunk, not a sample. Two renderers that agree on a fixture and diverge on a real world is the failure mode here, so the comparison is over the whole region.
Warming is asserted in three states: off with the cache disabled, parses with it on, and parses nothing on a second pass because everything is current.
The sliced on-thread parse is still asserted, with workers switched off for that one gate — with them on the main thread never blocks and the gate would be measuring nothing, exactly the way its fixture used to before #157.
MSMS_NO_TILE_WORKERSis the same switch for a machine that needs it.Gates green:
MSMS_SMOKE,_WORLDS,_WEB,_MODUPDATE,_ANALYSIS,_AUDIT,_ALERTS.