From 0d67bd732d951b48c53e963ffcc6772b47321bc8 Mon Sep 17 00:00:00 2001
From: Elie Bariche <33458222+ebariche@users.noreply.github.com>
Date: Thu, 30 Nov 2023 11:14:05 -0500
Subject: [PATCH] feat: InstanceTracker for FrameworkTemplatePool
---
.../FrameworkTemplatePool.InstanceTracker.cs | 127 ++++++++++++++++++
src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs | 66 ++++++++-
src/Uno.UI/UI/Xaml/UIElement.skia.cs | 16 ---
src/Uno.UI/UI/Xaml/UIElement.wasm.cs | 24 ----
4 files changed, 188 insertions(+), 45 deletions(-)
create mode 100644 src/Uno.UI/UI/Xaml/FrameworkTemplatePool.InstanceTracker.cs
diff --git a/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.InstanceTracker.cs b/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.InstanceTracker.cs
new file mode 100644
index 000000000000..aeccd4529ccd
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.InstanceTracker.cs
@@ -0,0 +1,127 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Runtime;
+using System.Runtime.InteropServices;
+
+namespace Windows.UI.Xaml
+{
+ public partial class FrameworkTemplatePool
+ {
+ ///
+ /// The InstanceTracker allows children to be returned to the .
+ /// It does so by tying the lifetime of the parent to their children using
+ /// and TrackerCookie without creating strong references.
+ /// Once a parent is collected, the cookie becomes eligible for gc, its finalizer will run,
+ /// the child will set its parent to null, making itself available to the pool.
+ ///
+ private static class InstanceTracker
+ {
+ private static readonly Stack _cookiePool = new();
+
+ private static readonly List _handles = new();
+ private static readonly Stack _handlesFreeList = new();
+
+ private static int _counter;
+
+ private const int HandleCleanupInterval = 1024;
+ private const int MaxCookiePoolSize = 256;
+
+ public static void Add(object parent, object instance)
+ {
+ TrackerCookie? cookie;
+
+ lock (_cookiePool)
+ {
+ // Cookies are pooled because we create lots of them.
+ if (_cookiePool.TryPop(out cookie))
+ {
+ cookie.Update(instance);
+ }
+ else
+ {
+ cookie = new TrackerCookie(instance);
+ }
+ }
+
+ // Try to get a free slot in the list, this avoids scanning the list everytime
+ if (_handlesFreeList.TryPop(out var index))
+ {
+ ref var handle = ref CollectionsMarshal.AsSpan(_handles)[index];
+
+ handle = new DependentHandle(parent, cookie);
+ }
+ else
+ {
+ // No slots are available, try to scrub the list, this is necessary because
+ // we don't want to leak handles (coreclr) or ephemerons (mono)
+ if ((_counter++ % HandleCleanupInterval) == 0)
+ {
+ var handles = CollectionsMarshal.AsSpan(_handles);
+
+ for (var x = 0; x < handles.Length; x++)
+ {
+ ref var handle = ref handles[x];
+
+ if (handle.IsAllocated && handle.Target == null)
+ {
+ handle.Dispose();
+
+ _handlesFreeList.Push(x);
+ }
+ }
+
+ // Maybe a slot is available now
+ if (_handlesFreeList.TryPop(out index))
+ {
+ ref var handle = ref handles[index];
+
+ handle = new DependentHandle(parent, cookie);
+
+ return;
+ }
+ }
+
+ // No slots are available
+ _handles.Add(new DependentHandle(parent, cookie));
+ }
+ }
+
+ public static void TryReturnCookie(TrackerCookie cookie)
+ {
+ lock (_cookiePool)
+ {
+ // The pool isn't full, resurrect the cookie so its finalizer will run again
+ if (_cookiePool.Count < MaxCookiePoolSize)
+ {
+ GC.ReRegisterForFinalize(cookie);
+
+ _cookiePool.Push(cookie);
+ }
+ }
+ }
+
+ public class TrackerCookie
+ {
+ private object? _instance;
+
+ public TrackerCookie(object instance)
+ {
+ _instance = instance;
+ }
+
+ ~TrackerCookie()
+ {
+ Instance.RaiseOnParentCollected(_instance!);
+
+ _instance = null;
+
+ TryReturnCookie(this);
+ }
+
+ public void Update(object instance) => _instance = instance;
+ }
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs b/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs
index 6ecf8b7e2441..3eba6f9c59a9 100644
--- a/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs
+++ b/src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs
@@ -6,12 +6,9 @@
using System;
using System.Collections.Generic;
-using System.Text;
-using System.Diagnostics;
-using System.Threading.Tasks;
-using System.Linq;
using Uno.Diagnostics.Eventing;
using Windows.UI.Xaml;
+using Uno.Buffers;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI;
@@ -72,7 +69,7 @@ namespace Windows.UI.Xaml
/// are strictly databound, but not if the control is using stateful code-behind. This is why this behavior can be disabled via
/// if the pooling interferes with the normal behavior of a control.
///
- public class FrameworkTemplatePool
+ public partial class FrameworkTemplatePool
{
internal static FrameworkTemplatePool Instance { get; } = new FrameworkTemplatePool();
public static class TraceProvider
@@ -284,6 +281,63 @@ private List GetTemplatePool(FrameworkTemplate template)
return instances;
}
+ private Stack