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 _instancesToRecycle = new(); + + private void RaiseOnParentCollected(object instance) + { + var shouldEnqueue = false; + + lock (_instancesToRecycle) + { + _instancesToRecycle.Push(instance); + + shouldEnqueue = _instancesToRecycle.Count == 1; + } + + if (shouldEnqueue) + { + NativeDispatcher.Main.Enqueue(Recycle); + } + } + + private const int RecycleBatchSize = 32; + + private void Recycle() + { + var array = ArrayPool.Shared.Rent(RecycleBatchSize); + + var count = 0; + + var shouldRequeue = false; + + lock (_instancesToRecycle) + { + while (_instancesToRecycle.TryPop(out var instance) && count < RecycleBatchSize) + { + array[count++] = instance; + } + + shouldRequeue = _instancesToRecycle.Count > 0; + } + + try + { + for (var x = 0; x < count; x++) + { + array[x].SetParent(null); + } + } + finally + { + ArrayPool.Shared.Return(array, clearArray: true); + } + + if (shouldRequeue) + { + NativeDispatcher.Main.Enqueue(Recycle); + } + } + /// /// Manually return an unused template root to the pool. /// @@ -355,6 +409,8 @@ private void TryReuseTemplateRoot(object instance, object? key, object? newParen } else { + InstanceTracker.Add(newParent, instance); + var index = list.FindIndex(e => ReferenceEquals(e.Control, instance)); if (index != -1) diff --git a/src/Uno.UI/UI/Xaml/UIElement.skia.cs b/src/Uno.UI/UI/Xaml/UIElement.skia.cs index 7eba2d545858..0603e971fa8f 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.skia.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.skia.cs @@ -46,11 +46,6 @@ public UIElement() UpdateHitTest(); } - ~UIElement() - { - Cleanup(); - } - public bool UseLayoutRounding { get => (bool)this.GetValue(UseLayoutRoundingProperty); @@ -376,17 +371,6 @@ partial void HideVisual() Visual IVisualElement2.GetVisualInternal() => ElementCompositionPreview.GetElementVisual(this); - private void Cleanup() - { - NativeDispatcher.Main.Enqueue(() => - { - for (var i = 0; i < _children.Count; i++) - { - _children[i].SetParent(null); - } - }, NativeDispatcherPriority.Idle); - } - #if DEBUG public string ShowLocalVisualTree() => this.ShowLocalVisualTree(1000); #endif diff --git a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs index 82008c8be38c..9df30d6ca948 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs @@ -87,8 +87,6 @@ internal UIElement(string htmlTag, bool isSvg) this.Log().Debug($"Collecting UIElement for [{HtmlId}]"); } - Cleanup(); - Uno.UI.Xaml.WindowManagerInterop.DestroyView(HtmlId); } catch (Exception e) @@ -516,28 +514,6 @@ private void RemoveNativeView(UIElement child) } } - private void Cleanup() - { - if (this.GetParent() is UIElement originalParent) - { - originalParent.RemoveChild(this); - } - - if (this is Windows.UI.Xaml.Controls.Panel panel) - { - panel.Children.Clear(); - } - else - { - for (var i = 0; i < _children.Count; i++) - { - RemoveNativeView(_children[i]); - } - - _children.Clear(); - } - } - internal bool RemoveChild(UIElement child) { if (child != null && _children.Remove(child))