Optimize: move ALL DB writes off main thread + increase auto-save to 2min

Spark showed 5.66% server thread from auto-save. Breakdown:
- store() DB write: 1.39% (already moved to background)
- StoreCurios DB write: 0.56% (was on main thread)
- storeAccessories DB write: 0.55% (was on main thread)
- storeCosmeticArmor DB write: 0.56% (was on main thread)
- storeNeoForgeAttachments DB write: 0.58% (was on main thread)
- storeSophisticatedStorage: 0.69% (was on main thread)
- storeSophisticatedBackpacks: 0.59% (was on main thread)

Changes:
1. Curios snapshot: new snapshotCuriosData() reads entity state on
   main thread (fast), returns serialized string. DB write in background.
2. ALL mod saves moved to background thread lambda:
   - ModCompatSync.storeAll (Accessories, CosmeticArmor, Attachments)
   - Sophisticated Backpacks/Storage/RS2
3. Auto-save interval doubled: 1200 -> 2400 ticks (1min -> 2min)
4. Main thread now only does: entity snapshot (~0.3ms) + curios snapshot

Expected: ~80% reduction in main thread usage (5.66% -> ~1%)

Vyrriox
This commit is contained in:
laforetbrut 2026-03-26 22:17:25 +01:00
parent 7613f4ecfb
commit 04a1f0128e
2 changed files with 52 additions and 16 deletions

View File

@ -1037,11 +1037,9 @@ public class VanillaSync {
}
}
// Mod data snapshots - also on main thread (reads entity state safely)
// Sophisticated Backpacks/Storage/RS2 are saved via their own store methods
if (ModList.get().isLoaded("sophisticatedbackpacks")) ModsSupport.storeSophisticatedBackpacks(player);
if (ModList.get().isLoaded("sophisticatedstorage")) ModsSupport.storeSophisticatedStorageItems(player);
if (ModList.get().isLoaded("refinedstorage")) ModsSupport.storeRefinedStorageDisks(player);
// NOTE: Sophisticated Backpacks/Storage/RS2 saves are NOT done here anymore.
// They are done in the background thread (their entity reads are on SavedData which is thread-safe,
// and their DB writes should not block the main thread).
return new PlayerDataSnapshot(
uuid, XP, score, foodLevel, health,
@ -1099,7 +1097,7 @@ public class VanillaSync {
private static int heartbeatTickCounter = 0;
private static final int HEARTBEAT_INTERVAL_TICKS = 600; // Every 30 seconds (20 tps * 30s)
private static int autoSaveTickCounter = 0;
private static final int AUTO_SAVE_INTERVAL_TICKS = 1200; // Every minute
private static final int AUTO_SAVE_INTERVAL_TICKS = 2400; // Every 2 minutes (was 1min, doubled to reduce main thread load)
private static int autoCleanCuriosCacheTickCounter = 0;
private static final int AUTO_CLEAN_CURIOS_CACHE_INTERVAL_TICKS = 36000; // Every 30 min
@ -1137,24 +1135,40 @@ public class VanillaSync {
ReentrantLock lock = getPlayerLock(puuid);
if (!lock.tryLock()) continue;
try {
// === MAIN THREAD: Snapshot entity data + mod data (reads are fast) ===
// === MAIN THREAD: Snapshot ALL data (entity reads only, no DB I/O) ===
final PlayerDataSnapshot snapshot = snapshotPlayerData(player);
// Curios/Accessories/CosmeticArmor/Attachments have their own DB writes internally,
// but they READ entity state here on the main thread (safe)
// Snapshot Curios data on main thread (entity read), DB write deferred
final String curiosSnapshot;
if (ModList.get().isLoaded("curios") && !player.isDeadOrDying()) {
new ModsSupport().StoreCurios(player, false);
}
if (!player.isDeadOrDying()) {
ModCompatSync.storeAll(player);
curiosSnapshot = ModsSupport.snapshotCuriosData(player);
} else {
curiosSnapshot = null;
}
// === BACKGROUND THREAD: Write main snapshot to DB (slow, off main thread) ===
// Use tryLock in the background task to skip if logout already saved newer data
// === BACKGROUND THREAD: ALL DB writes in one batch ===
executorService.submit(() -> {
ReentrantLock bgLock = getPlayerLock(puuid);
if (!bgLock.tryLock()) return; // logout won the race, skip stale snapshot
if (!bgLock.tryLock()) return;
try {
writeSnapshotToDB(snapshot);
// Write curios data
if (curiosSnapshot != null) {
JDBCsetUp.executePreparedUpdate(
"REPLACE INTO curios (uuid, curios_item) VALUES (?, ?)",
puuid, curiosSnapshot);
}
// Mod compat + storage saves (all DB writes, off main thread)
ModCompatSync.storeAll(player);
if (ModList.get().isLoaded("sophisticatedbackpacks")) {
ModsSupport.storeSophisticatedBackpacks(player);
}
if (ModList.get().isLoaded("sophisticatedstorage")) {
ModsSupport.storeSophisticatedStorageItems(player);
}
if (ModList.get().isLoaded("refinedstorage")) {
ModsSupport.storeRefinedStorageDisks(player);
}
} catch (Exception e) {
PlayerSync.LOGGER.error("Error auto-saving player {}", puuid, e);
} finally {

View File

@ -234,6 +234,28 @@ public class ModsSupport {
}
}
/**
* Snapshots Curios data into a serialized string on the main thread (no DB write).
* Returns the serialized data string, or null if no curios data.
*/
public static String snapshotCuriosData(Player player) {
if (!ModList.get().isLoaded("curios")) return null;
Optional<ICuriosItemHandler> handlerOpt = CuriosApi.getCuriosInventory(player);
Map<String, String> flatMap = new HashMap<>();
handlerOpt.ifPresent(handler -> {
handler.getCurios().forEach((slotType, stacksHandler) -> {
IDynamicStackHandler dynStacks = stacksHandler.getStacks();
for (int i = 0; i < dynStacks.getSlots(); i++) {
ItemStack stack = dynStacks.getStackInSlot(i);
if (!stack.isEmpty()) {
flatMap.put(slotType + ":" + i, VanillaSync.getNbtForStorage(stack));
}
}
});
});
return flatMap.toString();
}
public void StoreCurios(Player player, boolean init) throws SQLException {
if (!ModList.get().isLoaded("curios")) return;