From a5f5b9cfe5b1ce8723ce12e3c41b6aed24136d0e Mon Sep 17 00:00:00 2001
From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com>
Date: Tue, 29 Aug 2023 17:25:54 +0200
Subject: [PATCH] Make Logger.Listeners and Logger.Sources thread safe (#656)
---
BepInEx/Logging/Logger.cs | 155 ++++++++++++++++++++++++++++++++------
1 file changed, 130 insertions(+), 25 deletions(-)
diff --git a/BepInEx/Logging/Logger.cs b/BepInEx/Logging/Logger.cs
index e8fb8b5..5bab5fe 100644
--- a/BepInEx/Logging/Logger.cs
+++ b/BepInEx/Logging/Logger.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
namespace BepInEx.Logging
@@ -11,7 +12,9 @@ namespace BepInEx.Logging
///
/// Collection of all log listeners that receive log events.
///
- public static ICollection Listeners { get; } = new List();
+ public static ICollection Listeners => _Listeners;
+
+ private static readonly LogListenerCollection _Listeners = new LogListenerCollection();
///
/// Collection of all log source that output log events.
@@ -26,18 +29,15 @@ namespace BepInEx.Logging
{
if (internalLogsInitialized)
return;
-
+
Sources.Add(new HarmonyLogSource());
internalLogsInitialized = true;
}
-
+
internal static void InternalLogEvent(object sender, LogEventArgs eventArgs)
{
- foreach (var listener in Listeners)
- {
- listener?.LogEvent(sender, eventArgs);
- }
+ _Listeners.SendLogEvent(sender, eventArgs);
}
///
@@ -71,41 +71,146 @@ namespace BepInEx.Logging
return source;
}
-
- private class LogSourceCollection : List, ICollection
+ private sealed class LogSourceCollection : ThreadSafeCollection, ICollection
{
- void ICollection.Add(ILogSource item)
+ public override void Add(ILogSource item)
{
if (item == null)
throw new ArgumentNullException(nameof(item), "Log sources cannot be null when added to the source list.");
- item.LogEvent += InternalLogEvent;
-
- base.Add(item);
- }
-
- void ICollection.Clear()
- {
- foreach (var item in base.ToArray())
+ lock (Lock)
{
- ((ICollection)this).Remove(item);
+ item.LogEvent += InternalLogEvent;
+
+ var copy = new List(BaseList.Count + 1);
+ copy.AddRange(BaseList);
+ copy.Add(item);
+ BaseList = copy;
}
}
- bool ICollection.Remove(ILogSource item)
+ public override void Clear()
+ {
+ if (Count == 0)
+ return;
+
+ lock (Lock)
+ {
+ for (var i = 0; i < BaseList.Count; i++)
+ BaseList[i].LogEvent -= InternalLogEvent;
+
+ BaseList = new List(0);
+ }
+ }
+
+ public override bool Remove(ILogSource item)
{
if (item == null)
return false;
- if (!base.Contains(item))
+ lock (Lock)
+ {
+ var wasPresent = base.Remove(item);
+ if (wasPresent)
+ item.LogEvent -= InternalLogEvent;
+ return wasPresent;
+ }
+ }
+ }
+
+ private sealed class LogListenerCollection : ThreadSafeCollection
+ {
+ public void SendLogEvent(object sender, LogEventArgs eventArgs)
+ {
+ // Do this instead of foreach to avoid boxing, also very slightly faster
+ var aListInTime = BaseList;
+ for (int i = 0; i < aListInTime.Count; i++)
+ aListInTime[i].LogEvent(sender, eventArgs);
+ }
+ }
+
+ ///
+ /// Simple thread safe list that prioritizes read speed over write speed.
+ /// Read is the same as a normal list, while write locks and allocates a copy of the list.
+ /// Logger lists are rarely updated so this tradeoff should be fine.
+ ///
+ ///
+ private class ThreadSafeCollection : ICollection where T : class
+ {
+ protected readonly object Lock = new object();
+ protected List BaseList = new List(0);
+
+ public IEnumerator GetEnumerator()
+ {
+ // Can't avoid boxing
+ return BaseList.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ // Can't avoid boxing
+ return BaseList.GetEnumerator();
+ }
+
+ public virtual void Add(T item)
+ {
+ if (item == null)
+ throw new ArgumentNullException(nameof(item), "item can't be null");
+
+ lock (Lock)
+ {
+ var copy = new List(BaseList.Count + 1);
+ copy.AddRange(BaseList);
+ copy.Add(item);
+ BaseList = copy;
+ }
+ }
+
+ public virtual void Clear()
+ {
+ if (Count == 0)
+ return;
+
+ lock (Lock)
+ BaseList = new List(0);
+ }
+
+ public bool Contains(T item)
+ {
+ return BaseList.Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ BaseList.CopyTo(array, arrayIndex);
+ }
+
+ public virtual bool Remove(T item)
+ {
+ if (item == null)
return false;
- item.LogEvent -= InternalLogEvent;
+ lock (Lock)
+ {
+ var copy = new List(BaseList.Count);
+ var any = false;
+ for (int i = 0; i < BaseList.Count; i++)
+ {
+ var existingItem = BaseList[i];
+ if (existingItem.Equals(item))
+ any = true;
+ else
+ copy.Add(existingItem);
+ }
- base.Remove(item);
-
- return true;
+ BaseList = copy;
+ return any;
+ }
}
+
+ public int Count => BaseList.Count;
+
+ public bool IsReadOnly => false;
}
}
}
\ No newline at end of file