fix: Adds ReadType/Write(Type) and improves type referencing (#1172)

## Changes
* Improves type hashing by introducing xxHash3 (64bit)
* Removes individual `tdb` files in favor of a single `SerializedTypes.db` file. This file is only used to identify a type that is being deserialized, which doesn't exist.
* Adds duplicate type alias detection
* Adds `AssemblyHandler.FindTypeByHash`

View changed files whitespaces: https://github.com/modernuo/ModernUO/pull/1172/files?diff=split&w=1

## SerializedTypes.db
The serialized types file is used to get back the original name of a type in case it no longer exists in code. This can easily be necessary if a class is renamed in code and no `TypeAlias` is provided.

### Format
byte[4] - version
byte[4] - count
--array--
byte[8] - xxHash
byte[1] - flag, 0 - null, 1 - not null
byte[n] - Full class name in UTF8

### Example
<img width="472" alt="SerializedTypes_Example" src="https://user-images.githubusercontent.com/3953314/195255429-31d24293-6bd1-419e-811b-07874dd0f78d.png">

## Benchmarks
Serialized 500 Type fields. The 8192bytes comes from the _ConcurrentQueue_ that would later be used for SerializedTypes.
Note that the queue is never cleared, so it's size grew considerably.
```cs
|               Method |     Mean |    Error |   StdDev | Allocated |
|--------------------- |---------:|---------:|---------:|----------:|
|      BenchmarkXXHash | 18.44 us | 0.278 us | 0.260 us |    8192 B |
| BenchmarkTypeStrings | 25.09 us | 0.292 us | 0.259 us |         - |
```

TODO:
* Add support in the Serialization Generator for `ReadType()` and `Write(Type)`
* Remove `SetTypeRef` from Serialization Generator
This commit is contained in:
Kamron Batman 2022-10-11 22:17:22 -07:00 committed by GitHub
parent f268d5d4e2
commit e1e30998ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 616 additions and 291 deletions

View file

@ -14,12 +14,14 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Server.Logging;
namespace Server;
@ -27,6 +29,7 @@ public static class AssemblyHandler
{
private static readonly Dictionary<Assembly, TypeCache> m_TypeCaches = new();
private static TypeCache m_NullCache;
public static Assembly[] Assemblies { get; set; }
internal static Assembly AssemblyResolver(object sender, ResolveEventArgs args)
@ -66,6 +69,8 @@ public static class AssemblyHandler
EnsureAssemblyDirectories();
var assemblyDirectories = ServerConfiguration.AssemblyDirectories;
Assembly assembly = null;
foreach (var assemblyDir in assemblyDirectories)
{
var assemblyPath = PathUtility.GetFullPath(Path.Combine(assemblyDir, fileName), Core.BaseDirectory);
@ -74,12 +79,17 @@ public static class AssemblyHandler
var assemblyNameCheck = AssemblyName.GetAssemblyName(assemblyPath);
if (assemblyNameCheck.FullName == fullName)
{
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
break;
}
}
}
return null;
// This forces the type caching to be generated.
// We need this for world loading to find types by hash.
GetTypeCache(assembly);
return assembly;
}
public static Assembly LoadAssemblyByFileName(string assemblyFile)
@ -154,6 +164,11 @@ public static class AssemblyHandler
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong GetTypeHash(Type type) => GetTypeHash(type.FullName);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong GetTypeHash(string key) => key == null ? 0 : HashUtility.ComputeHash64(key);
public static TypeCache GetTypeCache(Assembly asm)
{
@ -195,84 +210,120 @@ public static class AssemblyHandler
return null;
}
public static Type FindTypeByHash(ulong hash)
{
for (var i = 0; i < Assemblies.Length; i++)
{
foreach (var type in GetTypeCache(Assemblies[i]).GetTypesByHash(hash, true, false))
{
return type;
}
}
foreach(var type in GetTypeCache(Core.Assembly).GetTypesByHash(hash, true, false))
{
return type;
}
return null;
}
}
public class TypeCache
{
private readonly Dictionary<string, int[]> _nameMap = new();
private readonly Dictionary<string, int[]> _nameMapInsensitive = new();
private readonly Dictionary<string, int[]> _fullNameMap = new();
private readonly Dictionary<string, int[]> _fullNameMapInsensitive = new();
private static ILogger logger = LogFactory.GetLogger(typeof(TypeCache));
private Dictionary<ulong, Type[]> _nameMap = new();
private Dictionary<ulong, Type[]> _nameMapInsensitive = new();
private Dictionary<ulong, Type[]> _fullNameMap = new();
private Dictionary<ulong, Type[]> _fullNameMapInsensitive = new();
public TypeCache(Assembly asm)
{
Types = asm?.GetTypes() ?? Type.EmptyTypes;
var nameMap = new Dictionary<string, HashSet<int>>();
var nameMapInsensitive = new Dictionary<string, HashSet<int>>();
var fullNameMap = new Dictionary<string, HashSet<int>>();
var fullNameMapInsensitive = new Dictionary<string, HashSet<int>>();
var nameMap = new Dictionary<string, HashSet<Type>>();
var nameMapInsensitive = new Dictionary<string, HashSet<Type>>();
var fullNameMap = new Dictionary<string, HashSet<Type>>();
var fullNameMapInsensitive = new Dictionary<string, HashSet<Type>>();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void addTypeToRefs(int index, string fullTypeName)
void addTypeToRefs(Type type, string typeName, string fullTypeName)
{
var typeName = fullTypeName[(fullTypeName.LastIndexOf('.') + 1)..];
AddToRefs(index, typeName, nameMap);
AddToRefs(index, typeName.ToLower(), nameMapInsensitive);
AddToRefs(index, fullTypeName, fullNameMap);
AddToRefs(index, fullTypeName.ToLower(), fullNameMapInsensitive);
AddToRefs(type, typeName, nameMap);
AddToRefs(type, typeName.ToLower(), nameMapInsensitive);
AddToRefs(type, fullTypeName, fullNameMap);
AddToRefs(type, fullTypeName.ToLower(), fullNameMapInsensitive);
}
var aliasType = typeof(TypeAliasAttribute);
for (var i = 0; i < Types.Length; i++)
{
var current = Types[i];
addTypeToRefs(i, current.FullName);
addTypeToRefs(current, current.Name, current.FullName ?? "");
if (current.GetCustomAttribute(aliasType, false) is TypeAliasAttribute alias)
{
for (var j = 0; j < alias.Aliases.Length; j++)
{
addTypeToRefs(i, alias.Aliases[j]);
var fullTypeName = alias.Aliases[j];
var typeName = fullTypeName[(fullTypeName.LastIndexOf('.')+1)..];
addTypeToRefs(current, typeName, fullTypeName);
}
}
}
foreach (var (key, value) in nameMap)
{
_nameMap[key] = value.ToArray();
_nameMap[HashUtility.ComputeHash64(key)] = value.ToArray();
}
foreach (var (key, value) in nameMapInsensitive)
{
_nameMapInsensitive[key] = value.ToArray();
_nameMapInsensitive[HashUtility.ComputeHash64(key)] = value.ToArray();
}
foreach (var (key, value) in fullNameMap)
{
_fullNameMap[key] = value.ToArray();
var values = value.ToArray();
_fullNameMap[HashUtility.ComputeHash64(key)] = value.ToArray();
#if DEBUG
if (values.Length > 1)
{
for (var i = 0; i < values.Length; i++)
{
var type = values[i];
logger.Warning(
"Duplicate type {Type1} for {Name}.",
type,
key
);
}
}
#endif
}
foreach (var (key, value) in fullNameMapInsensitive)
{
_fullNameMapInsensitive[key] = value.ToArray();
_fullNameMapInsensitive[HashUtility.ComputeHash64(key)] = value.ToArray();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddToRefs(int index, string key, Dictionary<string, HashSet<int>> map)
private static void AddToRefs(Type type, string key, Dictionary<string, HashSet<Type>> map)
{
if (key == null)
if (string.IsNullOrEmpty(key))
{
return;
}
if (map.TryGetValue(key, out var refs))
{
refs.Add(index);
refs.Add(type);
}
else
{
refs = new HashSet<int> { index };
refs = new HashSet<Type> { type };
map.Add(key, refs);
}
}
@ -280,64 +331,50 @@ public class TypeCache
public Type[] Types { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeEnumerable GetTypesByName(string name, bool full, bool ignoreCase) => new(name, this, full, ignoreCase);
public TypeEnumerator GetTypesByName(string name, bool full, bool ignoreCase) => new(name, this, full, ignoreCase);
public ref struct TypeEnumerable
{
private readonly TypeCache _cache;
private readonly string _name;
private readonly bool _ignoreCase;
private readonly bool _full;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeEnumerable(string name, TypeCache cache, bool full, bool ignoreCase)
{
_name = name;
_cache = cache;
_ignoreCase = ignoreCase;
_full = full;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeEnumerator GetEnumerator() => new(_name, _cache, _full, _ignoreCase);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeEnumerator GetTypesByHash(ulong hash, bool full, bool ignoreCase) => new(hash, this, full, ignoreCase);
public ref struct TypeEnumerator
{
private readonly TypeCache _cache;
private readonly int[] _values;
private Type[] _values;
private int _index;
private Type _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TypeEnumerator(string name, TypeCache cache, bool full, bool ignoreCase)
: this(HashUtility.ComputeHash64(ignoreCase ? name.ToLower() : name), cache, full, ignoreCase)
{
_cache = cache;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TypeEnumerator(ulong hash, TypeCache cache, bool full, bool ignoreCase)
{
if (ignoreCase)
{
var map = full ? _cache._fullNameMapInsensitive : _cache._nameMapInsensitive;
_values = map.TryGetValue(name.ToLower(), out var values) ? values : Array.Empty<int>();
var map = full ? cache._fullNameMapInsensitive : cache._nameMapInsensitive;
_values = map.TryGetValue(hash, out var values) ? values : Array.Empty<Type>();
}
else
{
var map = full ? _cache._fullNameMap : _cache._nameMap;
_values = map.TryGetValue(name, out var values) ? values : Array.Empty<int>();
var map = full ? cache._fullNameMap : cache._nameMap;
_values = map.TryGetValue(hash, out var values) ? values : Array.Empty<Type>();
}
_index = 0;
_current = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeEnumerator GetEnumerator() => this;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
int[] localList = _values;
if ((uint)_index < (uint)localList.Length)
if ((uint)_index < (uint)_values.Length)
{
_current = _cache.Types[_values[_index++]];
_current = _values[_index++];
return true;
}

View file

@ -31,25 +31,15 @@ public abstract class BaseGuild : ISerializable
{
Serial = World.NewGuild;
World.AddGuild(this);
SetTypeRef(GetType());
}
protected BaseGuild(Serial serial)
{
Serial = serial;
SetTypeRef(GetType());
}
public void SetTypeRef(Type type)
{
TypeRef = World.GuildTypes.IndexOf(type);
if (TypeRef == -1)
{
World.GuildTypes.Add(type);
TypeRef = World.GuildTypes.Count - 1;
}
}
public abstract string Abbreviation { get; set; }

View file

@ -43,10 +43,6 @@ public class Entity : IEntity
public Entity(Serial serial) => Serial = serial;
public void SetTypeRef(Type type)
{
}
DateTime ISerializable.Created { get; set; } = Core.Now;
DateTime ISerializable.LastSerialized { get; set; } = DateTime.MaxValue;

View file

@ -1809,8 +1809,7 @@ public class Container : Item
public class ContainerData
{
private static ILogger _logger;
private static ILogger Logger => _logger ??= LogFactory.GetLogger(typeof(ContainerData));
private static ILogger logger = LogFactory.GetLogger(typeof(ContainerData));
private static readonly Dictionary<int, ContainerData> m_Table;
static ContainerData()
@ -1875,7 +1874,7 @@ public class ContainerData
if (m_Table.ContainsKey(id))
{
Logger.Warning("double ItemID entry in Data\\containers.cfg");
logger.Warning("double ItemID entry in Data\\containers.cfg");
}
else
{

View file

@ -217,24 +217,15 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
SetLastMoved();
World.AddEntity(this);
SetTypeRef(GetType());
}
public Item(Serial serial)
{
Serial = serial;
SetTypeRef(GetType());
}
public void SetTypeRef(Type type)
{
TypeRef = World.ItemTypes.IndexOf(type);
if (TypeRef == -1)
{
World.ItemTypes.Add(type);
TypeRef = World.ItemTypes.Count - 1;
}
}
public int TempFlags
@ -786,8 +777,6 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
public int TypeRef { get; private set; }
public virtual void Serialize(IGenericWriter writer)
{
writer.Write(9); // version

View file

@ -340,7 +340,6 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
DefaultMobileInit();
World.AddEntity(this);
SetTypeRef(GetType());
}
public Mobile(Serial serial)
@ -351,19 +350,10 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
Aggressed = new List<AggressorInfo>();
NextSkillTime = Core.TickCount;
DamageEntries = new List<DamageEntry>();
SetTypeRef(GetType());
}
public void SetTypeRef(Type type)
{
TypeRef = World.MobileTypes.IndexOf(type);
if (TypeRef == -1)
{
World.MobileTypes.Add(type);
TypeRef = World.MobileTypes.Count - 1;
}
}
public static bool DragEffects { get; set; } = true;
@ -2272,8 +2262,6 @@ public class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyLis
[CommandProperty(AccessLevel.Counselor)]
public Serial Serial { get; }
public int TypeRef { get; private set; }
public virtual void Serialize(IGenericWriter writer)
{
writer.Write(33); // version

View file

@ -14,6 +14,8 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading.Tasks;
@ -27,30 +29,53 @@ public static class AdhocPersistence
* Note: The buffer may not be the same after returning from the function if more data is written
* than the initial buffer can handle.
*/
public static BufferWriter Serialize(Action<IGenericWriter> serializer)
public static BufferWriter Serialize(Action<IGenericWriter> serializer, ConcurrentQueue<Type> types)
{
var saveBuffer = new BufferWriter(true);
var saveBuffer = new BufferWriter(true, types);
serializer(saveBuffer);
return saveBuffer;
}
/**
* 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.
*/
public static void WriteSnapshot(string filePath, Span<byte> buffer)
public static void WriteSnapshot(FileInfo file, Span<byte> buffer)
{
var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory);
var file = new FileInfo(fullPath);
PathUtility.EnsureDirectory(file.DirectoryName);
var dirPath = file.DirectoryName;
PathUtility.EnsureDirectory(dirPath);
using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
using var fs = new FileStream(file.FullName, FileMode.Create, FileAccess.Write);
fs.Write(buffer);
}
public static void SerializeAndSnapshot(string filePath, Action<IGenericWriter> serializer)
/**
* Serializes to a memory buffer synchronously, then flushes to the path asynchronously.
* See WriteSnapshot for more info about how the snapshot.
*/
public static void SerializeAndSnapshot(string filePath, Action<IGenericWriter> serializer, ConcurrentQueue<Type> types = null)
{
var saveBuffer = Serialize(serializer);
Task.Run(() => { WriteSnapshot(filePath, saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position)); });
types ??= new ConcurrentQueue<Type>();
var saveBuffer = Serialize(serializer, types);
Task.Run(
() =>
{
var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory);
var file = new FileInfo(fullPath);
WriteSnapshot(file, saveBuffer.Buffer.AsSpan(0, (int)saveBuffer.Position));
// TODO: Create a PooledHashSet if performance becomes an issue.
var typesSet = new HashSet<Type>();
// Dedupe the queue.
foreach (var type in types)
{
typesSet.Add(type);
}
Persistence.WriteSerializedTypesSnapshot(file.DirectoryName, typesSet);
});
}
public static void Deserialize(string filePath, Action<IGenericReader> deserializer)

View file

@ -14,27 +14,34 @@
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Server.Buffers;
using Server.Collections;
using Server.Logging;
using Server.Text;
namespace Server;
public class BinaryFileReader : IGenericReader, IDisposable
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BinaryFileReader));
private Dictionary<ulong, string> _typesDb;
private BinaryReader _reader;
private Encoding _encoding;
public BinaryFileReader(BinaryReader br, Encoding encoding = null)
public BinaryFileReader(BinaryReader br, Dictionary<ulong, string> typesDb = null, Encoding encoding = null)
{
_reader = br;
_encoding = encoding ?? TextEncoding.UTF8;
_typesDb = typesDb;
}
public BinaryFileReader(Stream stream, Encoding encoding = null) : this(new BinaryReader(stream), encoding)
public BinaryFileReader(Stream stream, Dictionary<ulong, string> typesDb = null, Encoding encoding = null)
: this(new BinaryReader(stream), typesDb, encoding)
{
}
@ -46,23 +53,20 @@ public class BinaryFileReader : IGenericReader, IDisposable
public DateTime LastSerialized { get; init; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ReadString(bool intern = false)
{
if (!ReadBool())
{
return null;
}
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 ? Utility.Intern("") : "";
return "".Intern();
}
byte[] buffer = STArrayPool<byte>.Shared.Rent(length);
var str = TextEncoding.GetString(buffer.AsSpan(0, length), _encoding);
STArrayPool<byte>.Shared.Return(buffer);
return intern ? Utility.Intern(str) : str;
return intern ? str.Intern() : str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -101,6 +105,62 @@ public class BinaryFileReader : IGenericReader, IDisposable
[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<byte> buffer) => _reader.Read(buffer);

View file

@ -14,6 +14,7 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.IO;
namespace Server;
@ -23,11 +24,11 @@ public class BinaryFileWriter : BufferWriter, IDisposable
private readonly Stream _file;
private long _position;
public BinaryFileWriter(string filename, bool prefixStr) :
this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), prefixStr)
public BinaryFileWriter(string filename, bool prefixStr, ConcurrentQueue<Type> types = null) :
this(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None), prefixStr, types)
{}
public BinaryFileWriter(Stream stream, bool prefixStr) : base(prefixStr)
public BinaryFileWriter(Stream stream, bool prefixStr, ConcurrentQueue<Type> types = null) : base(prefixStr, types)
{
_file = stream;
_position = _file.Position;

View file

@ -15,30 +15,40 @@
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.Collections;
using Server.Logging;
using Server.Text;
namespace Server;
public class BufferReader : IGenericReader
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BufferReader));
private Dictionary<ulong, string> _typesDb;
private Encoding _encoding;
private byte[] _buffer;
private int _position;
public long Position => _position;
public BufferReader(byte[] buffer, Encoding encoding = null)
public BufferReader(byte[] buffer, Dictionary<ulong, string> typesDb = null, Encoding encoding = null)
{
_buffer = buffer;
_encoding = encoding ?? TextEncoding.UTF8;
_typesDb = typesDb;
}
public BufferReader(byte[] buffer, DateTime lastSerialized) : this(buffer) => LastSerialized = lastSerialized;
public BufferReader(byte[] buffer, DateTime lastSerialized, Dictionary<ulong, string> typesDb = null) : this(buffer)
{
LastSerialized = lastSerialized;
_typesDb = typesDb;
}
public void Reset(byte[] newBuffer, out byte[] oldBuffer)
{
@ -49,22 +59,20 @@ public class BufferReader : IGenericReader
public DateTime LastSerialized { get; init; }
public string ReadString(bool intern = false)
{
if (!ReadBool())
{
return null;
}
[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 ? Utility.Intern("") : "";
return "".Intern();
}
var str = TextEncoding.GetString(_buffer.AsSpan(_position, length), _encoding);
_position += length;
return intern ? Utility.Intern(str) : str;
return intern ? str.Intern() : str;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -143,6 +151,62 @@ public class BufferReader : IGenericReader
[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<byte> buffer)
{
var length = buffer.Length;

View file

@ -2,7 +2,7 @@
* ModernUO *
* Copyright 2019-2022 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: BufferedFileWriter.cs *
* File: BufferWriter.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 *
@ -14,6 +14,7 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
@ -25,8 +26,9 @@ namespace Server;
public class BufferWriter : IGenericWriter
{
private readonly Encoding m_Encoding;
private readonly bool m_PrefixStrings;
private ConcurrentQueue<Type> _types;
private Encoding _encoding;
private bool _prefixStrings;
private long _bytesWritten;
private long _index;
@ -53,22 +55,24 @@ public class BufferWriter : IGenericWriter
private byte[] _buffer;
public BufferWriter(byte[] buffer, bool prefixStr)
public BufferWriter(byte[] buffer, bool prefixStr, ConcurrentQueue<Type> types = null)
{
m_PrefixStrings = prefixStr;
m_Encoding = TextEncoding.UTF8;
_prefixStrings = prefixStr;
_encoding = TextEncoding.UTF8;
_buffer = buffer;
_types = types;
}
public BufferWriter(bool prefixStr) : this(0, prefixStr)
public BufferWriter(bool prefixStr, ConcurrentQueue<Type> types = null) : this(0, prefixStr, types)
{
}
public BufferWriter(int count, bool prefixStr)
public BufferWriter(int count, bool prefixStr, ConcurrentQueue<Type> types = null)
{
m_PrefixStrings = prefixStr;
m_Encoding = TextEncoding.UTF8;
_prefixStrings = prefixStr;
_encoding = TextEncoding.UTF8;
_buffer = GC.AllocateUninitializedArray<byte>(count < 1 ? BufferSize : count);
_types = types;
}
public virtual long Position => Index;
@ -135,6 +139,7 @@ public class BufferWriter : IGenericWriter
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(BitArray bitArray)
{
var byteLength = BitArray.GetByteArrayLengthFromBitLength(bitArray.Length);
@ -145,6 +150,7 @@ public class BufferWriter : IGenericWriter
Index += byteLength;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual long Seek(long offset, SeekOrigin origin)
{
Debug.Assert(
@ -168,9 +174,10 @@ public class BufferWriter : IGenericWriter
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(string value)
{
if (m_PrefixStrings)
if (_prefixStrings)
{
if (value == null)
{
@ -188,6 +195,7 @@ public class BufferWriter : IGenericWriter
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(long value)
{
FlushIfNeeded(8);
@ -202,6 +210,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 56);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ulong value)
{
FlushIfNeeded(8);
@ -216,6 +225,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 56);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(int value)
{
FlushIfNeeded(4);
@ -226,6 +236,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 24);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(uint value)
{
FlushIfNeeded(4);
@ -236,6 +247,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 24);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(short value)
{
FlushIfNeeded(2);
@ -244,6 +256,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 8);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ushort value)
{
FlushIfNeeded(2);
@ -252,6 +265,7 @@ public class BufferWriter : IGenericWriter
_buffer[Index++] = (byte)(value >> 8);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void Write(double value)
{
FlushIfNeeded(8);
@ -264,6 +278,7 @@ public class BufferWriter : IGenericWriter
Index += 8;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void Write(float value)
{
FlushIfNeeded(4);
@ -300,9 +315,25 @@ public class BufferWriter : IGenericWriter
[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)]
internal void InternalWriteString(string value)
{
var remaining = m_Encoding.GetByteCount(value);
var remaining = _encoding.GetByteCount(value);
((IGenericWriter)this).WriteEncodedInt(remaining);
@ -313,14 +344,14 @@ public class BufferWriter : IGenericWriter
// It is much faster to encode to stack buffer, then copy to the real buffer
Span<byte> span = stackalloc byte[Math.Min(BufferSize, 256)];
var maxChars = span.Length / m_Encoding.GetMaxByteCount(1);
var maxChars = span.Length / _encoding.GetMaxByteCount(1);
var charsLeft = value.Length;
var current = 0;
while (charsLeft > 0)
{
var charCount = Math.Min(charsLeft, maxChars);
var bytesWritten = m_Encoding.GetBytes(value.AsSpan(current, charCount), span);
var bytesWritten = _encoding.GetBytes(value.AsSpan(current, charCount), span);
remaining -= bytesWritten;
charsLeft -= charCount;
current += charCount;

View file

@ -14,6 +14,8 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
namespace Server;
@ -31,22 +33,22 @@ public static class GenericPersistence
void Serialize()
{
saveBuffer ??= new BufferWriter(true);
saveBuffer ??= new BufferWriter(true, World.SerializedTypes);
saveBuffer.Seek(0, SeekOrigin.Begin);
serializer(saveBuffer);
}
void WriterSnapshot(string savePath)
void WriteSnapshot(string savePath)
{
string binPath = Path.Combine(savePath, name, $"{name}.bin");
var buffer = saveBuffer!.Buffer.AsSpan(0, (int)saveBuffer.Position);
AdhocPersistence.WriteSnapshot(binPath, buffer);
AdhocPersistence.WriteSnapshot(new FileInfo(binPath), buffer);
}
void Deserialize(string savePath) =>
void Deserialize(string savePath, Dictionary<ulong, string> typesDb) =>
AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer);
Persistence.Register(name, Serialize, WriterSnapshot, Deserialize, priority);
Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority);
}
}

View file

@ -38,6 +38,7 @@ public interface IGenericReader
sbyte ReadSByte();
bool ReadBool();
Serial ReadSerial();
Type ReadType();
DateTime ReadDateTime() => new(ReadLong(), DateTimeKind.Utc);
TimeSpan ReadTimeSpan() => new(ReadLong());

View file

@ -37,6 +37,7 @@ public interface IGenericWriter
void Write(sbyte value);
void Write(bool value);
void Write(Serial serial);
void Write(Type type);
void Write(DateTime value)
{

View file

@ -14,6 +14,7 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.IO;
namespace Server;
@ -28,7 +29,6 @@ public interface ISerializable
long SavePosition { get; protected internal set; }
BufferWriter SaveBuffer { get; protected internal set; }
int TypeRef { get; }
Serial Serial { get; }
// Executed on every entity, before it's serialized.
@ -39,11 +39,9 @@ public interface ISerializable
void Delete();
bool Deleted { get; }
void SetTypeRef(Type type);
public void InitializeSaveBuffer(byte[] buffer)
public void InitializeSaveBuffer(byte[] buffer, ConcurrentQueue<Type> types)
{
SaveBuffer = new BufferWriter(buffer, true);
SaveBuffer = new BufferWriter(buffer, true, types);
if (World.DirtyTrackingEnabled)
{
SavePosition = SaveBuffer.Position;
@ -54,9 +52,9 @@ public interface ISerializable
}
}
public void Serialize()
public void Serialize(ConcurrentQueue<Type> types)
{
SaveBuffer ??= new BufferWriter(true);
SaveBuffer ??= new BufferWriter(true, types);
BeforeSerialize();

View file

@ -14,13 +14,14 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Server;
public class Persistence
public static class Persistence
{
public const int DefaultPriority = 100;
@ -30,7 +31,7 @@ public class Persistence
string name,
Action serializer,
Action<string> snapshotWriter,
Action<string> deserializer,
Action<string, Dictionary<ulong, string>> deserializer,
int priority = DefaultPriority
)
{
@ -50,24 +51,79 @@ public class Persistence
public static void Load(string path)
{
var typesDb = LoadTypes(path);
// This should probably not be parallel since Mobiles must be loaded before Items
foreach (var entry in _registry)
{
entry.Deserialize(path);
entry.Deserialize(path, typesDb);
}
}
private static Dictionary<ulong, string> LoadTypes(string path)
{
var db = new Dictionary<ulong, string>();
string tdbPath = Path.Combine(path, "SerializedTypes.db");
if (!File.Exists(tdbPath))
{
return db;
}
using FileStream tdb = new FileStream(tdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
BinaryReader tdbReader = new BinaryReader(tdb);
var version = tdbReader.ReadInt32();
var count = tdbReader.ReadInt32();
for (var i = 0; i < count; ++i)
{
var hash = tdbReader.ReadUInt64();
var typeName = tdbReader.ReadString();
db[hash] = typeName;
}
return db;
}
public static void Serialize()
{
Parallel.ForEach(_registry, entry => entry.Serialize());
}
public static void WriteSnapshot(string path)
public static void WriteSnapshot(string path, ConcurrentQueue<Type> types)
{
foreach (var entry in _registry)
{
entry.WriteSnapshot(path);
}
// Dedupe the queue.
foreach (var type in types)
{
_typesSet.Add(type);
}
WriteSerializedTypesSnapshot(path, _typesSet);
_typesSet.Clear();
}
private static HashSet<Type> _typesSet = new();
public static void WriteSerializedTypesSnapshot(string path, HashSet<Type> types)
{
string tdbPath = Path.Combine(path, "SerializedTypes.db");
using var tdb = new BinaryFileWriter(tdbPath, false);
tdb.Write(0); // version
tdb.Write(types.Count);
foreach (var type in types)
{
var fullName = type.FullName;
tdb.Write(HashUtility.ComputeHash64(fullName));
tdb.Write(fullName);
}
}
public record RegistryEntry
@ -76,7 +132,7 @@ public class Persistence
public int Priority { get; init; }
public Action Serialize { get; init; } // Serializing to memory buffers
public Action<string> WriteSnapshot { get; init; }
public Action<string> Deserialize { get; init; }
public Action<string, Dictionary<ulong, string>> Deserialize { get; init; }
}
internal class RegistryEntryComparer : IComparer<RegistryEntry>

View file

@ -38,6 +38,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="PollGroup" Version="1.2.1" />
<PackageReference Include="Standart.Hash.xxHash.Signed" Version="4.0.4" />
<PackageReference Include="Zlib.Bindings" Version="1.9.2" />
<PackageReference Include="ModernUO.Serialization.Annotations" Version="2.2.0" />

View file

@ -0,0 +1,48 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2022 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: HashUtility.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Runtime.CompilerServices;
using Standart.Hash.xxHash;
namespace Server;
/// <summary>
/// Represents supported non-cryptographic fast hash algorithms.
/// </summary>
public enum FastHashAlgorithm
{
None, // Used for collisions where full-data is serialized instead
XXHash3_64, // xxHash3 64bit
}
public static class HashUtility
{
// *************** DO NOT CHANGE THIS NUMBER ****************
// * Computed hashes might be serialized against this seed! *
// **********************************************************
private const ulong xxHash3Seed = 9609125370673258709ul; // Randomly generated 64-bit prime number
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong ComputeHash64(string? data, FastHashAlgorithm algorithm = FastHashAlgorithm.XXHash3_64) =>
algorithm switch
{
FastHashAlgorithm.XXHash3_64 => ComputeXXHash3_64(data),
_ => throw new NotSupportedException($"Hash {algorithm} is not supported.")
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong ComputeXXHash3_64(string? data) => data == null ? 0 : xxHash3.ComputeHash(data, xxHash3Seed);
}

View file

@ -120,8 +120,10 @@ public static class Utility
sb.Append(value);
}
public static string Intern(string str) => str?.Length > 0 ? string.Intern(str) : str;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Intern(this string str) => str?.Length > 0 ? string.Intern(str) : str;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Intern(ref string str)
{
str = Intern(str);

View file

@ -14,6 +14,7 @@
*************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
@ -25,13 +26,11 @@ namespace Server;
public static class EntityPersistence
{
private const int _idxVersion = 1;
public static void WriteEntities<I, T>(
IIndexInfo<I> indexInfo,
Dictionary<I, T> entities,
List<Type> types,
string savePath,
ConcurrentQueue<Type> types,
out Dictionary<string, int> counts
) where T : class, ISerializable
{
@ -44,20 +43,19 @@ public static class EntityPersistence
PathUtility.EnsureDirectory(path);
string idxPath = Path.Combine(path, $"{typeName}.idx");
string tdbPath = Path.Combine(path, $"{typeName}.tdb");
string binPath = Path.Combine(path, $"{typeName}.bin");
using var idx = new BinaryFileWriter(idxPath, false);
using var tdb = new BinaryFileWriter(tdbPath, false);
using var bin = new BinaryFileWriter(binPath, true);
using var idx = new BinaryFileWriter(idxPath, false, types);
using var bin = new BinaryFileWriter(binPath, true, types);
idx.Write(1); // Version
idx.Write(2); // Version
idx.Write(entities.Count);
foreach (var e in entities.Values)
{
long start = bin.Position;
idx.Write(e.TypeRef);
var t = e.GetType();
idx.Write(t);
idx.Write(e.Serial);
idx.Write(e.Created.Ticks);
idx.Write(e.LastSerialized.Ticks);
@ -73,12 +71,6 @@ public static class EntityPersistence
counts[type] = (counts.TryGetValue(type, out var count) ? count : 0) + 1;
}
}
tdb.Write(types.Count);
for (int i = 0; i < types.Count; ++i)
{
tdb.Write(types[i].FullName);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -90,6 +82,7 @@ public static class EntityPersistence
public static Dictionary<I, T> LoadIndex<I, T>(
string path,
IIndexInfo<I> indexInfo,
Dictionary<ulong, string> serializedTypes,
out List<EntitySpan<T>> entities
) where T : class, ISerializable
{
@ -99,11 +92,10 @@ public static class EntityPersistence
var indexType = indexInfo.TypeName;
string indexPath = Path.Combine(path, indexType, $"{indexType}.idx");
string typesPath = Path.Combine(path, indexType, $"{indexType}.tdb");
entities = new List<EntitySpan<T>>();
if (!File.Exists(indexPath) || !File.Exists(typesPath))
if (!File.Exists(indexPath))
{
return map;
}
@ -111,58 +103,77 @@ public static class EntityPersistence
using FileStream idx = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read);
BinaryReader idxReader = new BinaryReader(idx);
using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read);
BinaryReader tdbReader = new BinaryReader(tdb);
List<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(tdbReader);
int count;
var version = idxReader.ReadInt32();
int count = idxReader.ReadInt32();
// Handle non-versioned (version 0).
if (version > _idxVersion || idx.Length - 4 - version * 20 == 0)
var ctorArguments = new[] { typeof(I) };
List<ConstructorInfo> types;
string typesPath = Path.Combine(path, indexType, $"{indexType}.tdb");
if (File.Exists(typesPath))
{
count = version;
version = 0;
using FileStream tdb = new FileStream(typesPath, FileMode.Open, FileAccess.Read, FileShare.Read);
BinaryReader tdbReader = new BinaryReader(tdb);
types = ReadTypes(tdbReader, ctorArguments);
tdbReader.Close();
}
else
{
count = idxReader.ReadInt32();
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)
{
var typeID = idxReader.ReadInt32();
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);
var lastSerialized = version == 0 ? DateTime.MinValue : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc);
var pos = idxReader.ReadInt64();
var length = idxReader.ReadInt32();
Tuple<ConstructorInfo, string> objs = types[typeID];
if (objs == null)
if (ctor == null)
{
continue;
}
ConstructorInfo ctor = objs.Item1;
I indexer = indexInfo.CreateIndex(serial);
ctorArgs[0] = indexer;
if (ctor.Invoke(ctorArgs) is T t)
if (ctor.Invoke(ctorArgs) is T entity)
{
t.Created = created;
t.LastSerialized = lastSerialized;
entities.Add(new EntitySpan<T>(t, typeID, pos, length));
map[indexer] = t;
entity.Created = created;
entity.LastSerialized = lastSerialized;
entities.Add(new EntitySpan<T>(entity, pos, length));
map[indexer] = entity;
}
}
tdbReader.Close();
idxReader.Close();
return map;
@ -171,6 +182,7 @@ public static class EntityPersistence
public static void LoadData<I, T>(
string path,
IIndexInfo<I> indexInfo,
Dictionary<ulong, string> serializedTypes,
List<EntitySpan<T>> entities
) where T : class, ISerializable
{
@ -211,7 +223,7 @@ public static class EntityPersistence
var buffer = GC.AllocateUninitializedArray<byte>(entry.Length);
if (br == null)
{
br = new BufferReader(buffer, t.LastSerialized);
br = new BufferReader(buffer, t.LastSerialized, serializedTypes);
}
else
{
@ -236,7 +248,7 @@ public static class EntityPersistence
if (error == null)
{
t.InitializeSaveBuffer(buffer);
t.InitializeSaveBuffer(buffer, World.SerializedTypes);
}
else
{
@ -265,50 +277,52 @@ public static class EntityPersistence
}
}
private static List<Tuple<ConstructorInfo, string>> ReadTypes<I>(BinaryReader tdbReader)
private static ConstructorInfo GetConstructorFor(string typeName, Type t, Type[] constructorTypes)
{
var constructorTypes = new[] { typeof(I) };
if (t?.IsAbstract != false)
{
Console.WriteLine("failed");
var issue = t?.IsAbstract == true ? "marked abstract" : "not found";
Console.WriteLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.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<ConstructorInfo> ReadTypes(BinaryReader tdbReader, Type[] ctorArguments)
{
var count = tdbReader.ReadInt32();
var types = new List<Tuple<ConstructorInfo, string>>(count);
var types = new List<ConstructorInfo>(count);
for (var i = 0; i < count; ++i)
{
var typeName = tdbReader.ReadString();
var t = AssemblyHandler.FindTypeByFullName(typeName, false);
if (t?.IsAbstract != false)
{
Console.WriteLine("failed");
var issue = t?.IsAbstract == true ? "marked abstract" : "not found";
Console.WriteLine($"Error: Type '{typeName}' was {issue}. Delete all of those types? (y/n)");
if (Console.ReadKey(true).Key == ConsoleKey.Y)
{
types.Add(null);
Console.WriteLine("Loading...");
continue;
}
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)
{
types.Add(new Tuple<ConstructorInfo, string>(ctor, typeName));
}
else
{
throw new Exception($"Type '{t}' does not have a serialization constructor");
}
var ctor = GetConstructorFor(typeName, t, ctorArguments);
types.Add(ctor);
}
return types;

View file

@ -19,16 +19,13 @@ public struct EntitySpan<T> where T : ISerializable
{
public T Entity { get; }
public int TypeID { get; }
public long Position { get; }
public int Length { get; }
public EntitySpan(T entity, int typeID, long position, int length)
public EntitySpan(T entity, long position, int length)
{
Entity = entity;
TypeID = typeID;
Position = position;
Length = length;
}

View file

@ -38,12 +38,12 @@ public enum WorldState
public static class World
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(World));
private static ILogger logger = LogFactory.GetLogger(typeof(World));
private static readonly ManualResetEvent m_DiskWriteHandle = new(true);
private static readonly Dictionary<Serial, IEntity> _pendingAdd = new();
private static readonly Dictionary<Serial, IEntity> _pendingDelete = new();
private static readonly ConcurrentQueue<Item> _decayQueue = new();
private static ManualResetEvent m_DiskWriteHandle = new(true);
private static Dictionary<Serial, IEntity> _pendingAdd = new();
private static Dictionary<Serial, IEntity> _pendingDelete = new();
private static ConcurrentQueue<Item> _decayQueue = new();
private static string _tempSavePath; // Path to the temporary folder for the save
private static bool _enableSaveStats;
@ -125,10 +125,6 @@ public static class World
private static void OutOfMemory(string message) => throw new OutOfMemoryException(message);
internal static List<Type> ItemTypes { get; } = new();
internal static List<Type> MobileTypes { get; } = new();
internal static List<Type> GuildTypes { get; } = new();
public static string SavePath { get; private set; }
public static WorldState WorldState { get; private set; }
@ -232,15 +228,15 @@ public static class World
public static void Broadcast(int hue, bool ascii, string format, params object[] args) =>
Broadcast(hue, ascii, string.Format(format, args));
internal static void LoadEntities(string basePath)
internal static void LoadEntities(string basePath, Dictionary<ulong, string> typesDb)
{
IIndexInfo<Serial> itemIndexInfo = new EntityTypeIndex("Items");
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, out List<EntitySpan<Mobile>> mobiles);
Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, out List<EntitySpan<Item>> items);
Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, out List<EntitySpan<BaseGuild>> guilds);
Mobiles = EntityPersistence.LoadIndex(basePath, mobileIndexInfo, typesDb, out List<EntitySpan<Mobile>> mobiles);
Items = EntityPersistence.LoadIndex(basePath, itemIndexInfo, typesDb, out List<EntitySpan<Item>> items);
Guilds = EntityPersistence.LoadIndex(basePath, guildIndexInfo, typesDb, out List<EntitySpan<BaseGuild>> guilds);
if (Mobiles.Count > 0)
{
@ -257,9 +253,9 @@ public static class World
_lastGuild = Guilds.Keys.Max();
}
EntityPersistence.LoadData(basePath, mobileIndexInfo, mobiles);
EntityPersistence.LoadData(basePath, itemIndexInfo, items);
EntityPersistence.LoadData(basePath, guildIndexInfo, guilds);
EntityPersistence.LoadData(basePath, mobileIndexInfo, typesDb, mobiles);
EntityPersistence.LoadData(basePath, itemIndexInfo, typesDb, items);
EntityPersistence.LoadData(basePath, guildIndexInfo, typesDb, guilds);
}
public static void Load()
@ -392,9 +388,9 @@ public static class World
IIndexInfo<Serial> mobileIndexInfo = new EntityTypeIndex("Mobiles");
IIndexInfo<Serial> guildIndexInfo = new EntityTypeIndex("Guilds");
EntityPersistence.WriteEntities(mobileIndexInfo, Mobiles, MobileTypes, basePath, out var mobileCounts);
EntityPersistence.WriteEntities(itemIndexInfo, Items, ItemTypes, basePath, out var itemCounts);
EntityPersistence.WriteEntities(guildIndexInfo, Guilds, GuildTypes, basePath, out var guildCounts);
EntityPersistence.WriteEntities(mobileIndexInfo, Mobiles, basePath, SerializedTypes, out var mobileCounts);
EntityPersistence.WriteEntities(itemIndexInfo, Items, basePath, SerializedTypes, out var itemCounts);
EntityPersistence.WriteEntities(guildIndexInfo, Guilds, basePath, SerializedTypes, out var guildCounts);
if (_enableSaveStats)
{
@ -413,7 +409,7 @@ public static class World
var watch = Stopwatch.StartNew();
logger.Information("Writing world save snapshot");
Persistence.WriteSnapshot(tempPath);
Persistence.WriteSnapshot(tempPath, SerializedTypes);
watch.Stop();
@ -444,6 +440,9 @@ public static class World
}
}
// Clear types
SerializedTypes.Clear();
m_DiskWriteHandle.Set();
Core.LoopContext.Post(FinishWorldSave);
@ -463,6 +462,39 @@ public static class World
private static DateTime _serializationStart;
/**
* Duplicates can be weeded out asynchronously while flushing
* If performance becomes a problem, we need to build a dual mode concurrent array.
*
****************************************************** Proposal ******************************************************
* The structure is initialized with a large capacity to avoid unnecessary resizing.
* Write Mode:
* - Multiple threads can add a single, or a range of elements concurrently.
* - Elements can be Peeked, but there are no guarantees.
* - To resize the internal array, replaced it with the next size up from an array pool.
* - The structure cannot be cleared in this mode.
*
* Read Mode:
* - The array can be read from multiple threads using a ref struct enumerator.
* - Elements cannot be added or reassigned.
* - Cleared by replacing the internal array with another one from the pool.
* - Note: Upon clearing, the existing array is not sent back to the pool until there are zero enumerators.
*
* Enumeration:
* - Multiple threads can enumerate while in read mode. The enumerator will Interlocked.Increment a read counter.
* - Upon dispose of the enumerator, the read counter will be lowered with an Interlocked.Decrement
* - When the read counter reaches 0, if there is a cleared array, the array is sent back to the pool zeroed.
*
* Notes:
* - Elements can never be removed.
*
* How is this different from ConcurrentQueue?
* The functionality is very similar, except the constraints allow the implementation to be done without locks.
* Since this implementation uses pooled arrays, allocations will approach zero over time.
**********************************************************************************************************************
*/
public static ConcurrentQueue<Type> SerializedTypes { get; } = new();
internal static void SaveEntities()
{
_serializationStart = DateTime.UtcNow;
@ -479,7 +511,7 @@ public static class World
EnqueueForDecay(item);
}
entity.Serialize();
entity.Serialize(SerializedTypes);
}
public static void Save()
@ -632,7 +664,10 @@ public static class World
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddGuild(BaseGuild guild) => Guilds[guild.Serial] = guild;
public static void AddGuild(BaseGuild guild)
{
Guilds[guild.Serial] = guild;
}
public static void RemoveEntity<T>(T entity) where T : class, IEntity
{