feat: Upgrades serialization v4 (Threaded Heap Serialization) (#1947)

### Summary

- `GenericEntityPersistence` is now a type of `GenericPersistence`. This allows developers to serialize both entities and non-entities in the same system. 🎉
- Each `SerializationThreadWorker` now allocates 1MB of heap for serialization _permanently_. If more memory is needed, that thread will double it's memory, not to exceed increments of 64MB.
- Several bugs with serialization introduced with the pure MMF implementation have been fixed.
- `BinaryFileReader` has been added back. 🎉
- Adds `world.useMultithreadedSaves` to allow disabling threaded saves.

> [!IMPORTANT]
> **Developer Note**
> The split file serialization has been deprecated and is no longer used. We have effectively gone back to the same file writing we had before the pure MMF implementation.
This commit is contained in:
Kamron Batman 2024-09-14 09:57:43 -07:00 committed by GitHub
parent aaac0c596c
commit 465d3c8187
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 564 additions and 373 deletions

View file

@ -51,6 +51,10 @@ public class AccountPacketTests : IClassFixture<ServerFixture>
public Serial Serial { get; } public Serial Serial { get; }
public void Deserialize(IGenericReader reader) => throw new NotImplementedException(); public void Deserialize(IGenericReader reader) => throw new NotImplementedException();
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public void Serialize(IGenericWriter writer) => throw new NotImplementedException(); public void Serialize(IGenericWriter writer) => throw new NotImplementedException();
public bool Deleted { get; } public bool Deleted { get; }

View file

@ -52,6 +52,10 @@ public abstract class BaseGuild : ISerializable
[CommandProperty(AccessLevel.GameMaster, readOnly: true)] [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
public DateTime Created { get; set; } = Core.Now; public DateTime Created { get; set; } = Core.Now;
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public abstract void Serialize(IGenericWriter writer); public abstract void Serialize(IGenericWriter writer);
public abstract void Deserialize(IGenericReader reader); public abstract void Deserialize(IGenericReader reader);

View file

@ -115,6 +115,10 @@ public class Entity : IEntity
Timer.StartTimer(Delete); Timer.StartTimer(Delete);
} }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public void Serialize(IGenericWriter writer) public void Serialize(IGenericWriter writer)
{ {
} }

View file

