Skip to content

Commit

Permalink
feat: Add support for memory usage aware template pooling
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromelaban committed Jan 21, 2022
1 parent 66038f1 commit 6c98490
Show file tree
Hide file tree
Showing 9 changed files with 498 additions and 118 deletions.
232 changes: 232 additions & 0 deletions src/Uno.UI.Tests/Windows_UI_Xaml/Given_FrameworkTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.UI.Dispatching;
using Uno.UI.Tests.App.Xaml;
using Uno.UI.Tests.Helpers;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace Uno.UI.Tests.Windows_UI_Xaml
{
[TestClass]
public class Given_FrameworkTemplate
{
private FrameworkTemplatePoolMockPlatformProvider _mockProvider;
private bool _previousPoolingEnabled;

[TestInitialize]
public void Init()
{
UnitTestsApp.App.EnsureApplication();

_previousPoolingEnabled = FrameworkTemplatePool.IsPoolingEnabled;
FrameworkTemplatePool.Instance.SetPlatformProvider(_mockProvider = new());

FrameworkTemplatePool.IsPoolingEnabled = true;
}

[TestCleanup]
public void Cleanup()
{
FrameworkTemplatePool.IsPoolingEnabled = _previousPoolingEnabled;
FrameworkTemplatePool.Instance.SetPlatformProvider(null);
}

[TestMethod]
public void When_RemoveTemplate()
{
_mockProvider.CanUseMemoryManager = false;

var TemplateCreated = 0;
List<TemplatePoolAwareControl> _created = new List<TemplatePoolAwareControl>();
var dataTemplate = new DataTemplate(() =>
{
TemplateCreated++;
var b = new TemplatePoolAwareControl();
_created.Add(b);
return b;
});

var SUT = new ContentControl()
{
ContentTemplate = dataTemplate
};

var root = new Grid();
root.ForceLoaded();
root.Children.Add(SUT);

Assert.AreEqual(1, TemplateCreated);

SUT.ContentTemplate = null;

Assert.AreEqual(1, TemplateCreated);
Assert.AreEqual(1, _created.Count);
Assert.AreEqual(1, _created[0].TemplateRecycled);
}

[TestMethod]
public void When_RemoveTemplate_And_Reuse()
{
_mockProvider.CanUseMemoryManager = false;

var TemplateCreated = 0;
List<TemplatePoolAwareControl> _created = new List<TemplatePoolAwareControl>();
var dataTemplate = new DataTemplate(() =>
{
TemplateCreated++;
var b = new TemplatePoolAwareControl();
_created.Add(b);
return b;
});

var SUT = new ContentControl()
{
ContentTemplate = dataTemplate
};

var root = new Grid();
root.ForceLoaded();
root.Children.Add(SUT);

Assert.AreEqual(1, TemplateCreated);

SUT.ContentTemplate = null;

Assert.AreEqual(1, TemplateCreated);
Assert.AreEqual(1, _created.Count);
Assert.AreEqual(1, _created[0].TemplateRecycled);
Assert.IsNull(SUT.ContentTemplateRoot);

SUT.ContentTemplate = dataTemplate;

Assert.AreEqual(1, TemplateCreated);
Assert.AreEqual(1, _created.Count);
Assert.AreEqual(1, _created[0].TemplateRecycled);
Assert.IsNotNull(SUT.ContentTemplateRoot);
}

[TestMethod]
public void When_RemoveTemplate_And_Timeout()
{
_mockProvider.CanUseMemoryManager = false;

List<TemplatePoolAwareControl> _created = new();
var dataTemplate = new DataTemplate(() =>
{
var b = new TemplatePoolAwareControl();
_created.Add(b);
return b;
});

var SUT = new ContentControl()
{
ContentTemplate = dataTemplate
};

var root = new Grid();
root.ForceLoaded();
root.Children.Add(SUT);

Assert.AreEqual(1, _created.Count);

SUT.ContentTemplate = null;

Assert.AreEqual(1, _created.Count);
Assert.AreEqual(1, _created[0].TemplateRecycled);
Assert.IsNull(SUT.ContentTemplateRoot);

_mockProvider.Now = TimeSpan.FromMinutes(2);
FrameworkTemplatePool.Instance.Scavenge(false);

SUT.ContentTemplate = dataTemplate;

Assert.AreEqual(2, _created.Count);
Assert.AreEqual(1, _created[0].TemplateRecycled);
Assert.IsNotNull(SUT.ContentTemplateRoot);
}

[TestMethod]
public void When_RemoveTemplate_And_OutOfMemory()
{
_mockProvider.CanUseMemoryManager = true;
_mockProvider.AppMemoryUsageLimit = 100;

List<TemplatePoolAwareControl> _created = new();
var dataTemplate = new DataTemplate(() =>
{
var b = new TemplatePoolAwareControl();
_created.Add(b);
return b;
});

var SUT = new ContentControl()
{
ContentTemplate = dataTemplate
};

var root = new Grid();
root.ForceLoaded();
root.Children.Add(SUT);

Assert.AreEqual(1, _created.Count);

_mockProvider.AppMemoryUsage = 81;

SUT.ContentTemplate = null;

Assert.AreEqual(1, _created.Count);
Assert.AreEqual(0, _created[0].TemplateRecycled);
Assert.IsNull(SUT.ContentTemplateRoot);

_mockProvider.Now = TimeSpan.FromMinutes(2);
FrameworkTemplatePool.Instance.Scavenge(false);

SUT.ContentTemplate = dataTemplate;

Assert.AreEqual(2, _created.Count);
Assert.AreEqual(0, _created[0].TemplateRecycled);
Assert.IsNotNull(SUT.ContentTemplateRoot);

_mockProvider.AppMemoryUsage = 79;

SUT.ContentTemplate = null;

Assert.AreEqual(2, _created.Count);
Assert.AreEqual(0, _created[0].TemplateRecycled);
Assert.AreEqual(1, _created[1].TemplateRecycled);
Assert.IsNull(SUT.ContentTemplateRoot);
}

private class FrameworkTemplatePoolMockPlatformProvider : IFrameworkTemplatePoolPlatformProvider
{
public TimeSpan Now { get; set; }

public bool CanUseMemoryManager { get; set; }

public ulong AppMemoryUsage { get; set; }

public ulong AppMemoryUsageLimit { get; set; }

public Task Delay(TimeSpan duration) => throw new NotImplementedException();
public void Schedule(IdleDispatchedHandler action) => throw new NotImplementedException();
}
}

public class TemplatePoolAwareControl : Grid, IFrameworkTemplatePoolAware
{
public int TemplateRecycled { get; private set; }

public void OnTemplateRecycled()
{
TemplateRecycled++;
}
}
}
18 changes: 18 additions & 0 deletions src/Uno.UI/FeatureConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,24 @@ public static class FrameworkElement
public static bool UseLegacyHitTest { get; set; } = false;
}

