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