@ -802,6 +802,10 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
[CommandProperty(AccessLevel.Counselor)] [CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; } public Serial Serial { get; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public virtual void Serialize(IGenericWriter writer) public virtual void Serialize(IGenericWriter writer)
{ {
writer.Write(9); // version writer.Write(9); // version

View file

@ -2277,6 +2277,10 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
[CommandProperty(AccessLevel.Counselor)] [CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; } public Serial Serial { get; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public virtual void Serialize(IGenericWriter writer) public virtual void Serialize(IGenericWriter writer)
{ {
writer.Write(36); // version writer.Write(36); // version

View file

@ -55,8 +55,8 @@ public static class AdhocPersistence
{ {
var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory);
PathUtility.EnsureDirectory(Path.GetDirectoryName(fullPath)); PathUtility.EnsureDirectory(Path.GetDirectoryName(fullPath));
ConcurrentQueue<Type> types = []; HashSet<Type> typesSet = [];
var writer = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), sizeHint, types); var writer = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), sizeHint, typesSet);
serializer(writer); serializer(writer);
Task.Run( Task.Run(
@ -67,14 +67,6 @@ public static class AdhocPersistence
writer.Dispose(); writer.Dispose();
fs.Dispose(); fs.Dispose();
HashSet<Type> typesSet = [];
// Dedupe the queue.
foreach (var type in types)
{
typesSet.Add(type);
}
Persistence.WriteSerializedTypesSnapshot(Path.GetDirectoryName(fullPath), typesSet); Persistence.WriteSerializedTypesSnapshot(Path.GetDirectoryName(fullPath), typesSet);
}, },
Core.ClosingTokenSource.Token Core.ClosingTokenSource.Token

View file

@ -0,0 +1,109 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BinaryFileReader.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.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using System.Text;
namespace Server;
public sealed unsafe class BinaryFileReader : IDisposable, IGenericReader
{
private readonly bool _usePrefixes;
private readonly MemoryMappedFile _mmf;
private readonly MemoryMappedViewStream _accessor;
private readonly UnmanagedDataReader _reader;
public BinaryFileReader(string path, bool usePrefixes = true, Encoding encoding = null)
{
_usePrefixes = usePrefixes;
var fi = new FileInfo(path);
if (fi.Length > 0)
{
_mmf = MemoryMappedFile.CreateFromFile(path, FileMode.Open);
_accessor = _mmf.CreateViewStream();
byte* ptr = null;
_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
_reader = new UnmanagedDataReader(ptr, _accessor.Length, encoding: encoding);
}
else
{
_reader = new UnmanagedDataReader(null, 0, encoding: encoding);
}
}
public long Position => _reader.Position;
public void Dispose()
{
_accessor?.SafeMemoryMappedViewHandle.ReleasePointer();
_accessor?.Dispose();
_mmf?.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(bool intern = false) => _usePrefixes ? _reader.ReadString(intern) : _reader.ReadStringRaw(intern);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadStringRaw(bool intern = false) => _reader.ReadStringRaw(intern);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long ReadLong() => _reader.ReadLong();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong ReadULong() => _reader.ReadULong();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadInt() => _reader.ReadInt();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ReadUInt() => _reader.ReadUInt();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short ReadShort() => _reader.ReadShort();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort ReadUShort() => _reader.ReadUShort();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double ReadDouble() => _reader.ReadDouble();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float ReadFloat() => _reader.ReadFloat();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte ReadByte() => _reader.ReadByte();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public sbyte ReadSByte() => _reader.ReadSByte();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ReadBool() => _reader.ReadBool();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Serial ReadSerial() => _reader.ReadSerial();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type ReadType() => _reader.ReadType();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Read(Span<byte> buffer) => _reader.Read(buffer);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Seek(long offset, SeekOrigin origin) => _reader.Seek(offset, origin);
}

View file

@ -105,13 +105,7 @@ public class BufferWriter : IGenericWriter
_buffer = newBuffer; _buffer = newBuffer;
} }
public virtual void Flush() public virtual void Flush() => Resize(Math.Clamp(_buffer.Length * 2, BufferSize, _buffer.Length + 1024 * 1024 * 64));
{
// Need to avoid buffer.Length = 2, buffer * 2 is 4, but we need 8 or 16bytes, causing an exception.
// The least we need is 16bytes + Index, but we use BufferSize since it should always be big enough for a single
// non-dynamic field.
Resize(Math.Max(BufferSize, _buffer.Length * 2));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void FlushIfNeeded(int amount) private void FlushIfNeeded(int amount)

View file

@ -14,7 +14,6 @@
*************************************************************************/ *************************************************************************/
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -32,141 +31,115 @@ public interface IGenericEntityPersistence
public void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb); public void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb);
} }
public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistence where T : class, ISerializable public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPersistence where T : class, ISerializable
{ {
private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence<T>)); private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence<T>));
private static List<EntitySpan<T>>[] _entities;
private long _initialIdxSize = 1024 * 256; // Support legacy split file serialization
private long _initialBinSize = 1024 * 1024; private static Dictionary<int, List<EntitySpan<T>>> _entities;
private readonly string _name;
private readonly uint _minSerial; private readonly Serial _minSerial;
private readonly uint _maxSerial; private readonly Serial _maxSerial;
private Serial _lastEntitySerial; private Serial _lastEntitySerial;
private readonly Dictionary<Serial, T> _pendingAdd = new(); private readonly Dictionary<Serial, T> _pendingAdd = new();
private readonly Dictionary<Serial, T> _pendingDelete = new(); private readonly Dictionary<Serial, T> _pendingDelete = new();
private readonly uint[] _entitiesCount = new uint[World.GetThreadWorkerCount()];
private readonly (MemoryMapFileWriter idxWriter, MemoryMapFileWriter binWriter)[] _writers =
new (MemoryMapFileWriter, MemoryMapFileWriter)[World.GetThreadWorkerCount()];
public Dictionary<Serial, T> EntitiesBySerial { get; } = new(); public Dictionary<Serial, T> EntitiesBySerial { get; } = new();
public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : base(priority) public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : this(
name,
priority,
(Serial)minSerial,
(Serial)maxSerial
)
{
}
public GenericEntityPersistence(string name, int priority, Serial minSerial, Serial maxSerial) : base(name, priority)
{ {
_name = name;
_minSerial = minSerial; _minSerial = minSerial;
_maxSerial = maxSerial; _maxSerial = maxSerial;
_lastEntitySerial = (Serial)(minSerial - 1); _lastEntitySerial = minSerial - 1;
typeof(T).RegisterFindEntity(Find); typeof(T).RegisterFindEntity(Find);
} }
public override void Preserialize(string savePath, ConcurrentQueue<Type> types) public override void WriteSnapshot(string savePath, HashSet<Type> typeSet)
{ {
var path = Path.Combine(savePath, _name); var dir = Path.Combine(savePath, Name);
PathUtility.EnsureDirectory(path); PathUtility.EnsureDirectory(dir);
var threadCount = World.GetThreadWorkerCount(); var threads = World._threadWorkers;
for (var i = 0; i < threadCount; i++)
using var binFs = new FileStream(Path.Combine(dir, $"{Name}.bin"), FileMode.Create, FileAccess.Write, FileShare.None);
using var idxFs = new FileStream(Path.Combine(dir, $"{Name}.idx"), FileMode.Create);
using var idx = new MemoryMapFileWriter(idxFs, 1024 * 1024, typeSet); // 1MB
var binPosition = 0L;
// Support for non-entity generic serialization.
if (SerializedLength > 0)
{ {
var idxPath = Path.Combine(path, $"{_name}_{i}.idx"); try
var binPath = Path.Combine(path, $"{_name}_{i}.bin");
_writers[i] = (
new MemoryMapFileWriter(new FileStream(idxPath, FileMode.Create), _initialIdxSize, types),
new MemoryMapFileWriter(new FileStream(binPath, FileMode.Create), _initialBinSize, types)
);
_writers[i].idxWriter.Write(3); // version
_writers[i].idxWriter.Seek(4, SeekOrigin.Current); // Entity count
_entitiesCount[i] = 0;
}
}
public override void Serialize(IGenericSerializable e, int threadIndex)
{
var (idx, bin) = _writers[threadIndex];
var pos = bin.Position;
var entity = (ISerializable)e;
entity.Serialize(bin);
var length = (uint)(bin.Position - pos);
var t = entity.GetType();
idx.Write(t);
idx.Write(entity.Serial);
idx.Write(entity.Created.Ticks);
idx.Write(pos);
idx.Write(length);
_entitiesCount[threadIndex]++;
}
public override void WriteSnapshot()
{
var wroteFile = false;
string folderPath = null;
for (int i = 0; i < _writers.Length; i++)
{
var (idxWriter, binWriter) = _writers[i];
var binBytesWritten = binWriter.Position;
// Write the entity count
var pos = idxWriter.Position;
idxWriter.Seek(4, SeekOrigin.Begin);
idxWriter.Write(_entitiesCount[i]);
idxWriter.Seek(pos, SeekOrigin.Begin);
var idxFs = idxWriter.FileStream;
var idxFilePath = idxFs.Name;
var binFs = binWriter.FileStream;
var binFilePath = binFs.Name;
if (_initialIdxSize < idxFs.Position)
{ {
_initialIdxSize = idxFs.Position; binFs.Write(threads[SerializedThread].GetHeap(SerializedPosition, SerializedLength));
}
catch (Exception error)
{
logger.Error(
error,
"Error writing entity: (Thread: {Thread} - {Start} {Length})",
SerializedThread,
SerializedPosition,
SerializedLength
);
} }
if (_initialBinSize < binFs.Position) binPosition += SerializedLength;
{
_initialBinSize = binFs.Position;
}
idxWriter.Dispose();
binWriter.Dispose();
idxFs.Dispose();
binFs.Dispose();
if (binBytesWritten > 1)
{
wroteFile = true;
}
else
{
File.Delete(idxFilePath);
File.Delete(binFilePath);
folderPath = Path.GetDirectoryName(idxFilePath);
}
} }
if (!wroteFile && folderPath != null) idx.Write(3); // Version
idx.Write(EntitiesBySerial.Values.Count);
foreach (var e in EntitiesBySerial.Values)
{ {
Directory.Delete(folderPath); var thread = e.SerializedThread;
var heapStart = e.SerializedPosition;
var heapLength = e.SerializedLength;
idx.Write(e.GetType());
idx.Write(e.Serial);
idx.Write(e.Created.Ticks);
idx.Write(binPosition);
idx.Write(heapLength);
try
{
binFs.Write(threads[thread].GetHeap(heapStart, heapLength));
}
catch (Exception error)
{
logger.Error(
error,
"Error writing entity: {Entity} (Thread: {Thread} - {Start} {Length})",
e,
thread,
heapStart,
heapLength
);
}
binPosition += heapLength;
} }
} }
public override void Serialize() public override void Serialize()
{ {
World.ResetRoundRobin();
foreach (var entity in EntitiesBySerial.Values) foreach (var entity in EntitiesBySerial.Values)
{ {
World.PushToCache((entity, this)); World.PushToCache(entity);
} }
World.PushToCache(this);
} }
private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes) private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes)
@ -205,7 +178,7 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
*/ */
private unsafe Dictionary<int, ConstructorInfo> ReadTypes(string savePath) private unsafe Dictionary<int, ConstructorInfo> ReadTypes(string savePath)
{ {
string typesPath = Path.Combine(savePath, _name, $"{_name}.tdb"); string typesPath = Path.Combine(savePath, Name, $"{Name}.tdb");
if (!File.Exists(typesPath)) if (!File.Exists(typesPath))
{ {
return null; return null;
@ -236,59 +209,57 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
public virtual void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb) public virtual void DeserializeIndexes(string savePath, Dictionary<ulong, string> typesDb)
{ {
string indexPath = Path.Combine(savePath, _name, $"{_name}.idx"); string indexPath = Path.Combine(savePath, Name, $"{Name}.idx");
_entities ??= [];
// Support for legacy MUO Serialization that used split files
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
TryDeserializeMultithreadIndexes(savePath, typesDb); TryDeserializeSplitFileIndexes(savePath, typesDb);
return; return;
} }
_entities = [InternalDeserializeIndexes(indexPath, typesDb)]; InternalDeserializeIndexes(indexPath, typesDb, _entities[0] = []);
} }
private void TryDeserializeMultithreadIndexes(string savePath, Dictionary<ulong, string> typesDb) private void TryDeserializeSplitFileIndexes(string savePath, Dictionary<ulong, string> typesDb)
{ {
var index = 0; var index = 0;
var fileList = new List<string>();
while (true) while (true)
{ {
var path = Path.Combine(savePath, _name, $"{_name}_{index}.idx"); var path = Path.Combine(savePath, Name, $"{Name}_{index}.idx");
var fi = new FileInfo(path); var fi = new FileInfo(path);
if (!fi.Exists) if (!fi.Exists)
{ {
break; break;
} }
if (fi.Length != 0) if (fi.Length == 0)
{ {
fileList.Add(path); continue;
} }
InternalDeserializeIndexes(path, typesDb, _entities[index] = []);
index++; index++;
} }
_entities = new List<EntitySpan<T>>[fileList.Count];
for (var i = 0; i < fileList.Count; i++)
{
_entities[i] = InternalDeserializeIndexes(fileList[i], typesDb);
}
} }
private unsafe List<EntitySpan<T>> InternalDeserializeIndexes(string filePath, Dictionary<ulong, string> typesDb) private unsafe void InternalDeserializeIndexes(
string filePath, Dictionary<ulong, string> typesDb, List<EntitySpan<T>> entities
)
{ {
object[] ctorArgs = new object[1];
List<EntitySpan<T>> entities = [];
using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open);
using var accessor = mmf.CreateViewStream(); using var accessor = mmf.CreateViewStream();
byte* ptr = null; byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); var dataReader = new UnmanagedDataReader(ptr, accessor.Length);
var version = dataReader.ReadInt(); var version = dataReader.ReadInt();
Dictionary<int, ConstructorInfo> ctors = null; Dictionary<int, ConstructorInfo> ctors = [];
if (version < 2) if (version < 2)
{ {
ctors = ReadTypes(Path.GetDirectoryName(filePath)); ctors = ReadTypes(Path.GetDirectoryName(filePath));
@ -296,15 +267,16 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
if (typesDb == null && ctors == null) if (typesDb == null && ctors == null)
{ {
return entities; return;
} }
int count = dataReader.ReadInt();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var ctorArgs = new object[1];
Type[] ctorArguments = [typeof(Serial)]; Type[] ctorArguments = [typeof(Serial)];
for (int i = 0; i < count; ++i) var count = dataReader.ReadInt();
for (var i = 0; i < count; ++i)
{ {
ConstructorInfo ctor; ConstructorInfo ctor;
// Version 2 & 3 with SerializedTypes.db // Version 2 & 3 with SerializedTypes.db
@ -331,6 +303,7 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
{ {
dataReader.ReadLong(); // LastSerialized dataReader.ReadLong(); // LastSerialized
} }
var pos = dataReader.ReadLong(); var pos = dataReader.ReadLong();
var length = dataReader.ReadInt(); var length = dataReader.ReadInt();
@ -344,45 +317,39 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
if (ctor.Invoke(ctorArgs) is T entity) if (ctor.Invoke(ctorArgs) is T entity)
{ {
entity.Created = created; entity.Created = created;
entities.Add(new EntitySpan<T>(entity, pos, (int)length)); entities.Add(new EntitySpan<T>(entity, pos, length));
EntitiesBySerial[serial] = entity; EntitiesBySerial[serial] = entity;
} }
} }
accessor.SafeMemoryMappedViewHandle.ReleasePointer(); accessor.SafeMemoryMappedViewHandle.ReleasePointer();
entities.TrimExcess();
if (EntitiesBySerial.Count > 0) if (EntitiesBySerial.Count > 0)
{ {
_lastEntitySerial = EntitiesBySerial.Keys.Max(); _lastEntitySerial = EntitiesBySerial.Keys.Max();
} }
return entities;
} }
public override void Deserialize(string savePath, Dictionary<ulong, string> typesDb) public override void Deserialize(string savePath, Dictionary<ulong, string> typesDb)
{ {
string dataPath = Path.Combine(savePath, _name, $"{_name}.bin"); string dataPath = Path.Combine(savePath, Name, $"{Name}.bin");
var fi = new FileInfo(dataPath); var fi = new FileInfo(dataPath);
if (!fi.Exists) if (!fi.Exists)
{ {
TryDeserializeMultithread(savePath, typesDb); TryDeserializeMultithread(savePath, typesDb);
} }
else else if (fi.Length > 0)
{ {
if (fi.Length == 0)
{
return;
}
InternalDeserialize(dataPath, 0, typesDb); InternalDeserialize(dataPath, 0, typesDb);
} }
_entities.Clear();
_entities.TrimExcess();
_entities = null; _entities = null;
} }
private static unsafe void InternalDeserialize(string filePath, int index, Dictionary<ulong, string> typesDb) private unsafe void InternalDeserialize(string filePath, int index, Dictionary<ulong, string> typesDb)
{ {
using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open);
using var accessor = mmf.CreateViewStream(); using var accessor = mmf.CreateViewStream();
@ -390,6 +357,9 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
byte* ptr = null; byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb);
Deserialize(dataReader);
var deleteAllFailures = false; var deleteAllFailures = false;
foreach (var entry in _entities[index]) foreach (var entry in _entities[index])
@ -460,15 +430,25 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
return; return;
} }
var folderPath = Path.Combine(savePath, _name); var folderPath = Path.Combine(savePath, Name);
for (var i = 0; i < _entities.Length; i++) foreach (var i in _entities.Keys)
{ {
var path = Path.Combine(folderPath, $"{_name}_{i}.bin"); var path = Path.Combine(folderPath, $"{Name}_{i}.bin");
InternalDeserialize(path, i, typesDb); InternalDeserialize(path, i, typesDb);
} }
} }
// Override for non-entity serialization
public override void Serialize(IGenericWriter writer)
{
}
// Override for non-entity deserialization
public override void Deserialize(IGenericReader reader)
{
}
public override void PostWorldSave() public override void PostWorldSave()
{ {
ProcessSafetyQueues(); ProcessSafetyQueues();
@ -492,25 +472,26 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
); );
} }
#endif #endif
var last = _lastEntitySerial; var last = (uint)_lastEntitySerial;
var max = (Serial)_maxSerial; var min = (uint)_minSerial;
var max = (uint)_maxSerial;
for (uint i = 0; i < _maxSerial; i++) for (uint i = 0; i < max; i++)
{ {
last++; last++;
if (last > max) if (last > max)
{ {
last = (Serial)_minSerial; last = min;
} }
if (FindEntity<T>(last) == null) if (FindEntity<T>((Serial)last) == null)
{ {
return _lastEntitySerial = last; return _lastEntitySerial = (Serial)last;
} }
} }
OutOfMemory($"No serials left to allocate for {_name}"); OutOfMemory($"No serials left to allocate for {Name}");
return Serial.MinusOne; return Serial.MinusOne;
} }
} }
@ -530,17 +511,21 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
goto case WorldState.Loading; goto case WorldState.Loading;
} }
case WorldState.Loading: case WorldState.Loading:
case WorldState.WritingSave:
{ {
if (_pendingDelete.Remove(entity.Serial)) if (_pendingDelete.Remove(entity.Serial))
{ {
logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString()); logger.Warning(
"Deleted then added {Entity} during {WorldState} state.",
entity.GetType().Name,
worldState.ToString()
);
} }
_pendingAdd[entity.Serial] = entity; _pendingAdd[entity.Serial] = entity;
break; break;
} }
case WorldState.PendingSave: case WorldState.PendingSave:
case WorldState.WritingSave:
case WorldState.Running: case WorldState.Running:
{ {
ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists); ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists);
@ -591,13 +576,13 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
goto case WorldState.Loading; goto case WorldState.Loading;
} }
case WorldState.Loading: case WorldState.Loading:
case WorldState.WritingSave:
{ {
_pendingAdd.Remove(entity.Serial); _pendingAdd.Remove(entity.Serial);
_pendingDelete[entity.Serial] = entity; _pendingDelete[entity.Serial] = entity;
break; break;
} }
case WorldState.PendingSave: case WorldState.PendingSave:
case WorldState.WritingSave:
case WorldState.Running: case WorldState.Running:
{ {
EntitiesBySerial.Remove(entity.Serial); EntitiesBySerial.Remove(entity.Serial);
@ -613,8 +598,6 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
AddEntity(entity); AddEntity(entity);
} }
_pendingAdd.Clear();
foreach (var entity in _pendingDelete.Values) foreach (var entity in _pendingDelete.Values)
{ {
if (_pendingAdd.ContainsKey(entity.Serial)) if (_pendingAdd.ContainsKey(entity.Serial))
@ -625,10 +608,11 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
RemoveEntity(entity); RemoveEntity(entity);
} }
_pendingAdd.Clear();
_pendingDelete.Clear(); _pendingDelete.Clear();
} }
private void AppendSafetyLog(string action, ISerializable entity) private static void AppendSafetyLog(string action, ISerializable entity)
{ {
var message = var message =
$"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected."; $"Warning: Attempted to {{Action}} {{Entity}} during world save.{Environment.NewLine}This action could cause inconsistent state.{Environment.NewLine}It is strongly advised that the offending scripts be corrected.";
@ -638,7 +622,7 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
try try
{ {
using var op = new StreamWriter("world-save-errors.log", true); using var op = new StreamWriter("world-save-errors.log", true);
op.WriteLine("{0}\t{1}", DateTime.UtcNow, message); op.WriteLine($"{DateTime.UtcNow}\t{message}");
op.WriteLine(new StackTrace(2).ToString()); op.WriteLine(new StackTrace(2).ToString());
op.WriteLine(); op.WriteLine();
} }
@ -649,17 +633,9 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Find(Serial serial) => FindEntity<T>(serial, false, false); public T Find(Serial serial, bool returnDeleted = false) => FindEntity<T>(serial, returnDeleted);
[MethodImpl(MethodImplOptions.AggressiveInlining)] public R FindEntity<R>(Serial serial, bool returnDeleted = false) where R : class, T
public T Find(Serial serial, bool returnDeleted) => FindEntity<T>(serial, returnDeleted, false);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity<T>(serial, returnDeleted, returnPending);
public R FindEntity<R>(Serial serial) where R : class, T => FindEntity<R>(serial, false, false);
public R FindEntity<R>(Serial serial, bool returnDeleted, bool returnPending) where R : class, T
{ {
switch (World.WorldState) switch (World.WorldState)
{ {
@ -669,14 +645,14 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
} }
case WorldState.Loading: case WorldState.Loading:
case WorldState.Saving: case WorldState.Saving:
case WorldState.WritingSave:
{ {
if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) if (returnDeleted && _pendingDelete.TryGetValue(serial, out var entity))
{ {
return entity as R; return entity as R;
} }
if (returnPending && _pendingAdd.TryGetValue(serial, out entity) || if (_pendingAdd.TryGetValue(serial, out entity) || EntitiesBySerial.TryGetValue(serial, out entity))
EntitiesBySerial.TryGetValue(serial, out entity))
{ {
return entity as R; return entity as R;
} }
@ -684,7 +660,6 @@ public class GenericEntityPersistence<T> : Persistence, IGenericEntityPersistenc
return null; return null;
} }
case WorldState.PendingSave: case WorldState.PendingSave:
case WorldState.WritingSave:
case WorldState.Running: case WorldState.Running:
{ {
return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null;

View file

@ -14,65 +14,105 @@
*************************************************************************/ *************************************************************************/
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.MemoryMappedFiles;
namespace Server; namespace Server;
public abstract class GenericPersistence : Persistence, IGenericSerializable public abstract class GenericPersistence : Persistence, IGenericSerializable
{ {
private long _initialSize = 1024 * 1024;
private MemoryMapFileWriter _fileToSave;
public string Name { get; } public string Name { get; }
public string SaveFilePath { get; protected set; } // "<Folder>/<System>.bin"
public GenericPersistence(string name, int priority) : base(priority) => Name = name; public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public override void Preserialize(string savePath, ConcurrentQueue<Type> types) public GenericPersistence(string name, int priority) : base(priority)
{ {
var path = Path.Combine(savePath, Name); Name = name;
var filePath = Path.Combine(path, $"{Name}.bin"); SaveFilePath = Path.Combine(Name, $"{Name}.bin");
PathUtility.EnsureDirectory(path);
_fileToSave = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), _initialSize, types);
} }
public override void Serialize() public override void Serialize()
{ {
World.ResetRoundRobin(); World.PushToCache(this);
World.PushToCache((this, this));
} }
public override void WriteSnapshot() public override void WriteSnapshot(string savePath, HashSet<Type> typeSet)
{ {
string folderPath = null; if (SerializedLength == 0)
using (var fs = _fileToSave.FileStream)
{ {
if (fs.Position > _initialSize) return;
{
_initialSize = fs.Position;
}
_fileToSave.Dispose();
if (_fileToSave.Position == 0)
{
folderPath = Path.GetDirectoryName(fs.Name);
}
} }
if (folderPath != null) var file = Path.Combine(savePath, SaveFilePath);
{ var dir = Path.GetDirectoryName(file);
Directory.Delete(folderPath); PathUtility.EnsureDirectory(dir);
}
var threads = World._threadWorkers;
using var binFs = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
var thread = SerializedThread;
var heapStart = SerializedPosition;
var heapLength = SerializedLength;
binFs.Write(threads[thread].GetHeap(heapStart, heapLength));
} }
public override void Serialize(IGenericSerializable e, int threadIndex) => Serialize(_fileToSave); public override unsafe void Deserialize(string savePath, Dictionary<ulong, string> typesDb)
{
// Assume savePath has the Core.BaseDirectory already prepended
var dataPath = Path.GetFullPath(SaveFilePath, savePath);
var file = new FileInfo(dataPath);
if (!file.Exists || file.Length <= 0)
{
return;
}
var fileLength = file.Length;
string error;
try
{
using var mmf = MemoryMappedFile.CreateFromFile(dataPath, FileMode.Open);
using var accessor = mmf.CreateViewStream();
byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb);
Deserialize(dataReader);
error = dataReader.Position != fileLength
? $"Serialized {fileLength} bytes, but {dataReader.Position} bytes deserialized"
: null;
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
catch (Exception e)
{
error = e.ToString();
}
if (error != null)
{
Console.WriteLine($"***** Bad deserialize of {file.FullName} *****");
Console.WriteLine(error);
Console.Write("Skip this file and continue? (y/n): ");
var y = Console.ReadLine();
if (!y.InsensitiveEquals("y"))
{
throw new Exception("Deserialization failed.");
}
}
}
public abstract void Serialize(IGenericWriter writer); public abstract void Serialize(IGenericWriter writer);
public override void Deserialize(string savePath, Dictionary<ulong, string> typesDb) =>
AdhocPersistence.Deserialize(Path.Combine(savePath, Name, $"{Name}.bin"), Deserialize);
public abstract void Deserialize(IGenericReader reader); public abstract void Deserialize(IGenericReader reader);
} }

View file

@ -17,5 +17,9 @@ namespace Server;
public interface IGenericSerializable public interface IGenericSerializable
{ {
byte SerializedThread { get; set; }
int SerializedPosition { get; set; }
int SerializedLength { get; set; }
void Serialize(IGenericWriter writer); void Serialize(IGenericWriter writer);
} }

View file

@ -15,7 +15,7 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -29,7 +29,7 @@ public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable
{ {
private readonly Encoding _encoding; private readonly Encoding _encoding;
private readonly ConcurrentQueue<Type> _types; private readonly HashSet<Type> _types;
private readonly FileStream _fileStream; private readonly FileStream _fileStream;
private MemoryMappedFile _mmf; private MemoryMappedFile _mmf;
private MemoryMappedViewAccessor _accessor; private MemoryMappedViewAccessor _accessor;
@ -37,7 +37,7 @@ public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable
private long _position; private long _position;
private long _size; private long _size;
public MemoryMapFileWriter(FileStream fileStream, long initialSize, ConcurrentQueue<Type> types = null) public MemoryMapFileWriter(FileStream fileStream, long initialSize, HashSet<Type> types = null)
{ {
_types = types; _types = types;
_fileStream = fileStream; _fileStream = fileStream;
@ -244,7 +244,7 @@ public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable
{ {
Write((byte)0x2); // xxHash3 64bit Write((byte)0x2); // xxHash3 64bit
Write(AssemblyHandler.GetTypeHash(type)); Write(AssemblyHandler.GetTypeHash(type));
_types.Enqueue(type); _types.Add(type);
} }
} }

View file

@ -14,7 +14,6 @@
*************************************************************************/ *************************************************************************/
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
@ -53,7 +52,7 @@ public abstract class Persistence
} }
} }
private unsafe static Dictionary<ulong, string> LoadTypes(string path) private static unsafe Dictionary<ulong, string> LoadTypes(string path)
{ {
var db = new Dictionary<ulong, string>(); var db = new Dictionary<ulong, string>();
@ -85,32 +84,31 @@ public abstract class Persistence
} }
// Note: This is strictly on a background thread // Note: This is strictly on a background thread
internal static void PreSerializeAll(string path, ConcurrentQueue<Type> types) internal static void WriteSnapshotAll(string path, HashSet<Type> typeSet)
{ {
foreach (var p in _registry) foreach (var p in _registry)
{ {
p.Preserialize(path, types); p.WriteSnapshot(path, typeSet);
} }
WriteSerializedTypesSnapshot(path, typeSet);
} }
private static readonly HashSet<Type> _typesSet = []; public static void WriteSerializedTypesSnapshot(string path, HashSet<Type> types)
// Note: This is strictly on a background thread
internal static void WriteSnapshotAll(string path, ConcurrentQueue<Type> types)
{ {
foreach (var p in _registry) string typesPath = Path.Combine(path, "SerializedTypes.db");
{ using var fs = new FileStream(typesPath, FileMode.Create);
p.WriteSnapshot(); using var writer = new MemoryMapFileWriter(fs, 1024 * 1024 * 4);
}
writer.Write(0); // version
writer.Write(types.Count);
// Dedupe the queue.
foreach (var type in types) foreach (var type in types)
{ {
_typesSet.Add(type); var fullName = type.FullName;
writer.Write(HashUtility.ComputeHash64(fullName));
writer.WriteStringRaw(fullName);
} }
WriteSerializedTypesSnapshot(path, _typesSet);
_typesSet.Clear();
} }
internal static void SerializeAll() internal static void SerializeAll()
@ -137,32 +135,8 @@ public abstract class Persistence
} }
} }
public static void WriteSerializedTypesSnapshot(string path, HashSet<Type> types)
{
string typesPath = Path.Combine(path, "SerializedTypes.db");
using var fs = new FileStream(typesPath, FileMode.Create);
using var writer = new MemoryMapFileWriter(fs, 1024 * 1024 * 4);
writer.Write(0); // version
writer.Write(types.Count);
foreach (var type in types)
{
var fullName = type.FullName;
writer.Write(HashUtility.ComputeHash64(fullName));
writer.WriteStringRaw(fullName);
}
}
// Open file streams, MMFs, prepare data structures
// Note: This should only be run on a background thread // Note: This should only be run on a background thread
public abstract void Preserialize(string savePath, ConcurrentQueue<Type> types); public abstract void WriteSnapshot(string savePath, HashSet<Type> typeSet);
// Note: This should only be run on a background thread
public abstract void Serialize(IGenericSerializable e, int threadIndex);
// Note: This should only be run on a background thread
public abstract void WriteSnapshot();
public abstract void Serialize(); public abstract void Serialize();