public static class FrameworkTemplate
{
/// <summary>
/// Determines if the pooling is enabled. If false, all requested instances are new.
/// </summary>
public static bool IsPoolingEnabled { get => FrameworkTemplatePool.IsPoolingEnabled; set => FrameworkTemplatePool.IsPoolingEnabled = value; }

/// <summary>
/// Determines the duration for which a pooled template stays alive
/// </summary>
public static TimeSpan TimeToLive { get => FrameworkTemplatePool.TimeToLive; set => FrameworkTemplatePool.TimeToLive = value; }

/// <summary>
/// Defines the ratio of memory usage at which the pools starts to stop pooling elligible views, between 0 and 1
/// </summary>
public static float HighMemoryThreshold { get => FrameworkTemplatePool.HighMemoryThreshold; set => FrameworkTemplatePool.HighMemoryThreshold = value; }
}

public static class Image
{
/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Uno.UI/Mock/ContentControl.net.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ partial void RegisterContentTemplateRoot()
{
AddChild((FrameworkElement)ContentTemplateRoot);
}

partial void UnregisterContentTemplateRoot()
{
RemoveChild((FrameworkElement)ContentTemplateRoot);
}
}
}
54 changes: 44 additions & 10 deletions src/Uno.UI/UI/Xaml/FrameworkTemplatePool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
using System.Threading.Tasks;
using System.Linq;
using Uno.Diagnostics.Eventing;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI;
using Windows.UI.Xaml.Controls;
using Uno.UI.Dispatching;
using Windows.Foundation.Metadata;
using Windows.System;

