From 207d90de2805fb02fbca8ebac2a9799e94b3390c Mon Sep 17 00:00:00 2001 From: itsRevela Date: Sun, 5 Apr 2026 15:56:39 -0500 Subject: [PATCH] perf: process 16 chunks/player/tick on dedicated server, revert async save Chunk loading now batches up to 16 nearest-first requests per player per tick on dedicated server (client stays at 1), improving tick recovery time after player join. Reverts the async save system -- the background thread snapshot/compress path added complexity without measurable benefit. Autosave on Windows64 server now uses the standard synchronous flush like client, in preparation for a proper async implementation from upstream. --- Minecraft.Client/PlayerChunkMap.cpp | 53 ++++--- Minecraft.Client/ServerLevel.cpp | 7 +- Minecraft.Server/Windows64/ServerMain.cpp | 2 - Minecraft.World/ConsoleSaveFileOriginal.cpp | 161 -------------------- Minecraft.World/ConsoleSaveFileOriginal.h | 5 - 5 files changed, 36 insertions(+), 192 deletions(-) diff --git a/Minecraft.Client/PlayerChunkMap.cpp b/Minecraft.Client/PlayerChunkMap.cpp index d8d73f09..72c7b74c 100644 --- a/Minecraft.Client/PlayerChunkMap.cpp +++ b/Minecraft.Client/PlayerChunkMap.cpp @@ -487,37 +487,52 @@ void PlayerChunkMap::getChunkAndRemovePlayer(int x, int z, shared_ptr player) { if( addRequests.size() ) { - // Find the nearest chunk request to the player int px = static_cast(player->x); int pz = static_cast(player->z); - int minDistSq = -1; - auto itNearest = addRequests.end(); - for (auto it = addRequests.begin(); it != addRequests.end(); it++) - { - if( it->player == player ) + for (int processed = 0; processed < CHUNKS_PER_PLAYER_PER_TICK; processed++) + { + // Find the nearest chunk request to the player + int minDistSq = -1; + auto itNearest = addRequests.end(); + for (auto it = addRequests.begin(); it != addRequests.end(); it++) { - int xm = ( it->x * 16 ) + 8; - int zm = ( it->z * 16 ) + 8; - int distSq = (xm - px) * (xm - px) + - (zm - pz) * (zm - pz); - if( ( minDistSq == -1 ) || ( distSq < minDistSq ) ) + if( it->player == player ) { - minDistSq = distSq; - itNearest = it; + int xm = ( it->x * 16 ) + 8; + int zm = ( it->z * 16 ) + 8; + int distSq = (xm - px) * (xm - px) + + (zm - pz) * (zm - pz); + if( ( minDistSq == -1 ) || ( distSq < minDistSq ) ) + { + minDistSq = distSq; + itNearest = it; + } } } - } - // If we found one at all, then do this one - if( itNearest != addRequests.end() ) - { - getChunk(itNearest->x, itNearest->z, true)->add(itNearest->player); - addRequests.erase(itNearest); + // If we found one, process it and continue; otherwise done + if( itNearest != addRequests.end() ) + { + getChunk(itNearest->x, itNearest->z, true)->add(itNearest->player); + addRequests.erase(itNearest); + } + else + { + break; + } } } } diff --git a/Minecraft.Client/ServerLevel.cpp b/Minecraft.Client/ServerLevel.cpp index 5bc347e7..80a12c99 100644 --- a/Minecraft.Client/ServerLevel.cpp +++ b/Minecraft.Client/ServerLevel.cpp @@ -977,11 +977,8 @@ void ServerLevel::save(bool force, ProgressListener *progressListener, bool bAut if (progressListener != nullptr) progressListener->progressStage(IDS_PROGRESS_SAVING_CHUNKS); -#if defined(_XBOX_ONE) || defined(__ORBIS__) || (defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)) - // Autosave is a minimal save. Chunks are saved continuously by the - // per-tick trickle save process (ServerChunkCache::tick), so we only - // need to flush entity data here. The full chunkSource->save() would - // redundantly re-save all dirty chunks and block the main thread. +#if defined(_XBOX_ONE) || defined(__ORBIS__) + // Our autosave is a minimal save. All the chunks are saves by the constant save process if(bAutosave) { chunkSource->saveAllEntities(); diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index b298f78d..c5ae1346 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -16,7 +16,6 @@ #include "..\Security\IdentityTokenManager.h" #include "..\WorldManager.h" #include "..\Console\ServerCli.h" -#include "..\..\Minecraft.World\ConsoleSaveFileOriginal.h" #include "Tesselator.h" #include "Windows64/4JLibs/inc/4J_Render.h" #include "Windows64/GameConfig/Minecraft.spa.h" @@ -331,7 +330,6 @@ static void TickCoreSystems() g_NetworkManager.DoWork(); ProfileManager.Tick(); StorageManager.Tick(); - ConsoleSaveFileOriginal::CommitPendingAsyncSave(); } /** diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 1a5071e1..c6b29afa 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -12,26 +12,6 @@ #include "..\Minecraft.Client\Common\GameRules\LevelGenerationOptions.h" #include "..\Minecraft.World\net.minecraft.world.level.chunk.storage.h" -#ifdef _WINDOWS64 -#include -#include -#include -extern bool g_Win64DedicatedServer; -static std::atomic s_asyncSaveInFlight{false}; - -// Pending async save: background thread fills this, main thread commits it. -struct PendingAsyncSave -{ - ConsoleSaveFile *self; - PBYTE thumbData; - DWORD thumbSize; - BYTE textMetadata[88]; - int textMetadataBytes; - bool ready; -}; -static std::mutex s_pendingSaveMutex; -static PendingAsyncSave s_pendingSave = {}; -#endif #ifdef _XBOX @@ -694,130 +674,6 @@ void ConsoleSaveFileOriginal::Flush(bool autosave, bool updateThumbnail ) unsigned int fileSize = header.GetFileSize(); -#ifdef _WINDOWS64 - // --- Dedicated server async flush path --- - // Snapshot pvSaveMem while holding the lock (fast memcpy), then release - // the lock immediately so the main thread can continue ticking. Compression - // and disk write happen on a detached background thread. - if (g_Win64DedicatedServer) - { - // If a previous async save is still compressing, fall through to the - // synchronous path to avoid queuing unbounded background work. - if (s_asyncSaveInFlight.load(std::memory_order_acquire)) - { - app.DebugPrintf("Async save: previous still in flight, falling back to sync\n"); - goto sync_flush; - } - - // Snapshot: copy the entire save buffer so we can release the lock - QueryPerformanceCounter(&qwTime); - byte *snapshot = new (std::nothrow) byte[fileSize]; - if (snapshot == nullptr) - { - app.DebugPrintf("Async save: failed to allocate %u byte snapshot, falling back to sync\n", fileSize); - goto sync_flush; - } - memcpy(snapshot, pvSaveMem, fileSize); - QueryPerformanceCounter(&qwNewTime); - qwDeltaTime.QuadPart = qwNewTime.QuadPart - qwTime.QuadPart; - fElapsedTime = fSecsPerTick * static_cast(qwDeltaTime.QuadPart); - app.DebugPrintf("Async save: snapshot %u bytes in %.3f sec\n", fileSize, fElapsedTime); - - // Gather metadata while still on the main thread - PBYTE pbThumbnailData = nullptr; - DWORD dwThumbnailDataSize = 0; - app.GetSaveThumbnail(&pbThumbnailData, &dwThumbnailDataSize); - - BYTE bTextMetadata[88]; - ZeroMemory(bTextMetadata, 88); - int64_t seed = 0; - bool hasSeed = false; - if (MinecraftServer::getInstance() != nullptr && MinecraftServer::getInstance()->levels[0] != nullptr) - { - seed = MinecraftServer::getInstance()->levels[0]->getLevelData()->getSeed(); - hasSeed = true; - } - int iTextMetadataBytes = app.CreateImageTextData(bTextMetadata, seed, hasSeed, - app.GetGameHostOption(eGameHostOption_All), Minecraft::GetInstance()->getCurrentTexturePackId()); - - INT saveOrCheckpointId = 0; - StorageManager.GetSaveUniqueNumber(&saveOrCheckpointId); - TelemetryManager->RecordLevelSaveOrCheckpoint(ProfileManager.GetPrimaryPad(), saveOrCheckpointId, fileSize); - - // Allocate compression buffer while still on the game thread and - // holding the lock (StorageManager is not thread-safe). - byte *compData = static_cast(StorageManager.AllocateSaveData(fileSize + 8)); - if (compData == nullptr) - { - app.DebugPrintf("Async save: failed to allocate compression buffer, falling back to sync\n"); - delete[] snapshot; - goto sync_flush; - } - - // Release the lock -- main thread and chunk trickle saves can resume - ReleaseSaveAccess(); - - // Pack context for the background thread - struct AsyncSaveContext - { - byte *snapshot; - byte *compData; - unsigned int fileSize; - ConsoleSaveFile *self; - PBYTE thumbData; - DWORD thumbSize; - BYTE textMetadata[88]; - int textMetadataBytes; - }; - - auto *ctx = new AsyncSaveContext(); - ctx->snapshot = snapshot; - ctx->compData = compData; - ctx->fileSize = fileSize; - ctx->self = this; - ctx->thumbData = pbThumbnailData; - ctx->thumbSize = dwThumbnailDataSize; - memcpy(ctx->textMetadata, bTextMetadata, 88); - ctx->textMetadataBytes = iTextMetadataBytes; - - s_asyncSaveInFlight.store(true, std::memory_order_release); - - std::thread([ctx]() - { - unsigned int compLength = ctx->fileSize + 8; - Compression::getCompression()->Compress( - ctx->compData + 8, &compLength, ctx->snapshot, ctx->fileSize); - - ZeroMemory(ctx->compData, 8); - int saveVer = 0; - memcpy(ctx->compData, &saveVer, sizeof(int)); - unsigned int fs = ctx->fileSize; - memcpy(ctx->compData + 4, &fs, sizeof(int)); - - app.DebugPrintf("Async save: compressed %u -> %u bytes\n", ctx->fileSize, compLength); - - // Queue for the main thread to commit via StorageManager - { - std::lock_guard lock(s_pendingSaveMutex); - s_pendingSave.self = ctx->self; - s_pendingSave.thumbData = ctx->thumbData; - s_pendingSave.thumbSize = ctx->thumbSize; - memcpy(s_pendingSave.textMetadata, ctx->textMetadata, 88); - s_pendingSave.textMetadataBytes = ctx->textMetadataBytes; - s_pendingSave.ready = true; - } - - delete[] ctx->snapshot; - delete ctx; - }).detach(); - - return; - } -sync_flush: -#endif - - // --- Original synchronous flush path (game client / non-server) --- - // Assume that the compression will make it smaller so initially attempt to allocate the current file size // We add 4 bytes to the start so that we can signal compressed data // And another 4 bytes to store the decompressed data size @@ -1276,20 +1132,3 @@ void *ConsoleSaveFileOriginal::getWritePointer(FileEntry *file) return static_cast(pvSaveMem) + file->currentFilePointer;; } -#ifdef _WINDOWS64 -void ConsoleSaveFileOriginal::CommitPendingAsyncSave() -{ - std::lock_guard lock(s_pendingSaveMutex); - if (!s_pendingSave.ready) - return; - - StorageManager.SetSaveImages( - s_pendingSave.thumbData, s_pendingSave.thumbSize, - nullptr, 0, s_pendingSave.textMetadata, s_pendingSave.textMetadataBytes); - StorageManager.SaveSaveData( - &ConsoleSaveFileOriginal::SaveSaveDataCallback, s_pendingSave.self); - - s_pendingSave.ready = false; - s_asyncSaveInFlight.store(false, std::memory_order_release); -} -#endif diff --git a/Minecraft.World/ConsoleSaveFileOriginal.h b/Minecraft.World/ConsoleSaveFileOriginal.h index 315d49ca..9c91fafc 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.h +++ b/Minecraft.World/ConsoleSaveFileOriginal.h @@ -77,11 +77,6 @@ public: virtual void LockSaveAccess(); virtual void ReleaseSaveAccess(); -#ifdef _WINDOWS64 - // Called from the main thread to commit a completed async save to StorageManager. - static void CommitPendingAsyncSave(); -#endif - virtual ESavePlatform getSavePlatform(); virtual bool isSaveEndianDifferent(); virtual void setLocalPlatform();