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 @@
-
+