Phase 16: RS2 save — encode only the player's disks (no more world-wide sd.save())

User report: 'Je veut juste que ça prenne en compte les disks que le joueur à
dans l inventaire' — confirming the rs2=1000ms+ observed in [perf-logout]
breakdowns. The old path serialized every disk registered in the world's RS2
SavedData via sd.save() then searched the resulting blob for the player's
UUIDs. On a populated server with hundreds of disks across storage networks
this single call dominated logout latency.

New path (Phase 16):
  - Call repo.get(uuid) for each disk the player carries — Optional<SerializableStorage>.
  - Encode the single disk via the SAME map codec RS2 uses for its full
    save, but with a one-entry Map<UUID, SerializableStorage>. Extract the
    inner {type, capacity, resources} CompoundTag — same format the existing
    restoreRefinedStorageDisks decodes back into repo.set().
  - Complexity drops from O(world disks) to O(player disks carried).

Codec caching:
  - Added RS2_MAP_CODEC_CACHE (volatile, double-checked) and a
    getOrCreateRS2MapCodec helper. Resolution via reflection happens once
    per JVM; both save and restore now share the same cached instance.

Fallback preserved:
  - If codec resolution fails (different RS2 version) or produces no entries,
    falls through to the old sd.save() path. No regression for existing
    deployments that worked before.

Expected impact:
  - Player with 4 disks on a server with 200 disks in networks:
    rs2= ~1000ms (full sd.save)  ->  rs2= ~60-100ms (4 repo.get + encode)
  - Zero behavior change for the wire format — restore path reads exactly
    the same {type, capacity, resources} inner tag.

Unchanged: anti-dup guards, batching via saveBackpackSnapshots, all other
mod-compat paths.
This commit is contained in:
laforetbrut 2026-04-22 10:04:46 +02:00
parent ea54596d8c
commit 3a908ae131

View File

