Skip to content

Commit ce2d8e9

Browse files
committed
Enabled automatic WeakReferenceMessenger trimming
1 parent 5d0d3bf commit ce2d8e9

File tree

1 file changed

+100
-38
lines changed

1 file changed

+100
-38
lines changed

Microsoft.Toolkit.Mvvm/Messaging/WeakReferenceMessenger.cs

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Runtime.CompilerServices;
8+
using System.Threading;
89
using Microsoft.Collections.Extensions;
910
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
1011
#if NETSTANDARD2_0
@@ -13,14 +14,23 @@
1314
using RecipientsTable = System.Runtime.CompilerServices.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
1415
#endif
1516

17+
#pragma warning disable SA1204
18+
1619
namespace Microsoft.Toolkit.Mvvm.Messaging
1720
{
1821
/// <summary>
1922
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
2023
/// </summary>
2124
/// <remarks>
25+
/// <para>
2226
/// This <see cref="IMessenger"/> implementation uses weak references to track the registered
2327
/// recipients, so it is not necessary to manually unregister them when they're no longer needed.
28+
/// </para>
29+
/// <para>
30+
/// The <see cref="WeakReferenceMessenger"/> type will automatically perform internal trimming when
31+
/// full GC collections are invoked, so calling <see cref="Cleanup"/> manually is not necessary to
32+
/// ensure that on average the internal data structures are as trimmed and compact as possible.
33+
/// </para>
2434
/// </remarks>
2535
public sealed class WeakReferenceMessenger : IMessenger
2636
{
@@ -46,6 +56,22 @@ public sealed class WeakReferenceMessenger : IMessenger
4656
/// </summary>
4757
private readonly DictionarySlim<Type2, RecipientsTable> recipientsMap = new();
4858

59+
/// <summary>
60+
/// Initializes a new instance of the <see cref="WeakReferenceMessenger"/> class.
61+
/// </summary>
62+
public WeakReferenceMessenger()
63+
{
64+
// Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the
65+
// current messenger instance is trimmed and without leftover recipient maps that are no longer used.
66+
// This is necessary (as in, some form of cleanup, either explicit or automatic like in this case)
67+
// because the ConditionalWeakTable<TKey, TValue> instances will just remove key-value pairs on their
68+
// own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the Type2 instances
69+
// mapping to each conditional table for a pair of message and token types) to potentially remain in the
70+
// root mapping structure but without any remaining recipients actually registered there, which just
71+
// adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on.
72+
Gen2GcCallback.Register(static obj => ((WeakReferenceMessenger)obj).CleanupWithNonBlockingLock(), this);
73+
}
74+
4975
/// <summary>
5076
/// Gets the default <see cref="WeakReferenceMessenger"/> instance.
5177
/// </summary>
@@ -224,61 +250,97 @@ public void Cleanup()
224250
{
225251
lock (this.recipientsMap)
226252
{
227-
using ArrayPoolBufferWriter<Type2> type2s = ArrayPoolBufferWriter<Type2>.Create();
228-
using ArrayPoolBufferWriter<object> emptyRecipients = ArrayPoolBufferWriter<object>.Create();
253+
CleanupWithoutLock();
254+
}
255+
}
229256

230-
var enumerator = this.recipientsMap.GetEnumerator();
257+
/// <inheritdoc/>
258+
public void Reset()
259+
{
260+
lock (this.recipientsMap)
261+
{
262+
this.recipientsMap.Clear();
263+
}
264+
}
231265

232-
// First, we go through all the currently registered pairs of token and message types.
233-
// These represents all the combinations of generic arguments with at least one registered
234-
// handler, with the exception of those with recipients that have already been collected.
235-
while (enumerator.MoveNext())
266+
/// <summary>
267+
/// Executes a cleanup without locking the current instance. This method has to be
268+
/// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
269+
/// </summary>
270+
private void CleanupWithNonBlockingLock()
271+
{
272+
object lockObject = this.recipientsMap;
273+
bool lockTaken = false;
274+
275+
try
276+
{
277+
Monitor.TryEnter(lockObject, ref lockTaken);
278+
279+
if (lockTaken)
236280
{
237-
emptyRecipients.Reset();
281+
CleanupWithoutLock();
282+
}
283+
}
284+
finally
285+
{
286+
if (lockTaken)
287+
{
288+
Monitor.Exit(lockObject);
289+
}
290+
}
291+
}
238292

239-
bool hasAtLeastOneHandler = false;
293+
/// <summary>
294+
/// Executes a cleanup without locking the current instance. This method has to be
295+
/// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
296+
/// </summary>
297+
private void CleanupWithoutLock()
298+
{
299+
using ArrayPoolBufferWriter<Type2> type2s = ArrayPoolBufferWriter<Type2>.Create();
300+
using ArrayPoolBufferWriter<object> emptyRecipients = ArrayPoolBufferWriter<object>.Create();
240301

241-
// Go through the currently alive recipients to look for those with no handlers left. We track
242-
// the ones we find to remove them outside of the loop (can't modify during enumeration).
243-
foreach (KeyValuePair<object, IDictionarySlim> pair in enumerator.Value)
244-
{
245-
if (pair.Value.Count == 0)
246-
{
247-
emptyRecipients.Add(pair.Key);
248-
}
249-
else
250-
{
251-
hasAtLeastOneHandler = true;
252-
}
253-
}
302+
var enumerator = this.recipientsMap.GetEnumerator();
303+
304+
// First, we go through all the currently registered pairs of token and message types.
305+
// These represents all the combinations of generic arguments with at least one registered
306+
// handler, with the exception of those with recipients that have already been collected.
307+
while (enumerator.MoveNext())
308+
{
309+
emptyRecipients.Reset();
254310

255-
// Remove the handler maps for recipients that are still alive but with no handlers
256-
foreach (object recipient in emptyRecipients.Span)
311+
bool hasAtLeastOneHandler = false;
312+
313+
// Go through the currently alive recipients to look for those with no handlers left. We track
314+
// the ones we find to remove them outside of the loop (can't modify during enumeration).
315+
foreach (KeyValuePair<object, IDictionarySlim> pair in enumerator.Value)
316+
{
317+
if (pair.Value.Count == 0)
257318
{
258-
enumerator.Value.Remove(recipient);
319+
emptyRecipients.Add(pair.Key);
259320
}
260-
261-
// Track the type combinations with no recipients or handlers left
262-
if (!hasAtLeastOneHandler)
321+
else
263322
{
264-
type2s.Add(enumerator.Key);
323+
hasAtLeastOneHandler = true;
265324
}
266325
}
267326

268-
// Remove all the mappings with no handlers left
269-
foreach (Type2 key in type2s.Span)
327+
// Remove the handler maps for recipients that are still alive but with no handlers
328+
foreach (object recipient in emptyRecipients.Span)
329+
{
330+
enumerator.Value.Remove(recipient);
331+
}
332+
333+
// Track the type combinations with no recipients or handlers left
334+
if (!hasAtLeastOneHandler)
270335
{
271-
this.recipientsMap.TryRemove(key);
336+
type2s.Add(enumerator.Key);
272337
}
273338
}
274-
}
275339

276-
/// <inheritdoc/>
277-
public void Reset()
278-
{
279-
lock (this.recipientsMap)
340+
// Remove all the mappings with no handlers left
341+
foreach (Type2 key in type2s.Span)
280342
{
281-
this.recipientsMap.Clear();
343+
this.recipientsMap.TryRemove(key);
282344
}
283345
}
284346

0 commit comments

Comments
 (0)