diff --git a/Projects/Server.Tests/Tests/Serialization/FileBufferWriterTests.cs b/Projects/Server.Tests/Tests/Serialization/FileBufferWriterTests.cs index df3f52f0e..fcbfb70a0 100644 --- a/Projects/Server.Tests/Tests/Serialization/FileBufferWriterTests.cs +++ b/Projects/Server.Tests/Tests/Serialization/FileBufferWriterTests.cs @@ -111,23 +111,18 @@ public class FileBufferWriterTests } [Fact] - public void TypeWritesRegisterIntoTheTypeSet() + public void TypeWritesUseTagAndHashFormat() { var path = TempFile(); try { - var typeSet = new System.Collections.Generic.HashSet(); - - using (var writer = new FileBufferWriter(path, typeSet)) + using (var writer = new FileBufferWriter(path)) { writer.Write(typeof(string)); writer.Write((Type)null); } - Assert.Contains(typeof(string), typeSet); - Assert.Single(typeSet); - var bytes = File.ReadAllBytes(path); Assert.Equal(1 + 8 + 1, bytes.Length); // flag + hash + null flag Assert.Equal(2, bytes[0]); diff --git a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs index 9faf986f9..f2114a427 100644 --- a/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs +++ b/Projects/Server.Tests/Tests/Serialization/GenericEntityPersistenceRoundTripTests.cs @@ -123,7 +123,7 @@ public class GenericEntityPersistenceRoundTripTests // Background write phase: snapshot from the segment logs. v4 embeds the type // table in the idx, so no SerializedTypes.db is produced or needed. - persistence.WriteSnapshot(dir, []); + persistence.WriteSnapshot(dir); persistence.PostWorldSave(); // releases the entries snapshot diff --git a/Projects/Server/Serialization/AdhocPersistence.cs b/Projects/Server/Serialization/AdhocPersistence.cs index b40b25b23..d6c132549 100644 --- a/Projects/Server/Serialization/AdhocPersistence.cs +++ b/Projects/Server/Serialization/AdhocPersistence.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.MemoryMappedFiles; @@ -27,9 +26,9 @@ public static class AdhocPersistence /** * Serializes to memory. */ - public static IGenericWriter SerializeToBuffer(Action serializer, ConcurrentQueue types = null) + public static IGenericWriter SerializeToBuffer(Action serializer) { - var saveBuffer = new BufferWriter(true, types); + var saveBuffer = new BufferWriter(true); serializer(saveBuffer); return saveBuffer; } @@ -55,18 +54,11 @@ public static class AdhocPersistence { var fullPath = PathUtility.GetFullPath(filePath, Core.BaseDirectory); PathUtility.EnsureDirectory(Path.GetDirectoryName(fullPath)); - HashSet typesSet = []; - var writer = new FileBufferWriter(fullPath, typesSet, sizeHint); + + var writer = new FileBufferWriter(fullPath, sizeHint); serializer(writer); - Task.Run( - () => - { - writer.Dispose(); - Persistence.WriteSerializedTypesSnapshot(Path.GetDirectoryName(fullPath), typesSet); - }, - Core.ClosingTokenSource.Token - ); + Task.Run(() => writer.Dispose(), Core.ClosingTokenSource.Token); } public static unsafe void Deserialize(string filePath, Action deserializer) diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index ab441a71c..b811d68cb 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -17,7 +17,6 @@ using System; using System.Buffers; using System.Buffers.Binary; using System.Collections; -using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Net; @@ -30,7 +29,6 @@ namespace Server; public class BufferWriter : IGenericWriter { - private readonly ConcurrentQueue _types; private readonly Encoding _encoding; private readonly bool _prefixStrings; @@ -60,24 +58,22 @@ public class BufferWriter : IGenericWriter private byte[] _buffer; - public BufferWriter(byte[] buffer, bool prefixStr, ConcurrentQueue types = null) + public BufferWriter(byte[] buffer, bool prefixStr) { _prefixStrings = prefixStr; _encoding = TextEncoding.UTF8; _buffer = buffer; - _types = types; } - public BufferWriter(bool prefixStr, ConcurrentQueue types = null) : this(0, prefixStr, types) + public BufferWriter(bool prefixStr) : this(0, prefixStr) { } - public BufferWriter(int count, bool prefixStr, ConcurrentQueue types = null) + public BufferWriter(int count, bool prefixStr) { _prefixStrings = prefixStr; _encoding = TextEncoding.UTF8; _buffer = GC.AllocateUninitializedArray(count < 1 ? BufferSize : count); - _types = types; } public virtual long Position => _index; @@ -307,7 +303,6 @@ public class BufferWriter : IGenericWriter { Write((byte)0x2); // xxHash3 64bit Write(AssemblyHandler.GetTypeHash(type)); - _types?.Enqueue(type); } } diff --git a/Projects/Server/Serialization/FileBufferWriter.cs b/Projects/Server/Serialization/FileBufferWriter.cs index 8b5862403..c7408f7bc 100644 --- a/Projects/Server/Serialization/FileBufferWriter.cs +++ b/Projects/Server/Serialization/FileBufferWriter.cs @@ -15,8 +15,6 @@ using System; using System.Buffers; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; using Microsoft.Win32.SafeHandles; @@ -44,16 +42,14 @@ public class FileBufferWriter : BufferWriter, IDisposable private long _fileHighWater; // logical end of file across seeks /// Destination file; created/truncated. - /// Types written via register here. /// - /// Expected total file size when known (e.g. idx files are exactly - /// 8 + 33 * count bytes). Files at or under the staging cap never drain until close; - /// larger files stream through a pooled block at the cap. The block comes from - /// ArrayPool so sequential snapshot writers recycle one buffer instead of dropping - /// a large-object allocation per file per save. + /// Expected total file size when known. Files at or under the staging cap never + /// drain until close; larger files stream through a pooled block at the cap. The + /// block comes from ArrayPool so sequential snapshot writers recycle one buffer + /// instead of dropping a large-object allocation per file per save. /// - public FileBufferWriter(string filePath, HashSet typeSet = null, long expectedSize = MaxStagingSize) - : base(RentStaging(expectedSize), true, typeSet != null ? new ConcurrentQueue(typeSet) : null) + public FileBufferWriter(string filePath, long expectedSize = MaxStagingSize) + : base(RentStaging(expectedSize), true) { _rentedStaging = Buffer; _handle = File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.None, FileOptions.SequentialScan); diff --git a/Projects/Server/Serialization/GenericEntityPersistence.cs b/Projects/Server/Serialization/GenericEntityPersistence.cs index 5ee60c5e4..48288fde4 100644 --- a/Projects/Server/Serialization/GenericEntityPersistence.cs +++ b/Projects/Server/Serialization/GenericEntityPersistence.cs @@ -102,7 +102,7 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer typeof(T).RegisterFindEntity(Find); } - public override void WriteSnapshot(string savePath, HashSet typeSet) + public override void WriteSnapshot(string savePath) { var dir = Path.Combine(savePath, Name); PathUtility.EnsureDirectory(dir); @@ -117,7 +117,7 @@ public class GenericEntityPersistence : GenericPersistence, IGenericEntityPer // v4 records are fixed-width 26 bytes; the header carries the type table // (name lengths vary — 64 bytes per entry is a staging hint, not a contract). var expectedIdxSize = 12 + 26L * EntitiesBySerial.Count + 64L * _typeTable.Count; - using var idx = new FileBufferWriter(Path.Combine(dir, $"{Name}.idx"), typeSet, expectedIdxSize); + using var idx = new FileBufferWriter(Path.Combine(dir, $"{Name}.idx"), expectedIdxSize); var binPosition = 0L; diff --git a/Projects/Server/Serialization/GenericPersistence.cs b/Projects/Server/Serialization/GenericPersistence.cs index f1c939729..5b8e79c29 100644 --- a/Projects/Server/Serialization/GenericPersistence.cs +++ b/Projects/Server/Serialization/GenericPersistence.cs @@ -57,7 +57,7 @@ public abstract class GenericPersistence : Persistence, IGenericSerializable World.PushSingleToCache(this); } - public override void WriteSnapshot(string savePath, HashSet typeSet) + public override void WriteSnapshot(string savePath) { if (_selfLength == 0) { diff --git a/Projects/Server/Serialization/Persistence.cs b/Projects/Server/Serialization/Persistence.cs index 1e55a0fc6..35d1d7514 100644 --- a/Projects/Server/Serialization/Persistence.cs +++ b/Projects/Server/Serialization/Persistence.cs @@ -84,29 +84,11 @@ public abstract class Persistence } // Note: This is strictly on a background thread - internal static void WriteSnapshotAll(string path, HashSet typeSet) + internal static void WriteSnapshotAll(string path) { foreach (var p in _registry) { - p.WriteSnapshot(path, typeSet); - } - - WriteSerializedTypesSnapshot(path, typeSet); - } - - public static void WriteSerializedTypesSnapshot(string path, HashSet types) - { - var typesPath = Path.Combine(path, "SerializedTypes.db"); - using var writer = new FileBufferWriter(typesPath); - - writer.Write(0); // version - writer.Write(types.Count); - - foreach (var type in types) - { - var fullName = type.FullName; - writer.Write(HashUtility.ComputeHash64(fullName)); - writer.WriteRaw(fullName); + p.WriteSnapshot(path); } } @@ -148,7 +130,7 @@ public abstract class Persistence } // Note: This should only be run on a background thread - public abstract void WriteSnapshot(string savePath, HashSet typeSet); + public abstract void WriteSnapshot(string savePath); public abstract void Serialize(); diff --git a/Projects/Server/Serialization/SerializationThreadWorker.cs b/Projects/Server/Serialization/SerializationThreadWorker.cs index c4aee0750..8fe78911d 100644 --- a/Projects/Server/Serialization/SerializationThreadWorker.cs +++ b/Projects/Server/Serialization/SerializationThreadWorker.cs @@ -214,7 +214,7 @@ public class SerializationThreadWorker { ReleaseWriteLogs(); - var writer = new BufferWriter(_heap, true, World.SerializedTypes); + var writer = new BufferWriter(_heap, true); var entities = 0L; while (_chunkSource.TryTake(out var chunk)) @@ -234,13 +234,12 @@ public class SerializationThreadWorker var worker = (SerializationThreadWorker)obj; var chunkSource = worker._chunkSource; - var serializedTypes = World.SerializedTypes; while (worker._startEvent.WaitOne()) { worker.ReleaseWriteLogs(); - var writer = new BufferWriter(worker._heap, true, serializedTypes); + var writer = new BufferWriter(worker._heap, true); var entities = 0L; var spinner = new SpinWait(); diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index af6eda095..2c9e6d216 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -14,7 +14,6 @@ *************************************************************************/ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -239,39 +238,6 @@ public static class World } } - /** - * 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 SerializedTypes { get; } = new(); - public static void Save() { if (WorldState != WorldState.Running) @@ -367,8 +333,6 @@ public static class World } } - private static readonly HashSet _typesSet = []; - private static void WriteFiles(object state) { var snapshotPath = (string)state; @@ -377,15 +341,7 @@ public static class World var watch = Stopwatch.StartNew(); logger.Information("Writing world save snapshot"); - // Dedupe the types - while (SerializedTypes.TryDequeue(out var type)) - { - _typesSet.Add(type); - } - - Persistence.WriteSnapshotAll(snapshotPath, _typesSet); - - _typesSet.Clear(); + Persistence.WriteSnapshotAll(snapshotPath); try { @@ -409,9 +365,6 @@ public static class World BroadcastStaff(0x35, true, "Writing world save snapshot failed! Check the logs!"); } - // Clear types - SerializedTypes.Clear(); - _diskWriteHandle.Set(); Core.LoopContext.Post(FinishWorldSave); }