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:
parent
ea54596d8c
commit
3a908ae131
|
|
@ -1146,21 +1146,104 @@ public class ModsSupport {
|
||||||
* Saves RS2 disk storage contents by UUID using a pre-captured ServerLevel reference.
|
* 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).
|
* 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,
|
public static void saveRS2DisksByLevel(List<UUID> diskUuids, net.minecraft.server.level.ServerLevel level,
|
||||||
net.minecraft.core.HolderLookup.Provider registryAccess) {
|
net.minecraft.core.HolderLookup.Provider registryAccess) {
|
||||||
if (diskUuids.isEmpty()) return;
|
if (diskUuids.isEmpty()) return;
|
||||||
try {
|
try {
|
||||||
com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo =
|
com.refinedmods.refinedstorage.common.api.storage.StorageRepository repo =
|
||||||
com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(level);
|
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<>();
|
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) {
|
for (UUID uuid : diskUuids) {
|
||||||
net.minecraft.nbt.CompoundTag entryNbt = findRS2EntryInNbt(fullNbt, uuid.toString());
|
net.minecraft.nbt.CompoundTag entryNbt = findRS2EntryInNbt(fullNbt, uuid.toString());
|
||||||
if (entryNbt != null && !entryNbt.isEmpty()) {
|
if (entryNbt != null && !entryNbt.isEmpty()) {
|
||||||
|
|
@ -1168,8 +1251,8 @@ public class ModsSupport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!toSave.isEmpty()) {
|
if (!toSave.isEmpty()) {
|
||||||
saveBackpackSnapshots(toSave); // shared batched writer (backpack_data table)
|
saveBackpackSnapshots(toSave);
|
||||||
PlayerSync.LOGGER.info("Saved {} RS2 disk(s) in one batch", toSave.size());
|
PlayerSync.LOGGER.info("Saved {} RS2 disk(s) via legacy full-save fallback", toSave.size());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
PlayerSync.LOGGER.error("Error saving RS2 disks by level", 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.storage.StorageRepository repo =
|
||||||
com.refinedmods.refinedstorage.common.api.RefinedStorageApi.INSTANCE.getStorageRepository(sp.serverLevel());
|
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")
|
@SuppressWarnings("rawtypes")
|
||||||
com.mojang.serialization.Codec mapCodec;
|
com.mojang.serialization.Codec mapCodec = getOrCreateRS2MapCodec(repo);
|
||||||
try {
|
if (mapCodec == null) {
|
||||||
java.lang.reflect.Method getMapCodecMethod =
|
PlayerSync.LOGGER.error("Cannot get RS2 map codec, disk restore will fail");
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user