From 8ec203f3872cf747a654a2a0a8516acf4b37322f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Jun 2024 10:51:20 -0700 Subject: [PATCH] feat: Updates serialization to use MMF (considerable memory savings) (#1841) ### Summary Updates the serialization strategy to use `MemoryMappedFile` instead of thick buffers. This has the benefit of being on-par with the current implementation (based on hardware/OS), however won't incur the double-memory issue. > [!Important] > **Developer Note** > The `BinaryFileWriter` and `BinaryFileReader` has been removed in favor of `MemoryMapFileWriter` and `UnmanagedDataReader` --- .config/dotnet-tools.json | 2 +- .../Packets/Outgoing/AccountPacketTests.cs | 2 - Projects/Server/Guild.cs | 8 +- Projects/Server/IEntity.cs | 6 +- Projects/Server/Items/Item.cs | 21 +- Projects/Server/Main.cs | 13 +- Projects/Server/Mobiles/Mobile.cs | 21 +- .../Server/Serialization/AdhocPersistence.cs | 76 ++-- .../Server/Serialization/BinaryFileReader.cs | 173 ------- .../Server/Serialization/BinaryFileWriter.cs | 101 ----- Projects/Server/Serialization/BufferReader.cs | 23 +- Projects/Server/Serialization/BufferWriter.cs | 8 +- .../Serialization/GenericEntityPersistence.cs | 423 +++++++++++++++++- .../Serialization/GenericPersistence.cs | 53 ++- .../Server/Serialization/IGenericReader.cs | 5 +- .../Server/Serialization/IGenericWriter.cs | 4 +- .../Server/Serialization/ISerializable.cs | 47 +- .../Serialization/ISerializableExtensions.cs | 7 +- .../Serialization/MemoryMapFileWriter.cs | 304 +++++++++++++ Projects/Server/Serialization/Persistence.cs | 98 ++-- .../Serialization/UnmanagedDataReader.cs | 246 ++++++++++ Projects/Server/Server.csproj | 2 +- Projects/Server/World/EntityPersistence.cs | 330 -------------- Projects/Server/World/IGenericSerializable.cs | 9 +- Projects/Server/World/World.cs | 202 +++++---- Projects/UOContent/Misc/CrashGuard.cs | 6 +- Projects/UOContent/UOContent.csproj | 2 +- 27 files changed, 1261 insertions(+), 931 deletions(-) delete mode 100644 Projects/Server/Serialization/BinaryFileReader.cs delete mode 100644 Projects/Server/Serialization/BinaryFileWriter.cs create mode 100644 Projects/Server/Serialization/MemoryMapFileWriter.cs create mode 100644 Projects/Server/Serialization/UnmanagedDataReader.cs delete mode 100644 Projects/Server/World/EntityPersistence.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e86abb1285..d78909cdbc 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "2.10.9", + "version": "2.11.3", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs index a549658ba0..0ad93a512f 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/AccountPacketTests.cs @@ -48,8 +48,6 @@ public Mobile this[int index] } public DateTime Created { get; set; } - public long SavePosition { get; set; } - public BufferWriter SaveBuffer { get; set; } public Serial Serial { get; } public void Deserialize(IGenericReader reader) => throw new NotImplementedException(); diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index 1a793cc49b..ab11330c38 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Guild.cs * * * @@ -52,12 +52,6 @@ protected BaseGuild() [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [IgnoreDupe] - public long SavePosition { get; set; } = -1; - - [IgnoreDupe] - public BufferWriter SaveBuffer { get; set; } - public abstract void Serialize(IGenericWriter writer); public abstract void Deserialize(IGenericReader reader); diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index bcb5613f27..9187c78018 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: IEntity.cs * * * @@ -51,10 +51,6 @@ public Entity(Serial serial, Point3D loc, Map map) : this(serial) public DateTime Created { get; set; } = Core.Now; - public long SavePosition { get; set; } = -1; - - public BufferWriter SaveBuffer { get; set; } - public Serial Serial { get; } public Point3D Location { get; private set; } diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 66412b8cf8..5c73f91a3b 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -1,3 +1,18 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Item.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 . * + *************************************************************************/ + using System; using System.Collections.Generic; using System.Reflection; @@ -783,12 +798,6 @@ public virtual void GetProperties(IPropertyList list) [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [IgnoreDupe] - public long SavePosition { get; set; } = -1; - - [IgnoreDupe] - public BufferWriter SaveBuffer { get; set; } - [IgnoreDupe] [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index bacae45182..cdc30fa3df 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Main.cs * * * @@ -40,6 +40,7 @@ public static class Core private static bool _performProcessKill; private static bool _restartOnKill; private static bool _performSnapshot; + private static string _snapshotPath; private static bool _crashed; private static string _baseDirectory; @@ -411,7 +412,6 @@ private static void HandleClosed() logger.Information("Shutting down"); World.WaitForWriteCompletion(); - World.ExitSerializationThreads(); if (!_crashed) @@ -553,12 +553,13 @@ public static void RunEventLoop() if (_performSnapshot) { // Return value is the offset that can be used to fix timers that should drift - World.Snapshot(); + World.Snapshot(_snapshotPath); _performSnapshot = false; } if (_performProcessKill) { + World.WaitForWriteCompletion(); break; } @@ -594,7 +595,11 @@ public static void RunEventLoop() DoKill(_restartOnKill); } - internal static void RequestSnapshot() => _performSnapshot = true; + internal static void RequestSnapshot(string snapshotPath) + { + _performSnapshot = true; + _snapshotPath = snapshotPath; + } public static void VerifySerialization() { diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 583ad8f4d6..a454136557 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -1,3 +1,18 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Mobile.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 . * + *************************************************************************/ + using System; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -2262,12 +2277,6 @@ public virtual void GetProperties(IPropertyList list) [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [IgnoreDupe] - public long SavePosition { get; set; } = -1; - - [IgnoreDupe] - public BufferWriter SaveBuffer { get; set; } - [IgnoreDupe] [CommandProperty(AccessLevel.Counselor)] public Serial Serial { get; } diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index dd4161222a..332f3a83c1 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: AdhocPersistence.cs * * * @@ -25,11 +25,9 @@ namespace Server; public static class AdhocPersistence { /** - * Serializes to memory synchronously. Optional buffer can be provided. - * Note: The buffer may not be the same after returning from the function if more data is written - * than the initial buffer can handle. + * Serializes to memory. */ - public static BufferWriter Serialize(Action serializer, ConcurrentQueue types) + public static IGenericWriter SerializeToBuffer(Action serializer, ConcurrentQueue types = null) { var saveBuffer = new BufferWriter(true, types); serializer(saveBuffer); @@ -37,36 +35,39 @@ public static BufferWriter Serialize(Action serializer, Concurre } /** - * Writes a buffer to disk. This function should be called asynchronously. - * Writes the filePath for the binary data, and an accompanying SerializedTypes.db file of all possible types. + * Deserializes from a buffer. */ - public static void WriteSnapshot(FileInfo file, Span buffer) + public static IGenericReader DeserializeFromBuffer( + byte[] buffer, Action deserializer, Dictionary typesDb = null + ) { - var dirPath = file.DirectoryName; - PathUtility.EnsureDirectory(dirPath); - - using var fs = new FileStream(file.FullName, FileMode.Create, FileAccess.Write); - fs.Write(buffer); + var reader = new BufferReader(buffer, typesDb); + deserializer(reader); + return reader; } /** - * Serializes to a memory buffer synchronously, then flushes to the path asynchronously. - * See WriteSnapshot for more info about how to snapshot. + * Serializes to a Memory Mapped file synchronously, then flushes to the file asynchronously. */ - public static void SerializeAndSnapshot(string filePath, Action serializer, ConcurrentQueue types = null) + public static void SerializeAndSnapshot( + string filePath, Action serializer, long sizeHint = 1024 * 1024 * 32 + ) { - types ??= new ConcurrentQueue(); - var saveBuffer = Serialize(serializer, types); + var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); + PathUtility.EnsureDirectory(Path.GetDirectoryName(fullPath)); + ConcurrentQueue types = []; + var writer = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), sizeHint, types); + serializer(writer); + Task.Run( () => { - var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); - var file = new FileInfo(fullPath); + var fs = writer.FileStream; - WriteSnapshot(file, saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); + writer.Dispose(); + fs.Dispose(); - // TODO: Create a PooledHashSet if performance becomes an issue. - var typesSet = new HashSet(); + HashSet typesSet = []; // Dedupe the queue. foreach (var type in types) @@ -74,38 +75,41 @@ public static void SerializeAndSnapshot(string filePath, Action typesSet.Add(type); } - Persistence.WriteSerializedTypesSnapshot(file.DirectoryName, typesSet); - }); + Persistence.WriteSerializedTypesSnapshot(Path.GetDirectoryName(fullPath), typesSet); + }, + Core.ClosingTokenSource.Token + ); } - public static void Deserialize(string filePath, Action deserializer) + public static unsafe void Deserialize(string filePath, Action deserializer) { var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); var file = new FileInfo(fullPath); - if (!file.Exists) + if (!file.Exists || file.Length == 0) { return; } var fileLength = file.Length; - if (fileLength == 0) - { - return; - } string error; try { using var mmf = MemoryMappedFile.CreateFromFile(fullPath, FileMode.Open); - using var stream = mmf.CreateViewStream(); - using var br = new BinaryFileReader(stream); - deserializer(br); + using var accessor = mmf.CreateViewStream(); - error = br.Position != fileLength - ? $"Serialized {fileLength} bytes, but {br.Position} bytes deserialized" + byte* ptr = null; + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length); + deserializer(dataReader); + + error = dataReader.Position != fileLength + ? $"Serialized {fileLength} bytes, but {dataReader.Position} bytes deserialized" : null; + + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } catch (Exception e) { diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs deleted file mode 100644 index c3729836dd..0000000000 --- a/Projects/Server/Serialization/BinaryFileReader.cs +++ /dev/null @@ -1,173 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - 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 . * - *************************************************************************/ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; -using Server.Buffers; -using Server.Logging; -using Server.Text; - -namespace Server; - -public class BinaryFileReader : IGenericReader, IDisposable -{ - private static readonly ILogger logger = LogFactory.GetLogger(typeof(BinaryFileReader)); - - private Dictionary _typesDb; - private BinaryReader _reader; - private Encoding _encoding; - - public BinaryFileReader(BinaryReader br, Dictionary typesDb = null, Encoding encoding = null) - { - _reader = br; - _encoding = encoding ?? TextEncoding.UTF8; - _typesDb = typesDb; - } - - public BinaryFileReader(Stream stream, Dictionary typesDb = null, Encoding encoding = null) - : this(new BinaryReader(stream), typesDb, encoding) - { - } - - public long Position => _reader.BaseStream.Position; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Close() => _reader.Close(); - - public DateTime LastSerialized { get; init; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; - - public string ReadStringRaw(bool intern = false) - { - var length = ((IGenericReader)this).ReadEncodedInt(); - if (length <= 0) - { - return "".Intern(); - } - - byte[] buffer = STArrayPool.Shared.Rent(length); - var strBuffer = buffer.AsSpan(0, length); - _reader.Read(strBuffer); - var str = TextEncoding.GetString(strBuffer, _encoding); - STArrayPool.Shared.Return(buffer); - return intern ? str.Intern() : str; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long ReadLong() => _reader.ReadInt64(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong ReadULong() => _reader.ReadUInt64(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int ReadInt() => _reader.ReadInt32(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint ReadUInt() => _reader.ReadUInt32(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short ReadShort() => _reader.ReadInt16(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort ReadUShort() => _reader.ReadUInt16(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double ReadDouble() => _reader.ReadDouble(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float ReadFloat() => _reader.ReadSingle(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte ReadByte() => _reader.ReadByte(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public sbyte ReadSByte() => _reader.ReadSByte(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ReadBool() => _reader.ReadBoolean(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Serial ReadSerial() => (Serial)_reader.ReadUInt32(); - - public Type ReadType() => - ReadByte() switch - { - 0 => null, - 1 => AssemblyHandler.FindTypeByFullName(ReadStringRaw()), // Backward compatibility - 2 => ReadTypeByHash() - }; - - public Type ReadTypeByHash() - { - var hash = ReadULong(); - var t = AssemblyHandler.FindTypeByHash(hash); - - if (t != null) - { - return t; - } - - if (_typesDb == null) - { - logger.Error( - new Exception($"The file SerializedTypes.db was not loaded. Type hash '{hash}' could not be found."), - "Invalid {Hash} at position {Position}", - hash, - Position - ); - - return null; - } - - if (!_typesDb.TryGetValue(hash, out var typeName)) - { - logger.Error( - new Exception($"Type hash '{hash}' is not present in the serialized types database."), - "Invalid type hash {Hash} at position {Position}", - hash, - Position - ); - - return null; - } - - t = AssemblyHandler.FindTypeByFullName(typeName, false); - - if (t == null) - { - logger.Error( - new Exception($"Type '{typeName}' was not found."), - "Type {Type} was not found.", - typeName - ); - } - - return t; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int Read(Span buffer) => _reader.Read(buffer); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long Seek(long offset, SeekOrigin origin) => _reader.BaseStream.Seek(offset, origin); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Dispose() => Close(); -} diff --git a/Projects/Server/Serialization/BinaryFileWriter.cs b/Projects/Server/Serialization/BinaryFileWriter.cs deleted file mode 100644 index 947877177f..0000000000 --- a/Projects/Server/Serialization/BinaryFileWriter.cs +++ /dev/null @@ -1,101 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: BinaryFileWriter.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 . * - *************************************************************************/ - -using System; -using System.Collections.Concurrent; -using System.IO; - -namespace Server; - -public class BinaryFileWriter : BufferWriter, IDisposable -{ - private readonly Stream _file; - private long _position; - - public BinaryFileWriter(string filename, bool prefixStr, ConcurrentQueue types = null) : - this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), prefixStr, types) - {} - - public BinaryFileWriter(Stream stream, bool prefixStr, ConcurrentQueue types = null) : base(prefixStr, types) - { - _file = stream; - _position = _file.Position; - } - - public override long Position => _position + Index; - - protected override int BufferSize => 81920; - - public override void Flush() - { - if (Index > 0) - { - _position += Index; - - _file.Write(Buffer, 0, (int)Index); - Index = 0; - } - else - { - base.Flush(); // Increase buffer size - } - } - - public override void Write(byte[] bytes) => Write(bytes, 0, bytes.Length); - - public override void Write(byte[] bytes, int offset, int count) - { - if (Index > 0) - { - Flush(); - } - - _file.Write(bytes, offset, count); - _position += count; - } - - public override void Write(ReadOnlySpan bytes) - { - if (Index > 0) - { - Flush(); - } - - _file.Write(bytes); - _position += bytes.Length; - } - - public override void Close() - { - if (Index > 0) - { - Flush(); - } - - _file.Close(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - if (Index > 0) - { - Flush(); - } - - return _position = _file.Seek(offset, origin); - } - - public void Dispose() => Close(); -} diff --git a/Projects/Server/Serialization/BufferReader.cs b/Projects/Server/Serialization/BufferReader.cs index 133c0e2a6c..6e55a643ad 100644 --- a/Projects/Server/Serialization/BufferReader.cs +++ b/Projects/Server/Serialization/BufferReader.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: BufferReader.cs * * * @@ -29,9 +29,9 @@ public class BufferReader : IGenericReader { private static readonly ILogger logger = LogFactory.GetLogger(typeof(BufferReader)); - private Dictionary _typesDb; - private Encoding _encoding; - private byte[] _buffer; + private readonly Dictionary _typesDb; + private readonly Encoding _encoding; + private readonly byte[] _buffer; private int _position; public long Position => _position; @@ -44,21 +44,6 @@ public BufferReader(byte[] buffer, Dictionary typesDb = null, Enc _typesDb = typesDb; } - public BufferReader(byte[] buffer, DateTime lastSerialized, Dictionary typesDb = null) : this(buffer) - { - LastSerialized = lastSerialized; - _typesDb = typesDb; - } - - public void Reset(byte[] newBuffer, out byte[] oldBuffer) - { - oldBuffer = _buffer; - _buffer = newBuffer; - _position = 0; - } - - public DateTime LastSerialized { get; init; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index 065e61f28f..2d425ba1be 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: BufferWriter.cs * * * @@ -27,9 +27,9 @@ namespace Server; public class BufferWriter : IGenericWriter { - private ConcurrentQueue _types; - private Encoding _encoding; - private bool _prefixStrings; + private readonly ConcurrentQueue _types; + private readonly Encoding _encoding; + private readonly bool _prefixStrings; private long _bytesWritten; private long _index; diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index 283e96d648..28a9696da6 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -14,10 +14,13 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.MemoryMappedFiles; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Server.Logging; @@ -32,17 +35,23 @@ public interface IGenericEntityPersistence public class GenericEntityPersistence : Persistence, IGenericEntityPersistence where T : class, ISerializable { private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntityPersistence)); + private static List>[] _entities; - private static List> _entities; - - private string _name; + private long _initialIdxSize = 1024 * 256; + private long _initialBinSize = 1024 * 1024; + private readonly string _name; + private readonly uint _minSerial; + private readonly uint _maxSerial; private Serial _lastEntitySerial; private readonly Dictionary _pendingAdd = new(); private readonly Dictionary _pendingDelete = new(); - private uint _minSerial; - private uint _maxSerial; - public Dictionary EntitiesBySerial { get; private set; } = new(); + private readonly uint[] _entitiesCount = new uint[World.GetThreadWorkerCount()]; + + private readonly (MemoryMapFileWriter idxWriter, MemoryMapFileWriter binWriter)[] _writers = + new (MemoryMapFileWriter, MemoryMapFileWriter)[World.GetThreadWorkerCount()]; + + public Dictionary EntitiesBySerial { get; } = new(); public GenericEntityPersistence(string name, int priority, uint minSerial, uint maxSerial) : base(priority) { @@ -53,40 +62,414 @@ public GenericEntityPersistence(string name, int priority, uint minSerial, uint typeof(T).RegisterFindEntity(Find); } + public override void Preserialize(string savePath, ConcurrentQueue types) + { + var path = Path.Combine(savePath, _name); + PathUtility.EnsureDirectory(path); + + var threadCount = World.GetThreadWorkerCount(); + for (var i = 0; i < threadCount; i++) + { + var idxPath = Path.Combine(path, $"{_name}_{i}.idx"); + 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; + } + + if (_initialBinSize < binFs.Position) + { + _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) + { + Directory.Delete(folderPath); + } + } + public override void Serialize() { + World.ResetRoundRobin(); foreach (var entity in EntitiesBySerial.Values) { - World.PushToCache(entity); + World.PushToCache((entity, this)); + } + } + + private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes) + { + if (t?.IsAbstract != false) + { + Console.WriteLine("failed"); + + var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; + + Console.Write($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n): "); + + if (Console.ReadLine().InsensitiveEquals("y")) + { + Console.WriteLine("Loading..."); + return null; + } + + Console.WriteLine("Types will not be deleted. An exception will be thrown."); + + throw new Exception($"Bad type '{typeName}'"); } + + var ctor = t.GetConstructor(constructorTypes); + + if (ctor == null) + { + throw new Exception($"Type '{t}' does not have a serialization constructor"); + } + + return ctor; } - public override void WriteSnapshot(string basePath) + /** + * Legacy ReadTypes for backward compatibility with old saves that still have a tdb file + */ + private unsafe Dictionary ReadTypes(string savePath) { - IIndexInfo indexInfo = new EntityTypeIndex(_name); - EntityPersistence.WriteEntities(indexInfo, EntitiesBySerial, basePath,World.SerializedTypes); + string typesPath = Path.Combine(savePath, _name, $"{_name}.tdb"); + if (!File.Exists(typesPath)) + { + return null; + } + + Type[] ctorArguments = [typeof(Serial)]; + + 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 count = dataReader.ReadInt(); + var types = new Dictionary(count); + + for (var i = 0; i < count; ++i) + { + // Legacy didn't have the null flag check + var typeName = dataReader.ReadStringRaw(); + types.Add(i, GetConstructorFor(typeName, AssemblyHandler.FindTypeByName(typeName), ctorArguments)); + } + + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + return types; } public virtual void DeserializeIndexes(string savePath, Dictionary typesDb) { - IIndexInfo indexInfo = new EntityTypeIndex(_name); + string indexPath = Path.Combine(savePath, _name, $"{_name}.idx"); + if (!File.Exists(indexPath)) + { + TryDeserializeMultithreadIndexes(savePath, typesDb); + return; + } + + _entities = [InternalDeserializeIndexes(indexPath, typesDb)]; + } + + private void TryDeserializeMultithreadIndexes(string savePath, Dictionary typesDb) + { + var index = 0; + var fileList = new List(); + while (true) + { + var path = Path.Combine(savePath, _name, $"{_name}_{index}.idx"); + var fi = new FileInfo(path); + if (!fi.Exists) + { + break; + } + + if (fi.Length != 0) + { + fileList.Add(path); + } + + index++; + } + + _entities = new List>[fileList.Count]; + for (var i = 0; i < fileList.Count; i++) + { + _entities[i] = InternalDeserializeIndexes(fileList[i], typesDb); + } + } + + private unsafe List> InternalDeserializeIndexes(string filePath, Dictionary typesDb) + { + object[] ctorArgs = new object[1]; + List> entities = []; - EntitiesBySerial = EntityPersistence.LoadIndex(savePath, indexInfo, typesDb, out _entities); + using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); + using var accessor = mmf.CreateViewStream(); + + byte* ptr = null; + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); + + var version = dataReader.ReadInt(); + + Dictionary ctors = null; + if (version < 2) + { + ctors = ReadTypes(Path.GetDirectoryName(filePath)); + } + + if (typesDb == null && ctors == null) + { + return entities; + } + + int count = dataReader.ReadInt(); + + var now = DateTime.UtcNow; + Type[] ctorArguments = [typeof(Serial)]; + + for (int i = 0; i < count; ++i) + { + ConstructorInfo ctor; + // Version 2 & 3 with SerializedTypes.db + if (version >= 2) + { + var flag = dataReader.ReadByte(); + if (flag != 2) + { + throw new Exception($"Invalid type flag, expected 2 but received {flag}."); + } + + var hash = dataReader.ReadULong(); + typesDb!.TryGetValue(hash, out var typeName); + ctor = GetConstructorFor(typeName, AssemblyHandler.FindTypeByHash(hash), ctorArguments); + } + else + { + ctor = ctors?[dataReader.ReadInt()]; + } + + Serial serial = (Serial)dataReader.ReadUInt(); + var created = version == 0 ? now : new DateTime(dataReader.ReadLong(), DateTimeKind.Utc); + if (version is > 0 and < 3) + { + dataReader.ReadLong(); // LastSerialized + } + var pos = dataReader.ReadLong(); + var length = dataReader.ReadInt(); + + if (ctor == null) + { + continue; + } + + ctorArgs[0] = serial; + + if (ctor.Invoke(ctorArgs) is T entity) + { + entity.Created = created; + entities.Add(new EntitySpan(entity, pos, (int)length)); + EntitiesBySerial[serial] = entity; + } + } + + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + entities.TrimExcess(); if (EntitiesBySerial.Count > 0) { _lastEntitySerial = EntitiesBySerial.Keys.Max(); } + + return entities; } public override void Deserialize(string savePath, Dictionary typesDb) { - IIndexInfo indexInfo = new EntityTypeIndex(_name); - EntityPersistence.LoadData(savePath, indexInfo, typesDb, _entities); + string dataPath = Path.Combine(savePath, _name, $"{_name}.bin"); + var fi = new FileInfo(dataPath); + + if (!fi.Exists) + { + TryDeserializeMultithread(savePath, typesDb); + } + else + { + if (fi.Length == 0) + { + return; + } + + InternalDeserialize(dataPath, 0, typesDb); + } + _entities = null; } - public override void PostSerialize() + private static unsafe void InternalDeserialize(string filePath, int index, Dictionary typesDb) + { + using var mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Open); + using var accessor = mmf.CreateViewStream(); + + byte* ptr = null; + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + UnmanagedDataReader dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb); + var deleteAllFailures = false; + + foreach (var entry in _entities[index]) + { + T t = entry.Entity; + + if (entry.Length == 0) + { + t?.Delete(); + continue; + } + + // Skip this entry + if (t == null) + { + dataReader.Seek(entry.Length, SeekOrigin.Current); + continue; + } + + string error; + + try + { + var pos = dataReader.Position; + t.Deserialize(dataReader); + var lengthDeserialized = dataReader.Position - pos; + + error = lengthDeserialized != entry.Length + ? $"Serialized object was {entry.Length} bytes, but {lengthDeserialized} bytes deserialized" + : null; + } + catch (Exception e) + { + error = e.ToString(); + } + + if (error != null) + { + Console.WriteLine($"***** Bad deserialize of {t.GetType()} ({t.Serial}) *****"); + Console.WriteLine(error); + + if (!deleteAllFailures) + { + Console.Write("Delete the object and continue? (y/n/a): "); + var pressedKey = Console.ReadLine(); + + if (pressedKey.InsensitiveEquals("a")) + { + deleteAllFailures = true; + } + else if (!pressedKey.InsensitiveEquals("y")) + { + throw new Exception("Deserialization failed."); + } + } + + t.Delete(); + } + } + + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + } + + private void TryDeserializeMultithread(string savePath, Dictionary typesDb) + { + if (_entities == null) + { + return; + } + + var folderPath = Path.Combine(savePath, _name); + + for (var i = 0; i < _entities.Length; i++) + { + var path = Path.Combine(folderPath, $"{_name}_{i}.bin"); + InternalDeserialize(path, i, typesDb); + } + } + + public override void PostWorldSave() { ProcessSafetyQueues(); } @@ -144,10 +527,9 @@ public void AddEntity(T entity) case WorldState.Saving: { AppendSafetyLog("add", entity); - goto case WorldState.WritingSave; + goto case WorldState.Loading; } case WorldState.Loading: - case WorldState.WritingSave: { if (_pendingDelete.Remove(entity.Serial)) { @@ -158,6 +540,7 @@ public void AddEntity(T entity) break; } case WorldState.PendingSave: + case WorldState.WritingSave: case WorldState.Running: { ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(EntitiesBySerial, entity.Serial, out bool exists); @@ -205,16 +588,16 @@ public void RemoveEntity(T entity) case WorldState.Saving: { AppendSafetyLog("delete", entity); - goto case WorldState.WritingSave; + goto case WorldState.Loading; } case WorldState.Loading: - case WorldState.WritingSave: { _pendingAdd.Remove(entity.Serial); _pendingDelete[entity.Serial] = entity; break; } case WorldState.PendingSave: + case WorldState.WritingSave: case WorldState.Running: { EntitiesBySerial.Remove(entity.Serial); @@ -286,7 +669,6 @@ public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) wh } case WorldState.Loading: case WorldState.Saving: - case WorldState.WritingSave: { if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) { @@ -302,6 +684,7 @@ public R FindEntity(Serial serial, bool returnDeleted, bool returnPending) wh return null; } case WorldState.PendingSave: + case WorldState.WritingSave: case WorldState.Running: { return EntitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index 4a193ed2d4..a584b76a3e 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: GenericPersistence.cs * * * @@ -22,35 +22,54 @@ namespace Server; public abstract class GenericPersistence : Persistence, IGenericSerializable { + private long _initialSize = 1024 * 1024; + private MemoryMapFileWriter _fileToSave; + public string Name { get; } public GenericPersistence(string name, int priority) : base(priority) => Name = name; - public override void Serialize() + public override void Preserialize(string savePath, ConcurrentQueue types) { - World.PushToCache(this); - } + var path = Path.Combine(savePath, Name); + var filePath = Path.Combine(path, $"{Name}.bin"); + PathUtility.EnsureDirectory(path); - public long SavePosition { get; set; } + _fileToSave = new MemoryMapFileWriter(new FileStream(filePath, FileMode.Create), _initialSize, types); + } - public BufferWriter SaveBuffer { get; set; } + public override void Serialize() + { + World.ResetRoundRobin(); + World.PushToCache((this, this)); + } - public void Serialize(ConcurrentQueue types) + public override void WriteSnapshot() { - SaveBuffer ??= new BufferWriter(true, types); + string folderPath = null; + using (var fs = _fileToSave.FileStream) + { + if (fs.Position > _initialSize) + { + _initialSize = fs.Position; + } + + _fileToSave.Dispose(); + if (_fileToSave.Position == 0) + { + folderPath = Path.GetDirectoryName(fs.Name); + } + } - SaveBuffer.Seek(0, SeekOrigin.Begin); - Serialize(SaveBuffer); + if (folderPath != null) + { + Directory.Delete(folderPath); + } } - public abstract void Serialize(IGenericWriter writer); + public override void Serialize(IGenericSerializable e, int threadIndex) => Serialize(_fileToSave); - public override void WriteSnapshot(string basePath) - { - string binPath = Path.Combine(basePath, Name, $"{Name}.bin"); - var buffer = SaveBuffer!.Buffer.AsSpan(0, (int)SaveBuffer.Position); - AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer); - } + public abstract void Serialize(IGenericWriter writer); public override void Deserialize(string savePath, Dictionary typesDb) => AdhocPersistence.Deserialize(Path.Combine(savePath, Name, $"{Name}.bin"), Deserialize); diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index 31f5c4a7e8..e3fe07debb 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: IGenericReader.cs * * * @@ -22,9 +22,6 @@ namespace Server; public interface IGenericReader { - // Used to determine valid Entity deserialization - DateTime LastSerialized { get; init; } - string ReadString(bool intern = false); public string ReadStringRaw(bool intern = false); diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 72cabe8c72..63ac049702 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: IGenericWriter.cs * * * @@ -24,7 +24,6 @@ namespace Server; public interface IGenericWriter { long Position { get; } - void Close(); void Write(string value); void Write(long value); void Write(ulong value); @@ -98,6 +97,7 @@ void WriteEncodedInt(int value) Write((byte)v); } + void Write(Point3D value) { Write(value.m_X); diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs index 119111a282..e44c92b2fa 100644 --- a/Projects/Server/Serialization/ISerializable.cs +++ b/Projects/Server/Serialization/ISerializable.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: ISerializable.cs * * * @@ -14,61 +14,18 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; -using System.IO; namespace Server; public interface ISerializable : IGenericSerializable { - // Should be serialized/deserialized with the index so it can be referenced by IGenericReader + // Should be serialized/deserialized with the index that way it can be referenced by IGenericReader DateTime Created { get; set; } - long SavePosition { get; protected internal set; } - BufferWriter SaveBuffer { get; protected internal set; } - Serial Serial { get; } void Deserialize(IGenericReader reader); - void Serialize(IGenericWriter writer); bool Deleted { get; } void Delete(); - - public void InitializeSaveBuffer(byte[] buffer, ConcurrentQueue types) - { - SaveBuffer = new BufferWriter(buffer, true, types); - if (World.DirtyTrackingEnabled) - { - SavePosition = SaveBuffer.Position; - } - else - { - SavePosition = -1; - } - } - - void IGenericSerializable.Serialize(ConcurrentQueue types) - { - SaveBuffer ??= new BufferWriter(true, types); - - // Clean, don't bother serializing - if (SavePosition > -1) - { - SaveBuffer.Seek(SavePosition, SeekOrigin.Begin); - return; - } - - SaveBuffer.Seek(0, SeekOrigin.Begin); - Serialize(SaveBuffer); - - if (World.DirtyTrackingEnabled) - { - SavePosition = SaveBuffer.Position; - } - else - { - this.MarkDirty(); - } - } } diff --git a/Projects/Server/Serialization/ISerializableExtensions.cs b/Projects/Server/Serialization/ISerializableExtensions.cs index b7ded7a98b..2f407a0238 100644 --- a/Projects/Server/Serialization/ISerializableExtensions.cs +++ b/Projects/Server/Serialization/ISerializableExtensions.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: ISerializableExtensions.cs * * * @@ -24,10 +24,7 @@ public static class ISerializableExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MarkDirty(this ISerializable entity) { - if (entity != null) - { - entity.SavePosition = -1; - } + // TODO: Add dirty tracking back } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Projects/Server/Serialization/MemoryMapFileWriter.cs b/Projects/Server/Serialization/MemoryMapFileWriter.cs new file mode 100644 index 0000000000..e5de0849fe --- /dev/null +++ b/Projects/Server/Serialization/MemoryMapFileWriter.cs @@ -0,0 +1,304 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: MemoryMapFileWriter.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 . * + *************************************************************************/ + +using System; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using Server.Text; + +namespace Server; + +public unsafe class MemoryMapFileWriter : IGenericWriter, IDisposable +{ + private readonly Encoding _encoding; + + private readonly ConcurrentQueue _types; + private readonly FileStream _fileStream; + private MemoryMappedFile _mmf; + private MemoryMappedViewAccessor _accessor; + private byte* _ptr; + private long _position; + private long _size; + + public MemoryMapFileWriter(FileStream fileStream, long initialSize, ConcurrentQueue types = null) + { + _types = types; + _fileStream = fileStream; + _encoding = TextEncoding.UTF8; + _size = Math.Max(initialSize, 1024); + + ResizeMemoryMappedFile(initialSize); + } + + public long Position => _position; + + public FileStream FileStream => _fileStream; + + private void ResizeMemoryMappedFile(long newSize) + { + _accessor?.SafeMemoryMappedViewHandle.ReleasePointer(); + _accessor?.Dispose(); + _mmf?.Dispose(); + + // Do the actual resizing + _fileStream.SetLength(newSize); + + _mmf = MemoryMappedFile.CreateFromFile(_fileStream, null, newSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, leaveOpen: true); + _accessor = _mmf.CreateViewAccessor(); + _accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref _ptr); + } + + private void EnsureCapacity(long bytesToWrite) + { + var shouldResize = false; + while (_position + bytesToWrite > _size) + { + // Don't double forever, eventually we want to have a maximum, like 256MB at a time or something + _size += Math.Min(_size, 1024 * 1024 * 256); + shouldResize = true; + } + + if (shouldResize) + { + ResizeMemoryMappedFile(_size); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(byte[] bytes) => Write(bytes.AsSpan()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(byte[] bytes, int offset, int count) => Write(bytes.AsSpan(offset, count)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan bytes) + { + var byteCount = bytes.Length; + EnsureCapacity(byteCount); + + bytes.CopyTo(new Span(_ptr + _position, byteCount)); + _position += byteCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + { + if (offset > _size) + { + EnsureCapacity(offset); + } + + _position = offset; + break; + } + case SeekOrigin.Current: + { + EnsureCapacity(offset); + _position += offset; + break; + } + case SeekOrigin.End: + { + if (_position + offset > _size) + { + EnsureCapacity(offset); + } + + _position = _size + offset; + + if (_position < 0) + { + Dispose(); + throw new InvalidOperationException("Seek before start of file"); + } + break; + } + } + + return _position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(string value) + { + if (value == null) + { + Write(false); + } + else + { + Write(true); + WriteStringRaw(value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(long value) + { + EnsureCapacity(sizeof(long)); + BinaryPrimitives.WriteInt64LittleEndian(new Span(_ptr + _position, sizeof(long)), value); + _position += sizeof(long); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong value) + { + EnsureCapacity(sizeof(ulong)); + BinaryPrimitives.WriteUInt64LittleEndian(new Span(_ptr + _position, sizeof(ulong)), value); + _position += sizeof(ulong); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(int value) + { + EnsureCapacity(sizeof(int)); + BinaryPrimitives.WriteInt32LittleEndian(new Span(_ptr + _position, sizeof(int)), value); + _position += sizeof(int); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(uint value) + { + EnsureCapacity(sizeof(uint)); + BinaryPrimitives.WriteUInt32LittleEndian(new Span(_ptr + _position, sizeof(uint)), value); + _position += sizeof(uint); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(short value) + { + EnsureCapacity(sizeof(short)); + BinaryPrimitives.WriteInt16LittleEndian(new Span(_ptr + _position, sizeof(short)), value); + _position += sizeof(short); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ushort value) + { + EnsureCapacity(sizeof(ushort)); + BinaryPrimitives.WriteUInt16LittleEndian(new Span(_ptr + _position, sizeof(ushort)), value); + _position += sizeof(ushort); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(double value) + { + EnsureCapacity(sizeof(double)); + BinaryPrimitives.WriteDoubleLittleEndian(new Span(_ptr + _position, sizeof(double)), value); + _position += sizeof(double); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(float value) + { + EnsureCapacity(sizeof(float)); + BinaryPrimitives.WriteSingleLittleEndian(new Span(_ptr + _position, sizeof(float)), value); + _position += sizeof(float); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(byte value) + { + EnsureCapacity(1); + *(_ptr + _position++) = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(sbyte value) => Write((byte)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(bool value) => Write(*(byte*)&value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(Serial serial) => Write(serial.Value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(Type type) + { + if (type == null) + { + Write((byte)0); + } + else + { + Write((byte)0x2); // xxHash3 64bit + Write(AssemblyHandler.GetTypeHash(type)); + _types.Enqueue(type); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(decimal value) + { + Span buffer = stackalloc int[sizeof(decimal) / 4]; + decimal.GetBits(value, buffer); + + Write(MemoryMarshal.Cast(buffer)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteStringRaw(ReadOnlySpan value) + { + var length = _encoding.GetByteCount(value); + + EnsureCapacity(length + 5); + + // WriteEncodedInt + var v = (uint)length; + + while (v >= 0x80) + { + *(_ptr + _position++) = (byte)(v | 0x80); + v >>= 7; + } + *(_ptr + _position++) = (byte)v; + + _encoding.GetBytes(value, new Span(_ptr + _position, length)); + _position += length; + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _accessor.SafeMemoryMappedViewHandle.ReleasePointer(); + _accessor.Dispose(); + _mmf.Dispose(); + + // Truncate the file + _fileStream.SetLength(_position); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~MemoryMapFileWriter() + { + Dispose(false); + } +} diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index 2f99d20269..d98f6551c7 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Persistence.cs * * * @@ -17,6 +17,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.MemoryMappedFiles; namespace Server; @@ -52,99 +53,122 @@ public static void Load(string path) } } - private static Dictionary LoadTypes(string path) + private unsafe static Dictionary LoadTypes(string path) { var db = new Dictionary(); - string tdbPath = Path.Combine(path, "SerializedTypes.db"); - if (!File.Exists(tdbPath)) + string typesPath = Path.Combine(path, "SerializedTypes.db"); + if (!File.Exists(typesPath)) { return db; } - using FileStream tdb = new FileStream(tdbPath, FileMode.Open, FileAccess.Read, FileShare.Read); - BinaryReader tdbReader = new BinaryReader(tdb); + using var mmf = MemoryMappedFile.CreateFromFile(typesPath, FileMode.Open); + using var accessor = mmf.CreateViewStream(); - var version = tdbReader.ReadInt32(); - var count = tdbReader.ReadInt32(); + 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 = tdbReader.ReadUInt64(); - var typeName = tdbReader.ReadString(); + var hash = dataReader.ReadULong(); + var typeName = dataReader.ReadStringRaw(); db[hash] = typeName; } + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); return db; } - internal static void SerializeAll() + // Note: This is strictly on a background thread + internal static void PreSerializeAll(string path, ConcurrentQueue types) { foreach (var p in _registry) { - p.Serialize(); + p.Preserialize(path, types); } } - internal static void PostSerializeAll() + private static readonly HashSet _typesSet = []; + + // Note: This is strictly on a background thread + internal static void WriteSnapshotAll(string path, ConcurrentQueue types) { foreach (var p in _registry) { - p.PostSerialize(); + p.WriteSnapshot(); + } + + // Dedupe the queue. + foreach (var type in types) + { + _typesSet.Add(type); } + + WriteSerializedTypesSnapshot(path, _typesSet); + _typesSet.Clear(); } - internal static void PostDeserializeAll() + internal static void SerializeAll() { foreach (var p in _registry) { - p.PostDeserialize(); + p.Serialize(); } } - public static void WriteSnapshot(string path, ConcurrentQueue types) + internal static void PostWorldSaveAll() { - foreach (var entry in _registry) + foreach (var p in _registry) { - entry.WriteSnapshot(path); + p.PostWorldSave(); } + } - // Dedupe the queue. - foreach (var type in types) + internal static void PostDeserializeAll() + { + foreach (var p in _registry) { - _typesSet.Add(type); + p.PostDeserialize(); } - - WriteSerializedTypesSnapshot(path, _typesSet); - _typesSet.Clear(); } - private static HashSet _typesSet = new(); - public static void WriteSerializedTypesSnapshot(string path, HashSet types) { - string tdbPath = Path.Combine(path, "SerializedTypes.db"); - using var tdb = new BinaryFileWriter(tdbPath, false); + string typesPath = Path.Combine(path, "SerializedTypes.db"); + using var fs = new FileStream(typesPath, FileMode.Create); + using var writer = new MemoryMapFileWriter(fs, 1024 * 1024 * 4); - tdb.Write(0); // version - tdb.Write(types.Count); + writer.Write(0); // version + writer.Write(types.Count); foreach (var type in types) { var fullName = type.FullName; - tdb.Write(HashUtility.ComputeHash64(fullName)); - tdb.Write(fullName); + writer.Write(HashUtility.ComputeHash64(fullName)); + writer.WriteStringRaw(fullName); } } - // Serializes to memory buffers and run in parallel - public abstract void Serialize(); + // Open file streams, MMFs, prepare data structures + // Note: This should only be run on a background thread + public abstract void Preserialize(string savePath, ConcurrentQueue types); + + // Note: This should only be run on a background thread + public abstract void Serialize(IGenericSerializable e, int threadIndex); - public abstract void WriteSnapshot(string savePath); + // Note: This should only be run on a background thread + public abstract void WriteSnapshot(); + + public abstract void Serialize(); public abstract void Deserialize(string savePath, Dictionary typesDb); - public virtual void PostSerialize() + public virtual void PostWorldSave() { } diff --git a/Projects/Server/Serialization/UnmanagedDataReader.cs b/Projects/Server/Serialization/UnmanagedDataReader.cs new file mode 100644 index 0000000000..ed9675abb5 --- /dev/null +++ b/Projects/Server/Serialization/UnmanagedDataReader.cs @@ -0,0 +1,246 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: UnmanagedDataReader.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 . * + *************************************************************************/ + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using Server.Logging; +using Server.Text; + +namespace Server; + +public unsafe class UnmanagedDataReader : IGenericReader +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(UnmanagedDataReader)); + + private readonly byte* _ptr; + private long _position; + private readonly long _size; + + private readonly Dictionary _typesDb; + private readonly Encoding _encoding; + + public long Position => _position; + + public UnmanagedDataReader(byte* ptr, long size, Dictionary typesDb = null, Encoding encoding = null) + { + _encoding = encoding ?? TextEncoding.UTF8; + _typesDb = typesDb; + _ptr = ptr; + _size = size; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(bool intern = false) => ReadBool() ? ReadStringRaw(intern) : null; + + public string ReadStringRaw(bool intern = false) + { + // ReadEncodedInt + int length = 0, shift = 0; + byte b; + + do + { + b = *(_ptr + _position++); + length |= (b & 0x7F) << shift; + shift += 7; + } + while (b >= 0x80); + + if (length <= 0) + { + return "".Intern(); + } + + var str = TextEncoding.GetString(new ReadOnlySpan(_ptr + _position, length), _encoding); + _position += length; + return intern ? str.Intern() : str; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadLong() + { + var v = BinaryPrimitives.ReadInt64LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(long))); + _position += sizeof(long); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadULong() + { + var v = BinaryPrimitives.ReadUInt64LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(ulong))); + _position += sizeof(ulong); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt() + { + var v = BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(int))); + _position += sizeof(int); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt() + { + var v = BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(uint))); + _position += sizeof(uint); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadShort() + { + var v = BinaryPrimitives.ReadInt16LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(short))); + _position += sizeof(short); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUShort() + { + var v = BinaryPrimitives.ReadUInt16LittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(ushort))); + _position += sizeof(ushort); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double ReadDouble() + { + var v = BinaryPrimitives.ReadDoubleLittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(double))); + _position += sizeof(double); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float ReadFloat() + { + var v = BinaryPrimitives.ReadSingleLittleEndian(new ReadOnlySpan(_ptr + _position, sizeof(float))); + _position += sizeof(float); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() => *(_ptr + _position++); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sbyte ReadSByte() => (sbyte)ReadByte(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ReadBool() => ReadByte() != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Serial ReadSerial() => (Serial)ReadUInt(); + + public Type ReadType() => + ReadByte() switch + { + 0 => null, + 1 => AssemblyHandler.FindTypeByFullName(ReadStringRaw()), // Backward compatibility + 2 => ReadTypeByHash() + }; + + public Type ReadTypeByHash() + { + var hash = ReadULong(); + var t = AssemblyHandler.FindTypeByHash(hash); + + if (t != null) + { + return t; + } + + if (_typesDb == null) + { + logger.Error( + new Exception($"The file SerializedTypes.db was not loaded. Type hash '{hash}' could not be found."), + "Invalid {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + if (!_typesDb.TryGetValue(hash, out var typeName)) + { + logger.Error( + new Exception($"Type hash '{hash}' is not present in the serialized types database."), + "Invalid type hash {Hash} at position {Position}", + hash, + Position + ); + + return null; + } + + t = AssemblyHandler.FindTypeByFullName(typeName, false); + + if (t == null) + { + logger.Error( + new Exception($"Type '{typeName}' was not found."), + "Type {Type} was not found.", + typeName + ); + } + + return t; + } + + public int Read(Span buffer) + { + var length = buffer.Length; + if (length > _size - _position) + { + throw new OutOfMemoryException(); + } + + new ReadOnlySpan(_ptr + _position, length).CopyTo(buffer); + _position += length; + return length; + } + + public virtual long Seek(long offset, SeekOrigin origin) + { + Debug.Assert( + origin != SeekOrigin.End || offset <= 0 && offset > _size, + "Attempting to seek to an invalid position using SeekOrigin.End" + ); + Debug.Assert( + origin != SeekOrigin.Begin || offset >= 0 && offset < _size, + "Attempting to seek to an invalid position using SeekOrigin.Begin" + ); + Debug.Assert( + origin != SeekOrigin.Current || _position + offset >= 0 && _position + offset < _size, + "Attempting to seek to an invalid position using SeekOrigin.Current" + ); + + var position = Math.Max(0L, origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _size + offset, + _ => offset // Begin + }); + + _position = position; + return _position; + } +} diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 4bb9a5fdb3..fc974e96c2 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -39,7 +39,7 @@ - + diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs deleted file mode 100644 index 4656d8c73a..0000000000 --- a/Projects/Server/World/EntityPersistence.cs +++ /dev/null @@ -1,330 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: EntityPersistence.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 . * - *************************************************************************/ - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.IO.MemoryMappedFiles; -using System.Reflection; - -namespace Server; - -public static class EntityPersistence -{ - public static void WriteEntities( - IIndexInfo indexInfo, - Dictionary entities, - string savePath, - ConcurrentQueue types - ) where T : class, ISerializable - { - var typeName = indexInfo.TypeName; - - var path = Path.Combine(savePath, typeName); - - PathUtility.EnsureDirectory(path); - - string idxPath = Path.Combine(path, $"{typeName}.idx"); - string binPath = Path.Combine(path, $"{typeName}.bin"); - - using var idx = new BinaryFileWriter(idxPath, false, types); - using var bin = new BinaryFileWriter(binPath, true, types); - - idx.Write(3); // Version - idx.Write(entities.Count); - foreach (var e in entities.Values) - { - long start = bin.Position; - - var t = e.GetType(); - idx.Write(t); - idx.Write(e.Serial); - idx.Write(e.Created.Ticks); - idx.Write(start); - - e.SerializeTo(bin); - - idx.Write((int)(bin.Position - start)); - } - } - - public static Dictionary LoadIndex( - string path, - IIndexInfo indexInfo, - Dictionary serializedTypes, - out List> entities - ) where T : class, ISerializable - { - var map = new Dictionary(); - object[] ctorArgs = new object[1]; - - var indexType = indexInfo.TypeName; - - string indexPath = Path.Combine(path, indexType, $"{indexType}.idx"); - - entities = new List>(); - - if (!File.Exists(indexPath)) - { - return map; - } - - using FileStream idx = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read); - BinaryReader idxReader = new BinaryReader(idx); - - var version = idxReader.ReadInt32(); - int count = idxReader.ReadInt32(); - - var ctorArguments = new[] { typeof(I) }; - List types; - - string typesPath = Path.Combine(path, indexType, $"{indexType}.tdb"); - if (File.Exists(typesPath)) - { - using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read); - BinaryReader tdbReader = new BinaryReader(tdb); - types = ReadTypes(tdbReader, ctorArguments); - tdbReader.Close(); - } - else - { - types = null; - } - - // We must have a typeDb from SerializedTypes.db, or a tdb file - if (serializedTypes == null && types == null) - { - return map; - } - - var now = DateTime.UtcNow; - - for (int i = 0; i < count; ++i) - { - ConstructorInfo ctor; - if (version >= 2) - { - var flag = idxReader.ReadByte(); - if (flag != 2) - { - throw new Exception($"Invalid type flag, expected 2 but received {flag}."); - } - - var hash = idxReader.ReadUInt64(); - serializedTypes!.TryGetValue(hash, out var typeName); - ctor = GetConstructorFor(typeName, AssemblyHandler.FindTypeByHash(hash), ctorArguments); - } - else - { - ctor = types?[idxReader.ReadInt32()]; - } - - var serial = idxReader.ReadUInt32(); - var created = version == 0 ? now : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc); - if (version is > 0 and < 3) - { - idxReader.ReadInt64(); // LastSerialized - } - var pos = idxReader.ReadInt64(); - var length = idxReader.ReadInt32(); - - if (ctor == null) - { - continue; - } - - I indexer = indexInfo.CreateIndex(serial); - - ctorArgs[0] = indexer; - - if (ctor.Invoke(ctorArgs) is T entity) - { - entity.Created = created; - entities.Add(new EntitySpan(entity, pos, length)); - map[indexer] = entity; - } - } - - idxReader.Close(); - entities.TrimExcess(); - - return map; - } - - public static void LoadData( - string path, - IIndexInfo indexInfo, - Dictionary serializedTypes, - List> entities - ) where T : class, ISerializable - { - var indexType = indexInfo.TypeName; - - string dataPath = Path.Combine(path, indexType, $"{indexType}.bin"); - - if (!File.Exists(dataPath) || new FileInfo(dataPath).Length == 0) - { - return; - } - - using var mmf = MemoryMappedFile.CreateFromFile(dataPath, FileMode.Open); - using var stream = mmf.CreateViewStream(); - BufferReader br = null; - - var deleteAllFailures = false; - - foreach (var entry in entities) - { - T t = entry.Entity; - - var position = entry.Position; - stream.Seek(position, SeekOrigin.Begin); - - // Skip this entry - if (t == null) - { - continue; - } - - if (entry.Length == 0) - { - t.Delete(); - continue; - } - - var buffer = GC.AllocateUninitializedArray(entry.Length); - if (br == null) - { - br = new BufferReader(buffer, serializedTypes); - } - else - { - br.Reset(buffer, out _); - } - - stream.Read(buffer.AsSpan()); - string error; - - try - { - t.Deserialize(br); - - error = br.Position != entry.Length - ? $"Serialized object was {entry.Length} bytes, but {br.Position} bytes deserialized" - : null; - } - catch (Exception e) - { - error = e.ToString(); - } - - if (error == null) - { - t.InitializeSaveBuffer(buffer, World.SerializedTypes); - } - else - { - Console.WriteLine($"***** Bad deserialize of {t.GetType()} ({t.Serial}) *****"); - Console.WriteLine(error); - - if (!deleteAllFailures) - { - Console.Write("Delete the object and continue? (y/n/a): "); - var pressedKey = Console.ReadLine(); - - if (pressedKey.InsensitiveEquals("a")) - { - deleteAllFailures = true; - } - else if (!pressedKey.InsensitiveEquals("y")) - { - throw new Exception("Deserialization failed."); - } - } - - t.Delete(); - } - } - } - - private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes) - { - if (t?.IsAbstract != false) - { - Console.WriteLine("failed"); - - var issue = t?.IsAbstract == true ? "marked abstract" : "not found"; - - Console.Write($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n): "); - - if (Console.ReadLine().InsensitiveEquals("y")) - { - Console.WriteLine("Loading..."); - return null; - } - - Console.WriteLine("Types will not be deleted. An exception will be thrown."); - - throw new Exception($"Bad type '{typeName}'"); - } - - var ctor = t.GetConstructor(constructorTypes); - - if (ctor == null) - { - throw new Exception($"Type '{t}' does not have a serialization constructor"); - } - - return ctor; - } - - /** - * Legacy ReadTypes for backward compatibility with old saves that still have a tdb file - */ - private static List ReadTypes(BinaryReader tdbReader, Type[] ctorArguments) - { - var count = tdbReader.ReadInt32(); - - var types = new List(count); - - for (var i = 0; i < count; ++i) - { - var typeName = tdbReader.ReadString(); - var t = AssemblyHandler.FindTypeByFullName(typeName, false); - var ctor = GetConstructorFor(typeName, t, ctorArguments); - types.Add(ctor); - } - - return types; - } - - private static void SerializeTo(this ISerializable entity, IGenericWriter writer) - { - var saveBuffer = entity.SaveBuffer; - - // If nothing was serialized we expect the object to be deleted on deserialization - if (saveBuffer.Position == 0) - { - return; - } - - // Resize to the exact size - saveBuffer.Resize((int)saveBuffer.Position); - - // Write that amount - writer.Write(saveBuffer.Buffer); - } -} diff --git a/Projects/Server/World/IGenericSerializable.cs b/Projects/Server/World/IGenericSerializable.cs index 40b5c24bc4..02e6487afd 100644 --- a/Projects/Server/World/IGenericSerializable.cs +++ b/Projects/Server/World/IGenericSerializable.cs @@ -1,8 +1,8 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * - * File: IWorldSerializable.cs * + * File: IGenericSerializable.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 * @@ -13,12 +13,9 @@ * along with this program. If not, see . * *************************************************************************/ -using System; -using System.Collections.Concurrent; - namespace Server; public interface IGenericSerializable { - void Serialize(ConcurrentQueue types); + void Serialize(IGenericWriter writer); } diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index c50b46764b..567c27955e 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: World.cs * * * @@ -38,7 +38,7 @@ public enum WorldState public static class World { - private static ILogger logger = LogFactory.GetLogger(typeof(World)); + private static readonly ILogger logger = LogFactory.GetLogger(typeof(World)); private static readonly ItemPersistence _itemPersistence = new(); private static readonly MobilePersistence _mobilePersistence = new(); @@ -185,68 +185,10 @@ public static void Load() // Create the serialization threads. for (var i = 0; i < _threadWorkers.Length; i++) { - _threadWorkers[i] = new SerializationThreadWorker(); + _threadWorkers[i] = new SerializationThreadWorker(i); } } - private static void FinishWorldSave() - { - WorldState = WorldState.Running; - - Persistence.PostSerializeAll(); // Process safety queues - } - - public static void WriteFiles(object state) - { - Exception exception = null; - - var tempPath = PathUtility.EnsureRandomPath(_tempSavePath); - - try - { - var watch = Stopwatch.StartNew(); - logger.Information("Writing world save snapshot"); - - Persistence.WriteSnapshot(tempPath, SerializedTypes); - - watch.Stop(); - - logger.Information("Writing world save snapshot {Status} ({Duration:F2} seconds)", "done", watch.Elapsed.TotalSeconds); - } - catch (Exception ex) - { - exception = ex; - } - - if (exception != null) - { - logger.Error(exception, "Writing world save snapshot {Status}.", "failed"); - Persistence.TraceException(exception); - - BroadcastStaff(0x35, true, "Writing world save snapshot failed."); - } - else - { - try - { - EventSink.InvokeWorldSavePostSnapshot(SavePath, tempPath); - PathUtility.MoveDirectoryContents(tempPath, SavePath); - Directory.SetLastWriteTimeUtc(SavePath, Core.Now); - } - catch (Exception ex) - { - Persistence.TraceException(ex); - } - } - - // Clear types - SerializedTypes.Clear(); - - _diskWriteHandle.Set(); - - Core.LoopContext.Post(FinishWorldSave); - } - private static void ProcessDecay() { while (_decayQueue.TryDequeue(out var item)) @@ -301,28 +243,39 @@ public static void Save() return; } - WaitForWriteCompletion(); // Blocks Save until current disk flush is done. + WorldState = WorldState.PendingSave; + ThreadPool.QueueUserWorkItem(Preserialize); + } - _diskWriteHandle.Reset(); + internal static void Preserialize(object state) + { + var tempPath = PathUtility.EnsureRandomPath(_tempSavePath); - // Start our serialization threads - for (var i = 0; i < _threadWorkers.Length; i++) + try { - _threadWorkers[i].Wake(); + Persistence.PreSerializeAll(tempPath, SerializedTypes); + Core.RequestSnapshot(tempPath); } + catch (Exception ex) + { + logger.Error(ex, "Preparing to save world {Status}", "failed"); + Persistence.TraceException(ex); - WorldState = WorldState.PendingSave; - - Core.RequestSnapshot(); + BroadcastStaff(0x35, true, "Preparing for a world save failed! Check the logs!"); + } } - internal static TimeSpan Snapshot() + internal static void Snapshot(string snapshotPath) { if (WorldState != WorldState.PendingSave) { - return TimeSpan.Zero; + return; } + WaitForWriteCompletion(); // Blocks Save until current disk flush is done. + + _diskWriteHandle.Reset(); + WorldState = WorldState.Saving; Broadcast(0x35, true, "The world is saving, please wait."); @@ -336,14 +289,10 @@ internal static TimeSpan Snapshot() try { _serializationStart = Core.Now; - Persistence.SerializeAll(); - - // Pause the workers - foreach (var worker in _threadWorkers) - { - worker.Sleep(); - } + WakeSerializationThreads(); + Persistence.SerializeAll(); + PauseSerializationThreads(); EventSink.InvokeWorldSave(); } catch (Exception ex) @@ -351,7 +300,10 @@ internal static TimeSpan Snapshot() exception = ex; } - WorldState = WorldState.WritingSave; + WorldState = WorldState.PendingSave; + ThreadPool.QueueUserWorkItem(WriteFiles, snapshotPath); + Persistence.PostWorldSaveAll(); // Process safety queues + watch.Stop(); if (exception == null) { @@ -367,18 +319,74 @@ internal static TimeSpan Snapshot() BroadcastStaff(0x35, true, "World save failed! Check the logs!"); } + } - ThreadPool.QueueUserWorkItem(WriteFiles); + private static void WriteFiles(object state) + { + var snapshotPath = (string)state; + try + { + var watch = Stopwatch.StartNew(); + logger.Information("Writing world save snapshot"); + Persistence.WriteSnapshotAll(snapshotPath, SerializedTypes); - watch.Stop(); + try + { + EventSink.InvokeWorldSavePostSnapshot(SavePath, snapshotPath); + PathUtility.MoveDirectoryContents(snapshotPath, SavePath); + Directory.SetLastWriteTimeUtc(SavePath, Core.Now); + } + catch (Exception ex) + { + Persistence.TraceException(ex); + } - return watch.Elapsed; + watch.Stop(); + logger.Information("Writing world save snapshot {Status} ({Duration:F2} seconds)", "done", watch.Elapsed.TotalSeconds); + } + catch (Exception ex) + { + logger.Error(ex, "Writing world save snapshot failed"); + Persistence.TraceException(ex); + + BroadcastStaff(0x35, true, "Writing world save snapshot failed! Check the logs!"); + } + + // Clear types + SerializedTypes.Clear(); + + _diskWriteHandle.Set(); + WorldState = WorldState.Running; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WakeSerializationThreads() + { + for (var i = 0; i < _threadWorkers.Length; i++) + { + _threadWorkers[i].Wake(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void PushToCache(IGenericSerializable e) + internal static void PauseSerializationThreads() { - _threadWorkers[_threadId++].Push(e); + for (var i = 0; i < _threadWorkers.Length; i++) + { + _threadWorkers[i].Sleep(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int GetThreadWorkerCount() => Math.Max(Environment.ProcessorCount - 1, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ResetRoundRobin() => _threadId = 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void PushToCache((IGenericSerializable e, Persistence p) ep) + { + _threadWorkers[_threadId++].Push(ep); if (_threadId == _threadWorkers.Length) { _threadId = 0; @@ -488,15 +496,15 @@ public override void Serialize() EnqueueForDecay(item); } - PushToCache(item); + PushToCache((item, this)); } } - public override void PostSerialize() + public override void PostWorldSave() { ProcessDecay(); // Run this before the safety queue - base.PostSerialize(); + base.PostWorldSave(); } } @@ -522,18 +530,20 @@ public override void PostDeserialize() 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 _entities; + private readonly ConcurrentQueue<(IGenericSerializable, Persistence)> _entities; - public SerializationThreadWorker() + public SerializationThreadWorker(int index) { + _index = index; _startEvent = new AutoResetEvent(false); _stopEvent = new AutoResetEvent(false); - _entities = new ConcurrentQueue(); + _entities = new ConcurrentQueue<(IGenericSerializable, Persistence)>(); _thread = new Thread(Execute); _thread.Start(this); } @@ -556,14 +566,11 @@ public void Exit() Sleep(); } - public void Push(IGenericSerializable entity) - { - _entities.Enqueue(entity); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Push((IGenericSerializable e, Persistence p) ep) => _entities.Enqueue(ep); private static void Execute(object obj) { - var serializedTypes = SerializedTypes; SerializationThreadWorker worker = (SerializationThreadWorker)obj; var reader = worker._entities; @@ -573,9 +580,10 @@ private static void Execute(object obj) while (true) { bool pauseRequested = Volatile.Read(ref worker._pause); - if (reader.TryDequeue(out var entity)) + if (reader.TryDequeue(out var ep)) { - entity.Serialize(serializedTypes); + var (e, p) = ep; + p.Serialize(e, worker._index); } else if (pauseRequested) // Break when finished { diff --git a/Projects/UOContent/Misc/CrashGuard.cs b/Projects/UOContent/Misc/CrashGuard.cs index 5273ae0708..b8c291bc8b 100644 --- a/Projects/UOContent/Misc/CrashGuard.cs +++ b/Projects/UOContent/Misc/CrashGuard.cs @@ -39,8 +39,6 @@ public static void CrashGuard_OnCrash(ServerCrashedEventArgs e) GenerateCrashReport(e); } - World.WaitForWriteCompletion(); - if (SaveBackup) { Backup(); @@ -51,6 +49,10 @@ public static void CrashGuard_OnCrash(ServerCrashedEventArgs e) e.Close = true; Core.Kill(true); } + else + { + World.WaitForWriteCompletion(); + } } private static void SendEmail(string filePath) diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index cc463c0bd6..7d932b3417 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -45,7 +45,7 @@ - +