This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View File

@ -0,0 +1,45 @@
// API consistent with Microsoft's ObjectPool<T>.
// thread safe.
using System.Runtime.CompilerServices;
namespace Mirror
{
public static class ConcurrentNetworkWriterPool
{
// initial capacity to avoid allocations in the first few frames
// 1000 * 1200 bytes = around 1 MB.
public const int InitialCapacity = 1000;
// reuse ConcurrentPool<T>
// we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the
// position before reusing.
// this is also more consistent with NetworkReaderPool where we need to
// assign the internal buffer before reusing.
static readonly ConcurrentPool<ConcurrentNetworkWriterPooled> pool =
new ConcurrentPool<ConcurrentNetworkWriterPooled>(
// new object function
() => new ConcurrentNetworkWriterPooled(),
// initial capacity to avoid allocations in the first few frames
// 1000 * 1200 bytes = around 1 MB.
InitialCapacity
);
// pool size access for debugging & tests
public static int Count => pool.Count;
public static ConcurrentNetworkWriterPooled Get()
{
// grab from pool & reset position
ConcurrentNetworkWriterPooled writer = pool.Get();
writer.Position = 0;
return writer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(ConcurrentNetworkWriterPooled writer)
{
pool.Return(writer);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: fdf46e334f52400c854c9732f6fcf005
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs
uploadId: 736421

View File

@ -0,0 +1,10 @@
using System;
namespace Mirror
{
/// <summary>Pooled (not threadsafe) NetworkWriter used from Concurrent pool (thread safe). Automatically returned to concurrent pool when using 'using'</summary>
public sealed class ConcurrentNetworkWriterPooled : NetworkWriter, IDisposable
{
public void Dispose() => ConcurrentNetworkWriterPool.Return(this);
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9163d963b36b4e389318f312bfd8e488
timeCreated: 1691485295
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs
uploadId: 736421

View File

@ -0,0 +1,44 @@
// Pool to avoid allocations (from libuv2k)
// API consistent with Microsoft's ObjectPool<T>.
// concurrent for thread safe access.
//
// currently not in use. keep it in case we need it again.
using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace Mirror
{
public class ConcurrentPool<T>
{
// Mirror is single threaded, no need for concurrent collections
// concurrent bag is for items who's order doesn't matter.
// just about right for our use case here.
readonly ConcurrentBag<T> objects = new ConcurrentBag<T>();
// some types might need additional parameters in their constructor, so
// we use a Func<T> generator
readonly Func<T> objectGenerator;
public ConcurrentPool(Func<T> objectGenerator, int initialCapacity)
{
this.objectGenerator = objectGenerator;
// allocate an initial pool so we have fewer (if any)
// allocations in the first few frames (or seconds).
for (int i = 0; i < initialCapacity; ++i)
objects.Add(objectGenerator());
}
// take an element from the pool, or create a new one if empty
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Get() => objects.TryTake(out T obj) ? obj : objectGenerator();
// return an element to the pool
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Return(T item) => objects.Add(item);
// count to see how many objects are in the pool. useful for tests.
public int Count => objects.Count;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ed304bd790ff478ca37233f66d04d1c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Threading/ConcurrentPool.cs
uploadId: 736421

View File

@ -0,0 +1,112 @@
// threaded Debug.Log support (mischa 2022)
//
// Editor shows Debug.Logs from different threads.
// Builds don't show Debug.Logs from different threads.
//
// need to hook into logMessageReceivedThreaded to receive them in builds too.
using System.Collections.Concurrent;
using System.Threading;
using UnityEngine;
namespace Mirror
{
public static class ThreadLog
{
// queue log messages from threads
struct LogEntry
{
public int threadId;
public LogType type;
public string message;
public string stackTrace;
public LogEntry(int threadId, LogType type, string message, string stackTrace)
{
this.threadId = threadId;
this.type = type;
this.message = message;
this.stackTrace = stackTrace;
}
}
// ConcurrentQueue allocations are fine here.
// logs allocate anywway.
static readonly ConcurrentQueue<LogEntry> logs =
new ConcurrentQueue<LogEntry>();
// main thread id
static int mainThreadId;
#if !UNITY_EDITOR
// Editor as of Unity 2021 does log threaded messages.
// only builds don't.
// do nothing in editor, otherwise we would log twice.
// before scene load ensures thread logs are all caught.
// otherwise some component's Awake may be called before we hooked it up.
// for example, ThreadedTransport's early logs wouldn't be caught.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Initialize()
{
// set main thread id
mainThreadId = Thread.CurrentThread.ManagedThreadId;
// receive threaded log calls
Application.logMessageReceivedThreaded -= OnLog; // remove old first. TODO unnecessary?
Application.logMessageReceivedThreaded += OnLog;
// process logs on main thread Update
NetworkLoop.OnLateUpdate -= OnLateUpdate; // remove old first. TODO unnecessary?
NetworkLoop.OnLateUpdate += OnLateUpdate;
// log for debugging
Debug.Log("ThreadLog initialized.");
}
#endif
static bool IsMainThread() =>
Thread.CurrentThread.ManagedThreadId == mainThreadId;
// callback runs on the same thread where the Debug.Log is called.
// we can use this to buffer messages for main thread here.
static void OnLog(string message, string stackTrace, LogType type)
{
// only enqueue messages from other threads.
// otherwise OnLateUpdate main thread logging would be enqueued
// as well, causing deadlock.
if (IsMainThread()) return;
// queue for logging from main thread later
logs.Enqueue(new LogEntry(Thread.CurrentThread.ManagedThreadId, type, message, stackTrace));
}
static void OnLateUpdate()
{
// process queued logs on main thread
while (logs.TryDequeue(out LogEntry entry))
{
switch (entry.type)
{
// add [Thread#] prefix to make it super obvious where this log message comes from.
// some projects may see unexpected messages that were previously hidden,
// since Unity wouldn't log them without ThreadLog.cs.
case LogType.Log:
Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Warning:
Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Error:
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Exception:
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Assert:
Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
}
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 22360406b3844808b0a305486758a703
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Threading/ThreadLog.cs
uploadId: 736421

View File

@ -0,0 +1,169 @@
// worker thread for Unity (mischa 2022)
// thread with proper exception handling, profling, init, cleanup, etc. for Unity.
// use this from main thread.
using System;
using System.Diagnostics;
using System.Threading;
using UnityEngine.Profiling;
using Debug = UnityEngine.Debug;
namespace Mirror
{
public class WorkerThread
{
readonly Thread thread;
protected volatile bool active;
// stopwatch so we don't need to use Unity's Time (engine independent)
readonly Stopwatch watch = new Stopwatch();
// callbacks need to be set after constructor.
// inheriting classes can't pass their member funcs to base ctor.
// don't set them while the thread is running!
// -> Tick() returns a bool so it can easily stop the thread
// without needing to throw InterruptExceptions or similar.
public Action Init;
public Func<bool> Tick;
public Action Cleanup;
public WorkerThread(string identifier)
{
// start the thread wrapped in safety guard
// if main application terminates, this thread needs to terminate too
thread = new Thread(
() => Guard(identifier)
);
thread.IsBackground = true;
}
public void Start()
{
// only if thread isn't already running
if (thread.IsAlive)
{
Debug.LogWarning("WorkerThread is still active, can't start it again.");
return;
}
active = true;
thread.Start();
}
// signal the thread to stop gracefully.
// returns immediately, but the thread may take a while to stop.
// may be overwritten to clear more flags like 'computing' etc.
public virtual void SignalStop() => active = false;
// wait for the thread to fully stop
public bool StopBlocking(float timeout)
{
// only if alive
if (!thread.IsAlive) return true;
// double precision for long running servers.
watch.Restart();
// signal to stop
SignalStop();
// wait while thread is still alive
while (IsAlive)
{
// simply wait..
Thread.Sleep(0);
// deadlock detection
if (watch.Elapsed.TotalSeconds >= timeout)
{
// force kill all threads as last resort to stop them.
// return false to indicate deadlock.
Interrupt();
return false;
}
}
return true;
}
public bool IsAlive => thread.IsAlive;
// signal an interrupt in the thread.
// this function is very safe to use.
// https://stackoverflow.com/questions/5950994/thread-abort-vs-thread-interrupt
//
// note this does not always kill the thread:
// "If this thread is not currently blocked in a wait, sleep, or join
// state, it will be interrupted when it next begins to block."
// https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=net-6.0
//
// in other words, "while (true) {}" wouldn't throw an interrupt exception.
// and that's _okay_. using interrupt is safe & best practice.
// => Unity still aborts deadlocked threads on script reload.
// => and we catch + warn on AbortException.
public void Interrupt() => thread.Interrupt();
// thread constructor needs callbacks.
// always define them, and make them call actions.
// those can be set at any time.
void OnInit() => Init?.Invoke();
bool OnTick() => Tick?.Invoke() ?? false;
void OnCleanup() => Cleanup?.Invoke();
// guarded wrapper for thread code.
// catches exceptions which would otherwise be silent.
// shows in Unity profiler.
// etc.
public void Guard(string identifier)
{
try
{
// log when work begins = thread starts.
// very important for debugging threads.
Debug.Log($"{identifier}: started.");
// show this thread in Unity profiler
Profiler.BeginThreadProfiling("Mirror Worker Threads", $"{identifier}");
// run init once
OnInit();
// run thread func while active
while (active)
{
// Tick() returns a bool so it can easily stop the thread
// without needing to throw InterruptExceptions or similar.
if (!OnTick()) break;
}
}
// Thread.Interrupt() will gracefully raise a InterruptedException.
catch (ThreadInterruptedException)
{
Debug.Log($"{identifier}: interrupted. That's okay.");
}
// Unity domain reload will cause a ThreadAbortException.
// for example, when saving a changed script while in play mode.
catch (ThreadAbortException)
{
Debug.LogWarning($"{identifier}: aborted. This may happen after domain reload. That's okay.");
}
catch (Exception e)
{
Debug.LogException(e);
}
finally
{
// run cleanup (if any)
active = false;
OnCleanup();
// remove this thread from Unity profiler
Profiler.EndThreadProfiling();
// log when work ends = thread terminates.
// very important for debugging threads.
// 'finally' to log no matter what (even if exceptions)
Debug.Log($"{identifier}: ended.");
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 605fa1d7e32f40a08e5549bb43fc5c07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Threading/WorkerThread.cs
uploadId: 736421