#if XAMARIN_ANDROID
using View = Android.Views.View;
Expand Down Expand Up @@ -85,8 +87,8 @@ public static class TraceProvider

private readonly static IEventProvider _trace = Tracing.Get(TraceProvider.Id);

private readonly Stopwatch _watch = new Stopwatch();
private readonly Dictionary<FrameworkTemplate, List<TemplateEntry>> _pooledInstances = new Dictionary<FrameworkTemplate, List<TemplateEntry>>(FrameworkTemplate.FrameworkTemplateEqualityComparer.Default);
private IFrameworkTemplatePoolPlatformProvider _platformProvider = new FrameworkTemplatePoolDefaultPlatformProvider();

#if USE_HARD_REFERENCES
/// <summary>
Expand All @@ -113,27 +115,37 @@ public static class TraceProvider
/// </summary>
public static bool IsPoolingEnabled { get; set; } = true;

/// <summary>
/// Defines the ratio of memory usage at which the pools starts to stop pooling elligible views.
/// </summary>
internal static float HighMemoryThreshold { get; set; } = .8f;

/// <summary>
/// Registers a custom <see cref="IFrameworkTemplatePoolPlatformProvider"/>
/// </summary>
/// <param name="provider"></param>
internal void SetPlatformProvider(IFrameworkTemplatePoolPlatformProvider provider)
=> _platformProvider = provider;

private FrameworkTemplatePool()
{
_watch.Start();

#if !NET461
CoreDispatcher.Main.RunIdleAsync(Scavenger);
_platformProvider.Schedule(Scavenger);
#endif
}

private async void Scavenger(IdleDispatchedHandlerArgs e)
{
Scavenge(false);

await Task.Delay(TimeSpan.FromSeconds(30));
await _platformProvider.Delay(TimeSpan.FromSeconds(30));

CoreDispatcher.Main.RunIdleAsync(Scavenger);
_platformProvider.Schedule(Scavenger);
}

private void Scavenge(bool isManual)
internal void Scavenge(bool isManual)
{
var now = _watch.Elapsed;
var now = _platformProvider.Now;
var removedInstancesCount = 0;

foreach (var list in _pooledInstances.Values)
Expand Down Expand Up @@ -240,6 +252,16 @@ private void OnParentChanged(object instance, object? key, DependencyObjectParen
return;
}

if (!CanUsePool())
{
if (this.Log().IsEnabled(Uno.Foundation.Logging.LogLevel.Debug))
{
(this).Log().Debug($"Not caching template, memory threshold is reached");
}

return;
}

var list = GetTemplatePool(key as FrameworkTemplate ?? throw new InvalidOperationException($"Received {key} but expecting {typeof(FrameworkElement)}"));

if (args?.NewParent == null)
Expand All @@ -261,7 +283,7 @@ private void OnParentChanged(object instance, object? key, DependencyObjectParen

if (item != null)
{
list.Add(new TemplateEntry(_watch.Elapsed, item));
list.Add(new TemplateEntry(_platformProvider.Now, item));
#if USE_HARD_REFERENCES
_activeInstances.Remove(item);
#endif
Expand All @@ -288,6 +310,18 @@ private void OnParentChanged(object instance, object? key, DependencyObjectParen
}
}

private bool CanUsePool()
{
if (_platformProvider.CanUseMemoryManager)
{
return ((float)_platformProvider.AppMemoryUsage / _platformProvider.AppMemoryUsageLimit) < HighMemoryThreshold;
}
else
{
return true;
}
}

internal static void PropagateOnTemplateReused(object instance)
{
// If DataContext is not null, it means it has been explicitly set (not inherited). Resetting the view could push an invalid value through 2-way binding in this case.
Expand Down
Loading

0 comments on commit 6c98490

Please sign in to comment.