fix: Fixes serialization threading, moves world save to end of loop, and eliminates Parallel.ForEach. (#1530)

This commit is contained in:
Kamron Batman 2023-10-03 00:42:49 -07:00 • committed by GitHub
parent e18115dd94
commit 1f0acddc46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 228 additions and 58 deletions

View file

@ -0,0 +1,24 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: IWorldSerializable.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Concurrent;
namespace Server;
public interface IGenericSerializable
{
void Serialize(ConcurrentQueue<Type> types);
}

View file

@ -17,6 +17,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using Server.Guilds;
@ -30,6 +31,7 @@ public enum WorldState
Initial,
Loading,
Running,
PendingSave,
Saving,
WritingSave
}
@ -38,12 +40,14 @@ public static class World
{
private static ILogger logger = LogFactory.GetLogger(typeof(World));
private static ItemPersistence _itemPersistence = new();
private static MobilePersistence _mobilePersistence = new();
private static GenericEntityPersistence<BaseGuild> _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF);
private static readonly ItemPersistence _itemPersistence = new();
private static readonly MobilePersistence _mobilePersistence = new();
private static readonly GenericEntityPersistence<BaseGuild> _guildPersistence = new("Guilds", 3, 1, 0x7FFFFFFF);
private static ManualResetEvent m_DiskWriteHandle = new(true);
private static ConcurrentQueue<Item> _decayQueue = new();
private static int _threadId;
private static readonly SerializationThreadWorker[] _threadWorkers = new SerializationThreadWorker[Environment.ProcessorCount - 1];
private static readonly ManualResetEvent _diskWriteHandle = new(true);
private static readonly ConcurrentQueue<Item> _decayQueue = new();
private static string _tempSavePath; // Path to the temporary folder for the save
@ -78,7 +82,7 @@ public static class World
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WaitForWriteCompletion()
{
m_DiskWriteHandle.WaitOne();
_diskWriteHandle.WaitOne();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -177,14 +181,18 @@ public static class World
Mobiles.Count,
watch.Elapsed.TotalSeconds
);
// Create the serialization threads.
for (var i = 0; i < _threadWorkers.Length; i++)
{
_threadWorkers[i] = new SerializationThreadWorker();
}
}
private static void FinishWorldSave()
{
WorldState = WorldState.Running;
ProcessDecay();
Persistence.PostSerializeAll(); // Process safety queues
}
@ -223,6 +231,7 @@ public static class World
{
EventSink.InvokeWorldSavePostSnapshot(SavePath, tempPath);
PathUtility.MoveDirectory(tempPath, SavePath);
Directory.SetLastWriteTimeUtc(SavePath, Core.Now);
}
catch (Exception ex)
{
@ -233,7 +242,7 @@ public static class World
// Clear types
SerializedTypes.Clear();
m_DiskWriteHandle.Set();
_diskWriteHandle.Set();
Core.LoopContext.Post(FinishWorldSave);
}
@ -294,9 +303,25 @@ public static class World
WaitForWriteCompletion(); // Blocks Save until current disk flush is done.
WorldState = WorldState.Saving;
_diskWriteHandle.Reset();
m_DiskWriteHandle.Reset();
// Start our serialization threads
for (var i = 0; i < _threadWorkers.Length; i++)
{
_threadWorkers[i].Wake();
}
WorldState = WorldState.PendingSave;
Core.RequestSnapshot();
}
internal static TimeSpan Snapshot()
{
if (WorldState != WorldState.PendingSave)
{
return TimeSpan.Zero;
}
Broadcast(0x35, true, "The world is saving, please wait.");
@ -310,6 +335,13 @@ public static class World
{
_serializationStart = Core.Now;
Persistence.SerializeAll();
// Pause the workers
foreach (var worker in _threadWorkers)
{
worker.Sleep();
}
EventSink.InvokeWorldSave();
}
catch (Exception ex)
@ -319,28 +351,44 @@ public static class World
WorldState = WorldState.WritingSave;
watch.Stop();
if (exception == null)
{
var duration = watch.Elapsed.TotalSeconds;
logger.Information("Saving world {Status} ({Duration:F2} seconds)", "done", duration);
// Only broadcast if it took at least 150ms
if (duration >= 0.15)
{
Broadcast(0x35, true, $"World Save completed in {duration:F2} seconds.");
}
Broadcast(0x35, true, $"World save completed in {duration:F2} seconds.");
}
else
{
logger.Error(exception, "Saving world {Status}", "failed");
Persistence.TraceException(exception);
BroadcastStaff(0x35, true, "World save failed.");
BroadcastStaff(0x35, true, "World save failed! Check the logs!");
}
ThreadPool.QueueUserWorkItem(WriteFiles);
watch.Stop();
return watch.Elapsed;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void PushToCache(IGenericSerializable e)
{
_threadWorkers[_threadId++].Push(e);
if (_threadId == _threadWorkers.Length)
{
_threadId = 0;
}
}
internal static void SleepSerializationThreads()
{
for (var i = 0; i < _threadWorkers.Length; i++)
{
_threadWorkers[i].Sleep();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -414,16 +462,6 @@ public static class World
{
}
protected override void SerializeEntity(Item item)
{
if (item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart)
{
EnqueueForDecay(item);
}
((ISerializable)item).Serialize(SerializedTypes);
}
public override void PostDeserialize()
{
base.PostDeserialize();
@ -438,6 +476,26 @@ public static class World
item.ClearProperties();
}
}
public override void WriteSnapshot(string basePath)
{
base.WriteSnapshot(basePath);
foreach (var item in EntitiesBySerial.Values)
{
if (item.CanDecay() && item.LastMoved + item.DecayTime <= _serializationStart)
{
EnqueueForDecay(item);
}
}
}
public override void PostSerialize()
{
ProcessDecay(); // Run this before the safety queue
base.PostSerialize();
}
}
private class MobilePersistence : GenericEntityPersistence<Mobile>
@ -459,4 +517,70 @@ public static class World
}
}
}
private class SerializationThreadWorker
{
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 readonly ConcurrentQueue<IGenericSerializable> _entities;
public SerializationThreadWorker()
{
_startEvent = new AutoResetEvent(false);
_stopEvent = new AutoResetEvent(false);
_entities = new ConcurrentQueue<IGenericSerializable>();
_thread = new Thread(Execute);
_thread.Start(this);
}
public void Wake()
{
_startEvent.Set();
}
public void Sleep()
{
Volatile.Write(ref _pause, true);
_stopEvent.WaitOne();
}
public void Push(IGenericSerializable entity)
{
_entities.Enqueue(entity);
}
private static void Execute(object obj)
{
var serializedTypes = SerializedTypes;
SerializationThreadWorker worker = (SerializationThreadWorker)obj;
var reader = worker._entities;
while (worker._startEvent.WaitOne())
{
while (true)
{
bool pauseRequested = Volatile.Read(ref worker._pause);
if (reader.TryDequeue(out var entity))
{
entity.Serialize(serializedTypes);
}
else if (pauseRequested) // Break when finished
{
break;
}
}
worker._stopEvent.Set(); // Allow the main thread to continue now that we are finished
worker._pause = false;
if (Core.Closing)
{
return;
}
}
}
}
}