View file

@ -21,10 +21,10 @@ namespace Server;
public static class SerializationExtensions public static class SerializationExtensions
{ {
private static readonly Dictionary<Type, Func<Serial, bool, bool, ISerializable>> _directFinderTable = new(); private static readonly Dictionary<Type, Func<Serial, bool, ISerializable>> _directFinderTable = new();
private static readonly Dictionary<Type, Func<Serial, bool, bool, ISerializable>> _searchTable = new(); private static readonly Dictionary<Type, Func<Serial, bool, ISerializable>> _searchTable = new();
public static void RegisterFindEntity(this Type type, Func<Serial, bool, bool, ISerializable> func) public static void RegisterFindEntity(this Type type, Func<Serial, bool, ISerializable> func)
{ {
_searchTable[type] = func; _searchTable[type] = func;
} }
@ -47,12 +47,12 @@ public static class SerializationExtensions
if (typeof(IEntity).IsAssignableFrom(typeT)) if (typeof(IEntity).IsAssignableFrom(typeT))
{ {
return World.FindEntity<IEntity>(serial, returnPending: false) as T; return World.FindEntity<IEntity>(serial) as T;
} }
if (_directFinderTable.TryGetValue(typeT, out var finder)) if (_directFinderTable.TryGetValue(typeT, out var finder))
{ {
return finder(serial, false, false) as T; return finder(serial, false) as T;
} }
Type type = null; Type type = null;
@ -87,7 +87,7 @@ public static class SerializationExtensions
finder = _searchTable[type]; finder = _searchTable[type];
_directFinderTable[type] = finder; _directFinderTable[type] = finder;
return finder(serial, false, false) as T; return finder(serial, false) as T;
} }
public static List<T> ReadEntityList<T>( public static List<T> ReadEntityList<T>(

View file

@ -0,0 +1,113 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SerializationThreadWorker.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.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Server;
public class SerializationThreadWorker
{
private const int MinHeapSize = 1024 * 1024; // 1MB
private readonly int _index;
private readonly Thread _thread;
private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working
private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining
private bool _pause;
private bool _exit;
private byte[] _heap;
private readonly ConcurrentQueue<IGenericSerializable> _entities;
public SerializationThreadWorker(int index)
{
_index = index;
_startEvent = new AutoResetEvent(false);
_stopEvent = new AutoResetEvent(false);
_entities = new ConcurrentQueue<IGenericSerializable>();
_thread = new Thread(Execute);
_thread.Start(this);
}
public void Wake()
{
_startEvent.Set();
}
public void Sleep()
{
Volatile.Write(ref _pause, true);
_stopEvent.WaitOne();
}
public void Exit()
{
_exit = true;
Wake();
Sleep();
}
public void AllocateHeap() => _heap ??= GC.AllocateUninitializedArray<byte>(MinHeapSize); // 1MB
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Push(IGenericSerializable entity) => _entities.Enqueue(entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> GetHeap(int start, int length) => _heap.AsSpan(start, length);
private static void Execute(object obj)
{
var worker = (SerializationThreadWorker)obj;
var threadIndex = (byte)worker._index;
var queue = worker._entities;
var serializedTypes = World.SerializedTypes;
while (worker._startEvent.WaitOne())
{
var writer = new BufferWriter(worker._heap, true, serializedTypes);
while (true)
{
var pauseRequested = Volatile.Read(ref worker._pause);
if (queue.TryDequeue(out var e))
{
e.SerializedThread = threadIndex;
var start = e.SerializedPosition = (int)writer.Position;
e.Serialize(writer);
e.SerializedLength = (int)(writer.Position - start);
}
else if (pauseRequested) // Break when finished
{
break;
}
}
worker._heap = writer.Buffer;
writer.Close();
worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished
worker._pause = false;
if (Core.Closing || worker._exit)
{
return;
}
}
}
}

View file

@ -45,7 +45,7 @@ public static class World
private static readonly GenericEntityPersistence<BaseGuild> _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF); private static readonly GenericEntityPersistence<BaseGuild> _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF);
private static int _threadId; private static int _threadId;
private static readonly SerializationThreadWorker[] _threadWorkers = new SerializationThreadWorker[Math.Max(Environment.ProcessorCount - 1, 1)]; internal static SerializationThreadWorker[] _threadWorkers;
private static readonly ManualResetEvent _diskWriteHandle = new(true); private static readonly ManualResetEvent _diskWriteHandle = new(true);
private static readonly ConcurrentQueue<Item> _decayQueue = new(); private static readonly ConcurrentQueue<Item> _decayQueue = new();
@ -88,6 +88,7 @@ public static class World
public static Dictionary<Serial, Mobile> Mobiles => _mobilePersistence.EntitiesBySerial; public static Dictionary<Serial, Mobile> Mobiles => _mobilePersistence.EntitiesBySerial;
public static Dictionary<Serial, BaseGuild> Guilds => _guildPersistence.EntitiesBySerial; public static Dictionary<Serial, BaseGuild> Guilds => _guildPersistence.EntitiesBySerial;
public static bool UseMultiThreadedSaves { get; private set; }
public static string SavePath { get; private set; } public static string SavePath { get; private set; }
public static WorldState WorldState { get; private set; } public static WorldState WorldState { get; private set; }
public static bool Saving => WorldState == WorldState.Saving; public static bool Saving => WorldState == WorldState.Saving;
@ -101,13 +102,12 @@ public static class World
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves"); var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
SavePath = PathUtility.GetFullPath(savePath); SavePath = PathUtility.GetFullPath(savePath);
UseMultiThreadedSaves = ServerConfiguration.GetOrUpdateSetting("world.useMultithreadedSaves", true);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WaitForWriteCompletion() public static void WaitForWriteCompletion() => _diskWriteHandle.WaitOne();
{
_diskWriteHandle.WaitOne();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnqueueForDecay(Item item) private static void EnqueueForDecay(Item item)
@ -207,6 +207,9 @@ public static class World
); );
// Create the serialization threads. // Create the serialization threads.
var threadCount = UseMultiThreadedSaves ? Math.Max(Environment.ProcessorCount - 1, 1) : 1;
_threadWorkers = new SerializationThreadWorker[threadCount];
for (var i = 0; i < _threadWorkers.Length; i++) for (var i = 0; i < _threadWorkers.Length; i++)
{ {
_threadWorkers[i] = new SerializationThreadWorker(i); _threadWorkers[i] = new SerializationThreadWorker(i);
@ -267,17 +270,26 @@ public static class World
return; return;
} }
WaitForWriteCompletion(); // Blocks Save until current disk flush is done.
_diskWriteHandle.Reset();
WorldState = WorldState.PendingSave; WorldState = WorldState.PendingSave;
ThreadPool.QueueUserWorkItem(Preserialize); ThreadPool.QueueUserWorkItem(Preserialize);
} }
internal static void Preserialize(object state) private static void Preserialize(object state)
{ {
var tempPath = PathUtility.EnsureRandomPath(_tempSavePath); var tempPath = PathUtility.EnsureRandomPath(_tempSavePath);
try try
{ {
Persistence.PreSerializeAll(tempPath, SerializedTypes); // Allocate the heaps for the GC
foreach (var worker in _threadWorkers)
{
worker.AllocateHeap();
}
WakeSerializationThreads();
Core.RequestSnapshot(tempPath); Core.RequestSnapshot(tempPath);
} }
catch (Exception ex) catch (Exception ex)
@ -296,9 +308,7 @@ public static class World
return; return;
} }
WaitForWriteCompletion(); // Blocks Save until current disk flush is done. NetState.FlushAll();
_diskWriteHandle.Reset();
WorldState = WorldState.Saving; WorldState = WorldState.Saving;
@ -314,7 +324,6 @@ public static class World
{ {
_serializationStart = Core.Now; _serializationStart = Core.Now;
WakeSerializationThreads();
Persistence.SerializeAll(); Persistence.SerializeAll();
PauseSerializationThreads(); PauseSerializationThreads();
EventSink.InvokeWorldSave(); EventSink.InvokeWorldSave();
@ -324,9 +333,8 @@ public static class World
exception = ex; exception = ex;
} }
WorldState = WorldState.PendingSave; WorldState = WorldState.WritingSave;
ThreadPool.QueueUserWorkItem(WriteFiles, snapshotPath); ThreadPool.QueueUserWorkItem(WriteFiles, snapshotPath);
Persistence.PostWorldSaveAll(); // Process safety queues
watch.Stop(); watch.Stop();
if (exception == null) if (exception == null)
@ -345,6 +353,8 @@ public static class World
} }
} }
private static readonly HashSet<Type> _typesSet = [];
private static void WriteFiles(object state) private static void WriteFiles(object state)
{ {
var snapshotPath = (string)state; var snapshotPath = (string)state;
@ -352,7 +362,16 @@ public static class World
{ {
var watch = Stopwatch.StartNew(); var watch = Stopwatch.StartNew();
logger.Information("Writing world save snapshot"); logger.Information("Writing world save snapshot");
Persistence.WriteSnapshotAll(snapshotPath, SerializedTypes);
// Dedupe the types
while (SerializedTypes.TryDequeue(out var type))
{
_typesSet.Add(type);
}
Persistence.WriteSnapshotAll(snapshotPath, _typesSet);
_typesSet.Clear();
try try
{ {
@ -380,7 +399,13 @@ public static class World
SerializedTypes.Clear(); SerializedTypes.Clear();
_diskWriteHandle.Set(); _diskWriteHandle.Set();
Core.LoopContext.Post(FinishWorldSave);
}
private static void FinishWorldSave()
{
WorldState = WorldState.Running; WorldState = WorldState.Running;
Persistence.PostWorldSaveAll(); // Process decay and safety queues
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -408,9 +433,9 @@ public static class World
internal static void ResetRoundRobin() => _threadId = 0; internal static void ResetRoundRobin() => _threadId = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void PushToCache((IGenericSerializable e, Persistence p) ep) internal static void PushToCache(IGenericSerializable e)
{ {
_threadWorkers[_threadId++].Push(ep); _threadWorkers[_threadId++].Push(e);
if (_threadId == _threadWorkers.Length) if (_threadId == _threadWorkers.Length)
{ {
_threadId = 0; _threadId = 0;
@ -474,20 +499,20 @@ public static class World
// Legacy: Only used for retrieving Items and Mobiles. // Legacy: Only used for retrieving Items and Mobiles.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = false) => public static IEntity FindEntity(Serial serial, bool returnDeleted = false) =>
FindEntity<IEntity>(serial, returnDeleted, returnPending); FindEntity<IEntity>(serial, returnDeleted);
// Legacy: Only used for retrieving Items and Mobiles. // Legacy: Only used for retrieving Items and Mobiles.
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T FindEntity<T>(Serial serial, bool returnDeleted = false, bool returnPending = false) public static T FindEntity<T>(Serial serial, bool returnDeleted = false)
where T : class, IEntity where T : class, IEntity
{ {
if (serial.IsItem) if (serial.IsItem)
{ {
return _itemPersistence.Find(serial, returnDeleted, returnPending) as T; return _itemPersistence.Find(serial, returnDeleted) as T;
} }
return _mobilePersistence.Find(serial, returnDeleted, returnPending) as T; return _mobilePersistence.Find(serial, returnDeleted) as T;
} }
private class ItemPersistence : GenericEntityPersistence<Item> private class ItemPersistence : GenericEntityPersistence<Item>
@ -520,7 +545,7 @@ public static class World
EnqueueForDecay(item); EnqueueForDecay(item);
} }
PushToCache((item, this)); PushToCache(item);
} }
} }
@ -551,78 +576,4 @@ public static class World
} }
} }
} }
private class SerializationThreadWorker
{
private readonly int _index;
private readonly Thread _thread;
private readonly AutoResetEvent _startEvent; // Main thread tells the thread to start working
private readonly AutoResetEvent _stopEvent; // Main thread waits for the worker finish draining
private bool _pause;
private bool _exit;
private readonly ConcurrentQueue<(IGenericSerializable, Persistence)> _entities;
public SerializationThreadWorker(int index)
{
_index = index;
_startEvent = new AutoResetEvent(false);
_stopEvent = new AutoResetEvent(false);
_entities = new ConcurrentQueue<(IGenericSerializable, Persistence)>();
_thread = new Thread(Execute);
_thread.Start(this);
}
public void Wake()
{
_startEvent.Set();
}
public void Sleep()
{
Volatile.Write(ref _pause, true);
_stopEvent.WaitOne();
}
public void Exit()
{
_exit = true;
Wake();
Sleep();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Push((IGenericSerializable e, Persistence p) ep) => _entities.Enqueue(ep);
private static void Execute(object obj)
{
SerializationThreadWorker worker = (SerializationThreadWorker)obj;
var reader = worker._entities;
while (worker._startEvent.WaitOne())
{
while (true)
{
bool pauseRequested = Volatile.Read(ref worker._pause);
if (reader.TryDequeue(out var ep))
{
var (e, p) = ep;
p.Serialize(e, worker._index);
}
else if (pauseRequested) // Break when finished
{
break;
}
}
worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished
worker._pause = false;
if (Core.Closing || worker._exit)
{
return;
}
}
}
}
} }

