Skip to content

Commit

Permalink
Add auto purging of invokersHolderManagerCreators field in EventManager
Browse files Browse the repository at this point in the history
  • Loading branch information
Enderlook committed Jun 24, 2022
1 parent 4b081eb commit 0e17bdc
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 12 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion Enderlook.EventManager/Enderlook.EventManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Product>Enderlook.EventManager</Product>
<RepositoryUrl>https://github.com/Enderlook/Net-Event-Manager</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>0.4.2</Version>
<Version>0.4.3</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<LangVersion>10</LangVersion>
Expand Down
35 changes: 31 additions & 4 deletions Enderlook.EventManager/src/Dictionary2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<TKey, TValue> item)
{
Entry[]? entries_ = entries;
Expand Down
109 changes: 109 additions & 0 deletions Enderlook.EventManager/src/EventManager.AutoPurger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<EventManager, InvokersHolderManager> 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);
}
}
}
43 changes: 37 additions & 6 deletions Enderlook.EventManager/src/EventManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ namespace Enderlook.EventManager;
/// </summary>
public sealed partial class EventManager : IDisposable
{
// TODO: We could add purging to these.
private static int invokersHolderManagerCreatorsLock;
private static int invokersHolderManagerCreatorsReaders;
private static Dictionary2<Type, Func<EventManager, InvokersHolderManager>> invokersHolderManagerCreators;
private static int invokersHolderManagerCreatorsState;
private static int invokersHolderManagerCreatorsStateLock;
private static int invokersHolderManagerCreatorsPurgeIndex;
private static Dictionary2<Type, (Func<EventManager, InvokersHolderManager> Delegate, int MillisecondsTimestamp)> invokersHolderManagerCreators;
private static Type[]? one;

// Value type is actually InvokersHolder<TEvent, TInvoke>.
Expand Down Expand Up @@ -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<EventManager, InvokersHolderManager>? creator;
if (invokersHolderManagerCreators.TryGetValue(type, out creator))
ref (Func<EventManager, InvokersHolderManager> 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--;
Expand All @@ -250,7 +260,7 @@ private InvokersHolderManager CreateInvokersHolderManagerDynamic(Type type)
}
}

ref Func<EventManager, InvokersHolderManager> value = ref invokersHolderManagerCreators.GetOrCreateValueSlot(type, out bool found);
ref (Func<EventManager, InvokersHolderManager> Delegate, int MillisecondsTimestamp) value2 = ref invokersHolderManagerCreators.GetOrCreateValueSlot(type, out found);
if (!found)
{
MethodInfo? methodInfo = typeof(EventManager).GetMethod(nameof(CreateInvokersHolderManager), BindingFlags.NonPublic | BindingFlags.Instance);
Expand All @@ -260,16 +270,37 @@ private InvokersHolderManager CreateInvokersHolderManagerDynamic(Type type)
MethodInfo methodInfoFull = methodInfo.MakeGenericMethod(array);
array[0] = null!;
one = array;
value = creator = (Func<EventManager, InvokersHolderManager>)methodInfoFull.CreateDelegate(typeof(Func<EventManager, InvokersHolderManager>));
value2.Delegate = creator = (Func<EventManager, InvokersHolderManager>)methodInfoFull.CreateDelegate(typeof(Func<EventManager, InvokersHolderManager>));
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)]
Expand Down

0 comments on commit 0e17bdc

Please sign in to comment.