From 0e17bdc30cca60b8f3b60d92ea2f46831f80b514 Mon Sep 17 00:00:00 2001 From: Enderlook Date: Thu, 23 Jun 2022 16:37:19 -0300 Subject: [PATCH] Add auto purging of invokersHolderManagerCreators field in EventManager --- CHANGELOG.md | 3 +- .../Enderlook.EventManager.csproj | 2 +- Enderlook.EventManager/src/Dictionary2.cs | 35 +++++- .../src/EventManager.AutoPurger.cs | 109 ++++++++++++++++++ Enderlook.EventManager/src/EventManager.cs | 43 ++++++- 5 files changed, 180 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a4315..a7649e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## +## v0.4.3 - Fix and improve documentation. - Fix racing condition in `DynamicRaise` method. - Improve performance by avoiding some array index checks. - Improve incremental capabilities of auto purger when canceled. +- Improve auto purger to avoid a small memory leak produced by storing some internal values in a static field. ## v0.4.2 diff --git a/Enderlook.EventManager/Enderlook.EventManager.csproj b/Enderlook.EventManager/Enderlook.EventManager.csproj index 60edde1..a8f17e8 100644 --- a/Enderlook.EventManager/Enderlook.EventManager.csproj +++ b/Enderlook.EventManager/Enderlook.EventManager.csproj @@ -10,7 +10,7 @@ Enderlook.EventManager https://github.com/Enderlook/Net-Event-Manager git - 0.4.2 + 0.4.3 true true 10 diff --git a/Enderlook.EventManager/src/Dictionary2.cs b/Enderlook.EventManager/src/Dictionary2.cs index 8da2492..40b6e60 100644 --- a/Enderlook.EventManager/src/Dictionary2.cs +++ b/Enderlook.EventManager/src/Dictionary2.cs @@ -342,11 +342,11 @@ public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Get(TKey key) + public ref TValue TryFind(TKey key, out bool found) { - ref TValue value = ref FindValue(key); - Debug.Assert(!Utils.IsNullRef(ref value)); - return value; + ref TValue valRef = ref FindValue(key); + found = !Utils.IsNullRef(ref valRef); + return ref valRef; } private ref TValue FindValue(TKey key) @@ -619,6 +619,33 @@ public bool TryGetFromIndex(int index, [NotNullWhen(true)] out TValue value) return false; } + public bool TryGetFromIndex(int index, [NotNullWhen(true)] out TKey key, [NotNullWhen(true)] out TValue value) + { + Debug.Assert(index < count, "Index out of range."); + + Entry[]? entries_ = entries; + Debug.Assert(entries_ is not null); + ref Entry entries_Root = ref Utils.GetArrayDataReference(entries_); + Debug.Assert(index < entries_.Length, "Index out of range."); + ref Entry entry = ref Unsafe.Add(ref entries_Root, index); + + if (entry.Next >= -1) + { + value = entry.Value!; + key = entry.Key!; + return true; + } + +#if NET5_0_OR_GREATER + Unsafe.SkipInit(out key); + Unsafe.SkipInit(out value); +#else + key = default; + value = default; +#endif + return false; + } + public bool MoveNext(ref int index, out KeyValuePair item) { Entry[]? entries_ = entries; diff --git a/Enderlook.EventManager/src/EventManager.AutoPurger.cs b/Enderlook.EventManager/src/EventManager.AutoPurger.cs index 8818a6f..22bd41b 100644 --- a/Enderlook.EventManager/src/EventManager.AutoPurger.cs +++ b/Enderlook.EventManager/src/EventManager.AutoPurger.cs @@ -181,6 +181,12 @@ private sealed class AutoPurger { private readonly GCHandle handle; + static AutoPurger() + { + // We use this static constructor instead of EventManager's static constructor to prevent adding an static constructor in a public type. + StaticAutoPurger _ = new(); + } + public AutoPurger(EventManager manager) => handle = GCHandle.Alloc(manager, GCHandleType.WeakTrackResurrection); ~AutoPurger() @@ -201,4 +207,107 @@ private sealed class AutoPurger } } } + + private sealed class StaticAutoPurger + { + ~StaticAutoPurger() + { + const int LowAfterMilliseconds = 240 * 1000; // Trim after 240 seconds for low pressure. + const int MediumAfterMilliseconds = 120 * 1000; // Trim after 120 seconds for medium pressure. + + for (int j = 0; j < PurgeAttempts; j++) + { + // Check if dictionary was not used. + if (invokersHolderManagerCreators.EndIndex == 0) + return; + + // Get write lock. + Lock(ref invokersHolderManagerCreatorsLock); + if (invokersHolderManagerCreatorsReaders != 0) + { + Unlock(ref invokersHolderManagerCreatorsLock); + while (true) + { + Lock(ref invokersHolderManagerCreatorsLock); + if (invokersHolderManagerCreatorsReaders > 0) + Unlock(ref invokersHolderManagerCreatorsLock); + else + break; + if ((invokersHolderManagerCreatorsState & IS_CANCELLATION_REQUESTED) != 0) + goto yield; + } + } + + Lock(ref invokersHolderManagerCreatorsStateLock); + { + if (invokersHolderManagerCreatorsStateLock == IS_CANCELLATION_REQUESTED) + { + Unlock(ref invokersHolderManagerCreatorsStateLock); + goto exit; + } + invokersHolderManagerCreatorsStateLock = IS_PURGING; + } + Unlock(ref invokersHolderManagerCreatorsStateLock); + + int currentMilliseconds = Environment.TickCount; + MemoryPressure memoryPressure = Utils.GetMemoryPressure(); + int trimMilliseconds = memoryPressure switch + { + MemoryPressure.Low => LowAfterMilliseconds, + MemoryPressure.Medium => MediumAfterMilliseconds, + _ => 0, + }; + + int purgeIndex_ = invokersHolderManagerCreatorsPurgeIndex; + int end = invokersHolderManagerCreators.EndIndex; + int count; + if (purgeIndex_ >= end) + { + purgeIndex_ = 0; + count = end; + } + else + count = end - purgeIndex_; + + for (; count > 0; count--) + { + if ((invokersHolderManagerCreatorsState & IS_CANCELLATION_REQUESTED) == 0) + { + if (invokersHolderManagerCreators.TryGetFromIndex(purgeIndex_, out Type key, out (Func Delegate, int MillisecondsTimestamp) value) + && (currentMilliseconds - value.MillisecondsTimestamp) > trimMilliseconds) + { + invokersHolderManagerCreators.Remove(key); + continue; + } + purgeIndex_++; + if (purgeIndex_ == end) + purgeIndex_ = 0; + } + else + break; + } + + if ((invokersHolderManagerCreatorsState & IS_CANCELLATION_REQUESTED) == 0) + invokersHolderManagerCreators.TryShrink(); + + invokersHolderManagerCreatorsPurgeIndex = purgeIndex_; + + exit: + // Release write lock. + Unlock(ref invokersHolderManagerCreatorsLock); + + if ((invokersHolderManagerCreatorsState & IS_CANCELLATION_REQUESTED) == 0) + { + invokersHolderManagerCreatorsState = 0; + break; + } + + yield: + if (!Thread.Yield()) + Thread.Sleep(PurgeSleepMilliseconds); + } + + GC.ReRegisterForFinalize(this); + } + } } diff --git a/Enderlook.EventManager/src/EventManager.cs b/Enderlook.EventManager/src/EventManager.cs index 6fe0c47..e48f0bc 100644 --- a/Enderlook.EventManager/src/EventManager.cs +++ b/Enderlook.EventManager/src/EventManager.cs @@ -13,10 +13,12 @@ namespace Enderlook.EventManager; /// public sealed partial class EventManager : IDisposable { - // TODO: We could add purging to these. private static int invokersHolderManagerCreatorsLock; private static int invokersHolderManagerCreatorsReaders; - private static Dictionary2> invokersHolderManagerCreators; + private static int invokersHolderManagerCreatorsState; + private static int invokersHolderManagerCreatorsStateLock; + private static int invokersHolderManagerCreatorsPurgeIndex; + private static Dictionary2 Delegate, int MillisecondsTimestamp)> invokersHolderManagerCreators; private static Type[]? one; // Value type is actually InvokersHolder. @@ -219,14 +221,22 @@ void SlowPath() [MethodImpl(MethodImplOptions.NoInlining)] private InvokersHolderManager CreateInvokersHolderManagerDynamic(Type type) { + if (invokersHolderManagerCreatorsState != 0) + TryRequestCancellation(); + // Get read lock. Lock(ref invokersHolderManagerCreatorsLock); invokersHolderManagerCreatorsReaders++; Unlock(ref invokersHolderManagerCreatorsLock); Func? creator; - if (invokersHolderManagerCreators.TryGetValue(type, out creator)) + ref (Func Delegate, int MillisecondsTimestamp) value = ref invokersHolderManagerCreators.TryFind(type, out bool found); + if (found) { + creator = value.Delegate; + // Atomic swap. + value.MillisecondsTimestamp = Environment.TickCount; + // Release read lock. Lock(ref invokersHolderManagerCreatorsLock); invokersHolderManagerCreatorsReaders--; @@ -250,7 +260,7 @@ private InvokersHolderManager CreateInvokersHolderManagerDynamic(Type type) } } - ref Func value = ref invokersHolderManagerCreators.GetOrCreateValueSlot(type, out bool found); + ref (Func Delegate, int MillisecondsTimestamp) value2 = ref invokersHolderManagerCreators.GetOrCreateValueSlot(type, out found); if (!found) { MethodInfo? methodInfo = typeof(EventManager).GetMethod(nameof(CreateInvokersHolderManager), BindingFlags.NonPublic | BindingFlags.Instance); @@ -260,16 +270,37 @@ private InvokersHolderManager CreateInvokersHolderManagerDynamic(Type type) MethodInfo methodInfoFull = methodInfo.MakeGenericMethod(array); array[0] = null!; one = array; - value = creator = (Func)methodInfoFull.CreateDelegate(typeof(Func)); + value2.Delegate = creator = (Func)methodInfoFull.CreateDelegate(typeof(Func)); + value2.MillisecondsTimestamp = Environment.TickCount; } else - creator = value; + { + creator = value.Delegate; + value2.MillisecondsTimestamp = Environment.TickCount; + } // Release write lock. Unlock(ref invokersHolderManagerCreatorsLock); } return creator(this); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryRequestCancellation() + { + int invokersHolderManagerCreatorsState_ = invokersHolderManagerCreatorsState; + if ((invokersHolderManagerCreatorsState_ & IS_PURGING) != 0) + { + Lock(ref invokersHolderManagerCreatorsStateLock); + { + invokersHolderManagerCreatorsState_ = invokersHolderManagerCreatorsState; + + if ((invokersHolderManagerCreatorsState_ & IS_PURGING) != 0) + invokersHolderManagerCreatorsState = invokersHolderManagerCreatorsState_ | IS_CANCELLATION_REQUESTED; + } + Unlock(ref invokersHolderManagerCreatorsStateLock); + } + } } [MethodImpl(MethodImplOptions.NoInlining)]