View file

@ -284,6 +284,10 @@ public partial class Account : IAccount, IComparable<Account>
public Serial Serial { get; set; } public Serial Serial { get; set; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
[AfterDeserialization(false)] [AfterDeserialization(false)]
private void AfterDeserialization() private void AfterDeserialization()
{ {

View file

@ -26,6 +26,10 @@ public abstract partial class BaseBOBEntry : IBOBEntry
public Serial Serial { get; } public Serial Serial { get; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public bool Deleted { get; private set; } public bool Deleted { get; private set; }
public BaseBOBEntry() public BaseBOBEntry()

View file

@ -16,6 +16,10 @@ public partial class EthicsEntity : ISerializable
public Serial Serial { get; } public Serial Serial { get; }
public byte SerializedThread { get; set; }
public int SerializedPosition { get; set; }
public int SerializedLength { get; set; }
public bool Deleted { get; private set; } public bool Deleted { get; private set; }
public void Delete() public void Delete()

View file

@ -3968,7 +3968,14 @@ namespace Server.Gumps
InvokeCommand("Save"); InvokeCommand("Save");
} }
Core.Kill(restart); // Kill the server on a different thread otherwise we will dead lock
ThreadPool.QueueUserWorkItem(
_ =>
{
World.WaitForWriteCompletion();
Core.Kill(restart);
}
);
} }
private void InvokeCommand(string c) private void InvokeCommand(string c)