Skip to content

Commit

Permalink
feat: InstanceTracker for FrameworkTemplatePool
Browse files Browse the repository at this point in the history
  • Loading branch information
ebariche committed Dec 7, 2023
1 parent 72dfb91 commit 0d67bd7
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 45 deletions.
127 changes: 127 additions & 0 deletions src/Uno.UI/UI/Xaml/FrameworkTemplatePool.InstanceTracker.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The InstanceTracker allows children to be returned to the <see cref="FrameworkTemplatePool"/>.
/// It does so by tying the lifetime of the parent to their children using <see cref="DependentHandle"/>
/// and <see cref="TrackerCookie">TrackerCookie</see> 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.
/// </summary>
private static class InstanceTracker
{
private static readonly Stack<TrackerCookie> _cookiePool = new();

private static readonly List<DependentHandle> _handles = new();
private static readonly Stack<int> _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;
}
}
}
}
66 changes: 61 additions & 5 deletions src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <see cref="IsPoolingEnabled"/>
/// if the pooling interferes with the normal behavior of a control.
/// </remarks>
public class FrameworkTemplatePool
public partial class FrameworkTemplatePool
{
internal static FrameworkTemplatePool Instance { get; } = new FrameworkTemplatePool();
public static class TraceProvider
Expand Down Expand Up @@ -284,6 +281,63 @@ private List<TemplateEntry> GetTemplatePool(FrameworkTemplate template)
return instances;
}

private Stack<object> _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<object>.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<object>.Shared.Return(array, clearArray: true);
}

if (shouldRequeue)
{
NativeDispatcher.Main.Enqueue(Recycle);
}
}

/// <summary>
/// Manually return an unused template root to the pool.
/// </summary>
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 0 additions & 16 deletions src/Uno.UI/UI/Xaml/UIElement.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ public UIElement()
UpdateHitTest();
}

~UIElement()
{
Cleanup();
}

public bool UseLayoutRounding
{
get => (bool)this.GetValue(UseLayoutRoundingProperty);
Expand Down Expand Up @@ -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
Expand Down
24 changes: 0 additions & 24 deletions src/Uno.UI/UI/Xaml/UIElement.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 0d67bd7

Please sign in to comment.