ModernUO/Projects/Server/Serialization/Persistence.cs
Kamron Batman 3b11a376f0
perf(saves): delete SerializedTypes.db write path and runtime type tracking
The idx v4 type table supersedes the global type queue: workers no longer
enqueue every Write(Type) into a shared ConcurrentQueue during the freeze,
WriteFiles no longer drains and dedupes millions of entries, and the db file
is no longer produced. Loading old saves still reads SerializedTypes.db via
the legacy v2/v3 path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:38:49 -07:00

193 lines
5.7 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Persistence.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
namespace Server;
public abstract class Persistence
{
private static readonly SortedSet<Persistence> _registry = new(new PersistenceComparer());
public int Priority { get; }
public Persistence(int priority = 100)
{
Priority = priority;
_registry.Add(this);
}
public bool Register() => _registry.Add(this);
public void Unregister() => _registry.Remove(this);
public static void Load(string path)
{
var typesDb = LoadTypes(path);
foreach (var entry in _registry)
{
(entry as IGenericEntityPersistence)?.DeserializeIndexes(path, typesDb);
}
// This should probably not be parallel since Mobiles must be loaded before Items
foreach (var entry in _registry)
{
entry.Deserialize(path, typesDb);
}
}
private static unsafe Dictionary<ulong, string> LoadTypes(string path)
{
var db = new Dictionary<ulong, string>();
var typesPath = Path.Combine(path, "SerializedTypes.db");
if (!File.Exists(typesPath))
{
return db;
}
using var mmf = MemoryMappedFile.CreateFromFile(typesPath, FileMode.Open);
using var accessor = mmf.CreateViewStream();
byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
var dataReader = new UnmanagedDataReader(ptr, accessor.Length);
var version = dataReader.ReadInt();
var count = dataReader.ReadInt();
for (var i = 0; i < count; ++i)
{
var hash = dataReader.ReadULong();
var typeName = dataReader.ReadStringRaw();
db[hash] = typeName;
}
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
return db;
}
// Note: This is strictly on a background thread
internal static void WriteSnapshotAll(string path)
{
foreach (var p in _registry)
{
p.WriteSnapshot(path);
}
}
internal static void SerializeAll()
{
// Largest known payloads first (LPT scheduling). An indivisible multi-megabyte system
// serialized last extends the freeze by its entire duration; serialized first it overlaps
// the entity stream. Sizes come from the previous save, or the loaded files on first save.
var ordered = new Persistence[_registry.Count];
_registry.CopyTo(ordered);
Array.Sort(ordered, static (a, b) => GetEstimatedSize(b).CompareTo(GetEstimatedSize(a)));
foreach (var p in ordered)
{
// Chunks are persistence-homogeneous: publishing the partial chunk at each
// boundary lets workers attribute buffer-chunk records to their owner without
// any per-entity state.
World.SetChunkSourceOwner(p);
p.Serialize();
}
}
private static long GetEstimatedSize(Persistence p) => (p as GenericPersistence)?.EstimatedSize ?? 0;
internal static void PostWorldSaveAll()
{
foreach (var p in _registry)
{
p.PostWorldSave();
}
}
internal static void PostDeserializeAll()
{
foreach (var p in _registry)
{
p.PostDeserialize();
}
}
// Note: This should only be run on a background thread
public abstract void WriteSnapshot(string savePath);
public abstract void Serialize();
public abstract void Deserialize(string savePath, Dictionary<ulong, string> typesDb);
public virtual void PostWorldSave()
{
}
public virtual void PostDeserialize()
{
}
internal class PersistenceComparer : IComparer<Persistence>
{
public int Compare(Persistence x, Persistence y)
{
if (x == y)
{
return 0;
}
if (x == null)
{
return 1;
}
if (y == null)
{
return -1;
}
// First sort by priority
var cmp = x.Priority.CompareTo(y.Priority);
// Then alphabetically. We won't allow the same entry (by name) twice in the SortedSet
return cmp != 0 ? cmp : x.GetHashCode().CompareTo(y.GetHashCode());
}
}
public static void TraceException(Exception ex)
{
try
{
using var op = new StreamWriter("save-errors.log", true);
op.WriteLine("# {0}", Core.Now);
op.WriteLine(ex);
op.WriteLine();
op.WriteLine();
}
catch
{
// ignored
}
Console.WriteLine(ex);
}
}