@ -1146,21 +1146,104 @@ public class ModsSupport {
* Saves RS2 disk storage contents by UUID using a pre-captured ServerLevel reference.
* Can be called from a background thread (SavedData read + DB write, no entity access).
*/
/**
* PHASE 16: cached RS2 codec. Resolution via reflection is expensive enough to be
* visible in Spark profiles when repeated per-save; we only need the codec instance
* once per JVM life. Volatile + double-checked idiom.
*/
@SuppressWarnings("rawtypes")
private static volatile com.mojang.serialization.Codec RS2_MAP_CODEC_CACHE;
@SuppressWarnings({"rawtypes", "unchecked"})
private static com.mojang.serialization.Codec getOrCreateRS2MapCodec(Object repo) {
com.mojang.serialization.Codec c = RS2_MAP_CODEC_CACHE;
if (c != null) return c;
synchronized (ModsSupport.class) {
c = RS2_MAP_CODEC_CACHE;
if (c != null) return c;
try {
java.lang.reflect.Method m = repo.getClass().getDeclaredMethod("createCodec", Runnable.class);
m.setAccessible(true);
c = (com.mojang.serialization.Codec) m.invoke(null, (Runnable) () -> {});
RS2_MAP_CODEC_CACHE = c;
} catch (Throwable t) {
PlayerSync.LOGGER.error("[rs2] cannot resolve map codec — save/restore will fallback", t);
}
return c;
}
}
/**
* PHASE 16: save ONLY the disks the player actually carries in their inventory,
* never the full RS2 SavedData.
*
* <p>Previous implementation called {@code sd.save(new CompoundTag, registry)}
* which serializes every disk registered on the server into a single NBT blob
* then searched it for the player's UUIDs. On a populated server (hundreds of
* disks in storage networks) this single call dominated logout latency
* (rs2=1064ms observed in production).
*
* <p>New implementation uses the RS2 {@code createCodec} (same one
* {@link #restoreRefinedStorageDisks} uses for decode) to ENCODE one disk at a
* time only the UUIDs the player has in their inventory. Cost is O(player
* disk count) instead of O(world disk count).
*
* <p>If codec resolution fails (older RS2 version, refactor), falls back to
* the old full-save path so a player with a disk still gets their data synced.
*/
public static void saveRS2DisksByLevel(List<UUID> diskUuids, net.minecraft.server.level.ServerLevel level,
net.minecraft.core.HolderLookup.Provider registryAccess) {
if (diskUuids.isEmpty()) return;
try {
com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo =
com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(level);
if (!(repo instanceof net.minecraft.world.level.saveddata.SavedData sd)) return;
if (repo == null) return;
net.minecraft.nbt.CompoundTag fullNbt = sd.save(new net.minecraft.nbt.CompoundTag(), registryAccess);
// PHASE 13 PERF: collect all disk NBTs into a single Map and delegate to the
// batched writer. Previous behavior made N sequential REPLACE INTO calls
// (observed as rs2=500ms+ in [perf-logout] breakdowns). One batched transaction
// now dominates by ~10× for players with multiple disks.
Map<UUID, CompoundTag> toSave = new HashMap<>();
@SuppressWarnings("rawtypes")
com.mojang.serialization.Codec mapCodec = getOrCreateRS2MapCodec(repo);
if (mapCodec != null) {
var ops = registryAccess.createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE);
for (UUID uuid : diskUuids) {
try {
Optional<com.refinedmods.refinedstorage.common.api.storage.SerializableStorage> diskOpt = repo.get(uuid);
if (diskOpt.isEmpty()) continue; // disk exists in inventory but empty in repo
// Build a single-entry map {uuid -> disk} and encode via the same map codec
// RS2 uses for its full save. The output CompoundTag is
// {"uuid-string": {type, capacity, resources}}
// We store ONLY the inner {type, capacity, resources}, matching what
// restoreRefinedStorageDisks expects via restoreStorageContents.
Map<UUID, com.refinedmods.refinedstorage.common.api.storage.SerializableStorage> singleMap =
java.util.Collections.singletonMap(uuid, diskOpt.get());
@SuppressWarnings("unchecked")
com.mojang.serialization.DataResult<net.minecraft.nbt.Tag> enc =
mapCodec.encodeStart(ops, singleMap);
Optional<net.minecraft.nbt.Tag> tagOpt = enc.result();
if (tagOpt.isEmpty()) {
PlayerSync.LOGGER.warn("[rs2-save] codec encode returned empty for disk {}", uuid);
continue;
}
if (!(tagOpt.get() instanceof CompoundTag wrapped)) continue;
CompoundTag inner = wrapped.getCompound(uuid.toString());
if (inner != null && !inner.isEmpty()) {
toSave.put(uuid, inner);
}
} catch (Throwable t) {
PlayerSync.LOGGER.warn("[rs2-save] encode failed for disk {} ({}) — skipping", uuid, t.getMessage());
}
}
if (!toSave.isEmpty()) {
saveBackpackSnapshots(toSave);
PlayerSync.LOGGER.info("Saved {} RS2 disk(s) via direct codec (player-scoped)", toSave.size());
return;
}
}
// Fallback: legacy sd.save() if codec path fails or produced nothing.
if (!(repo instanceof net.minecraft.world.level.saveddata.SavedData sd)) return;
PlayerSync.LOGGER.debug("[rs2-save] codec path empty, falling back to sd.save() for {} disk(s)", diskUuids.size());
net.minecraft.nbt.CompoundTag fullNbt = sd.save(new net.minecraft.nbt.CompoundTag(), registryAccess);
for (UUID uuid : diskUuids) {
net.minecraft.nbt.CompoundTag entryNbt = findRS2EntryInNbt(fullNbt, uuid.toString());
if (entryNbt != null && !entryNbt.isEmpty()) {
@ -1168,8 +1251,8 @@ public class ModsSupport {
}
}
if (!toSave.isEmpty()) {
saveBackpackSnapshots(toSave); // shared batched writer (backpack_data table)
PlayerSync.LOGGER.info("Saved {} RS2 disk(s) in one batch", toSave.size());
saveBackpackSnapshots(toSave);
PlayerSync.LOGGER.info("Saved {} RS2 disk(s) via legacy full-save fallback", toSave.size());
}
} catch (Exception e) {
PlayerSync.LOGGER.error("Error saving RS2 disks by level", e);
@ -1210,16 +1293,12 @@ public class ModsSupport {
com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo =
com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel());
// Get the map codec via reflection (same codec used for save)
// PHASE 16: use the shared codec cache (same one saveRS2DisksByLevel uses).
// Saves reflection cost on every player join.
@SuppressWarnings("rawtypes")
com.mojang.serialization.Codec mapCodec;
try {
java.lang.reflect.Method getMapCodecMethod =
repo.getClass().getDeclaredMethod("createCodec", Runnable.class);
getMapCodecMethod.setAccessible(true);
mapCodec = (com.mojang.serialization.Codec) getMapCodecMethod.invoke(null, (Runnable) () -> {});
} catch (Exception e) {
PlayerSync.LOGGER.error("Cannot get RS2 map codec, disk restore will fail", e);
com.mojang.serialization.Codec mapCodec = getOrCreateRS2MapCodec(repo);
if (mapCodec == null) {
PlayerSync.LOGGER.error("Cannot get RS2 map codec, disk restore will fail");
return;
}