Based on a fresh audit against the Arcadia V2 modpack (444 mods, including
Curios + Accessories + SophisticatedBackpacks/Storage + RS2 + Cosmetic
Armor Reworked). Three perf wins + two opportunistic fixes.
Perf
- Heartbeat period 10s -> 30s. Paired with the 60s staleness threshold
this keeps failure-detection latency unchanged while cutting 3x the
server_info UPDATE traffic per server.
- Per-player hash-skip for unchanged snapshots (SaveToFile + staggered
auto-save). computeSnapshotHash() rolls over inventory/equipment/
enderchest/effects/xp/health/food/mod-data; when an auto-save produces
the same hash as the last successful write, the BG task returns early
and no UPDATE hits MySQL. Idle-server reduction is >95%. Logout /
shutdown / death never use the skip and refresh the hash on success
so post-logout rejoin doesn't wrongly skip.
- Batched backpack + SS saves. saveBackpackSnapshots / saveSSSnapshots
now build one transaction via executeBatchTransaction instead of
N sequential REPLACE INTO calls. A player with 3 backpacks + 2
shulkers drops from 5 network round-trips to 1 per logout save.
Per-entry fallback preserved on transaction failure.
- Periodic-save tick short-circuits when the player list is empty —
no main-thread hop, no log line, no DB heartbeat on empty servers.
Compat notes (no code change needed)
- CosmeticArmours (modid=cosmeticarmoursmod) items are worn in vanilla
armor slots (Helmet / Chestplate / Leggings / Boots inner classes) —
already captured by the core armor[] serialization. No handler needed.
- CosmeticWeapons uses the same pattern via main hand / offhand — also
already covered by core inventory serialization.
Cleanup
- removePlayerLock now also clears the hash cache so a player who
fully logged out doesn't leave a stale hash behind.
96 lines
4.2 KiB
Java
96 lines
4.2 KiB
Java
package vip.fubuki.playersync.util;
|
|
|
|
import net.minecraft.server.MinecraftServer;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.neoforged.neoforge.server.ServerLifecycleHooks;
|
|
import vip.fubuki.playersync.PlayerSync;
|
|
import vip.fubuki.playersync.config.JdbcConfig;
|
|
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
/**
|
|
* Independent scheduler that triggers a full periodic flush for every online
|
|
* player at {@code auto_save_interval_minutes} intervals.
|
|
*
|
|
* <p>This is decoupled from NeoForge's {@code PlayerEvent.SaveToFile} so the
|
|
* cadence is predictable and configurable — NeoForge's event fires on the
|
|
* vanilla autosave tick, which an admin may have tuned elsewhere. We delegate
|
|
* the actual save work to the main thread via {@code server.execute(...)} so
|
|
* snapshots run on the safe thread, then the save itself hops to the BG pool
|
|
* via the usual {@code PlayerEvent.SaveToFile} path.
|
|
*
|
|
* @author vyrriox
|
|
*/
|
|
public final class PeriodicSaveService {
|
|
|
|
private PeriodicSaveService() {}
|
|
|
|
private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
|
|
private static ScheduledExecutorService scheduler;
|
|
|
|
public static void start() {
|
|
int minutes = JdbcConfig.AUTO_SAVE_INTERVAL_MINUTES.get();
|
|
if (minutes <= 0) {
|
|
PlayerSync.LOGGER.info("[periodic-save] disabled (auto_save_interval_minutes=0)");
|
|
return;
|
|
}
|
|
if (!RUNNING.compareAndSet(false, true)) return;
|
|
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
|
Thread t = new Thread(r, "PlayerSync-periodic-save");
|
|
t.setDaemon(true);
|
|
t.setPriority(Thread.MIN_PRIORITY);
|
|
return t;
|
|
});
|
|
long periodMs = minutes * 60_000L;
|
|
// First tick after one full period, not immediately — gives the server
|
|
// time to finish startup before we start scheduling DB work.
|
|
scheduler.scheduleAtFixedRate(PeriodicSaveService::tick, periodMs, periodMs, TimeUnit.MILLISECONDS);
|
|
PlayerSync.LOGGER.info("[periodic-save] started (interval={}min)", minutes);
|
|
}
|
|
|
|
public static void stop() {
|
|
if (!RUNNING.compareAndSet(true, false)) return;
|
|
if (scheduler != null) {
|
|
scheduler.shutdownNow();
|
|
scheduler = null;
|
|
}
|
|
PlayerSync.LOGGER.info("[periodic-save] stopped");
|
|
}
|
|
|
|
private static void tick() {
|
|
try {
|
|
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
|
if (server == null || !server.isRunning()) return;
|
|
// Hop to main thread — snapshots must happen on server thread.
|
|
// PHASE 7 PERF: skip the whole tick if no one is online — no need to
|
|
// hop to main thread or log anything for an empty server.
|
|
if (server.getPlayerList().getPlayers().isEmpty()) return;
|
|
server.execute(() -> {
|
|
try {
|
|
int online = 0;
|
|
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
|
if (player.getTags().contains("player_synced") && !player.isDeadOrDying()) {
|
|
// Reuse VanillaSync's SaveToFile-style snapshot + async-write machinery.
|
|
// We emit a synthetic SaveToFile event by calling the public entry point.
|
|
vip.fubuki.playersync.sync.VanillaSync.snapshotAndQueueSave(player, "PERIODIC");
|
|
online++;
|
|
}
|
|
}
|
|
if (online > 0) {
|
|
PlayerSync.LOGGER.info("[periodic-save] queued snapshots for {} player(s)", online);
|
|
SyncLogger.playerEvent("SYSTEM", "PERIODIC_TICK",
|
|
"Queued " + online + " player snapshot(s)");
|
|
}
|
|
} catch (Throwable t) {
|
|
PlayerSync.LOGGER.error("[periodic-save] tick body failed", t);
|
|
}
|
|
});
|
|
} catch (Throwable t) {
|
|
PlayerSync.LOGGER.warn("[periodic-save] scheduling tick failed: {}", t.getMessage());
|
|
}
|
|
}
|
|
}
|