Skip to content

Commit

Permalink
feat: Improve reference checking memory leaks runtime tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromelaban committed Dec 21, 2020
1 parent 2058063 commit 56245e6
Showing 1 changed file with 127 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
#if !WINDOWS_UWP
// Uncomment to get additional reference tracking
// #define TRACK_REFS
#nullable enable

#if !WINDOWS_UWP
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Private.Infrastructure;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml.Controls;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;

#if __MACOS__
using AppKit;
#elif __IOS__
using UIKit;
#endif

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml
{
[TestClass]
Expand Down Expand Up @@ -44,9 +57,28 @@ public class Given_FrameworkElement_And_Leak
[DataRow(typeof(Border), 15)]
[DataRow(typeof(ContentControl), 15)]
[DataRow(typeof(ContentDialog), 15)]
public async Task When_Add_Remove(Type controlType, int count)
[DataRow("SamplesApp.Windows_UI_Xaml.Clipping.XamlButtonWithClipping_Scrollable", 15)]
[DataRow("Uno.UI.Samples.Content.UITests.ButtonTestsControl.AppBar_KeyBoard", 15)]
[DataRow("Uno.UI.Samples.Content.UITests.ButtonTestsControl.Buttons", 15)]
public async Task When_Add_Remove(object controlTypeRaw, int count)
{
var _holders = new ConditionalWeakTable<FrameworkElement, Holder>();
#if TRACK_REFS
var initialInactiveStats = Uno.UI.DataBinding.BinderReferenceHolder.GetInactiveViewReferencesStats();
var initialActiveStats = Uno.UI.DataBinding.BinderReferenceHolder.GetReferenceStats();
#endif

Type GetType(string s)
=> AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType(s)).Where(t => t != null).First()!;

var controlType = controlTypeRaw switch
{
Type ct => ct,
string s => GetType(s),
_ => throw new InvalidOperationException()
};

var _holders = new ConditionalWeakTable<DependencyObject, Holder>();
void TrackDependencyObject(DependencyObject target) => _holders.Add(target, new Holder(HolderUpdate));

var maxCounter = 0;
var activeControls = 0;
Expand All @@ -60,19 +92,11 @@ public async Task When_Add_Remove(Type controlType, int count)

for (int i = 0; i < count; i++)
{
var item = Activator.CreateInstance(controlType) as FrameworkElement;
_holders.Add(item, new Holder(HolderUpdate));
rootContainer.Content = item;
await TestServices.WindowHelper.WaitForIdle();
rootContainer.Content = null;
GC.Collect();
GC.WaitForPendingFinalizers();

// Waiting for idle is required for collection of
// DispatcherConditionalDisposable to be executed
await TestServices.WindowHelper.WaitForIdle();
await MaterializeControl(controlType, _holders, maxCounter, rootContainer);
}

TestServices.WindowHelper.WindowContent = null;

void HolderUpdate(int value)
{
_ = rootContainer.Dispatcher.RunAsync(CoreDispatcherPriority.High,
Expand All @@ -97,7 +121,96 @@ void HolderUpdate(int value)
await TestServices.WindowHelper.WaitForIdle();
}

#if TRACK_REFS
Uno.UI.DataBinding.BinderReferenceHolder.LogInactiveViewReferencesStatsDiff(initialInactiveStats);
Uno.UI.DataBinding.BinderReferenceHolder.LogActiveViewReferencesStatsDiff(initialActiveStats);
#endif

#if NET5_0
if (activeControls != 0)
{
var retainedTypes = _holders.AsEnumerable().Select(ExtractTargetName).JoinBy(";");
Console.WriteLine($"Retained types: {retainedTypes}");
}
#endif

Assert.AreEqual(0, activeControls);

static string? ExtractTargetName(KeyValuePair<DependencyObject, Holder> p)
{
if(p.Key is FrameworkElement fe)
{
return $"{fe}/{fe.Name}";
}
else
{
return p.Key?.ToString();
}
}

async Task MaterializeControl(Type controlType, ConditionalWeakTable<DependencyObject, Holder> _holders, int maxCounter, ContentControl rootContainer)
{
var item = (FrameworkElement)Activator.CreateInstance(controlType);
TrackDependencyObject(item);
rootContainer.Content = item;
await TestServices.WindowHelper.WaitForIdle();

// Add all children to the tracking
foreach (var child in item.EnumerateAllChildren(maxDepth: 200).OfType<UIElement>())
{
TrackDependencyObject(child);

if (child is FrameworkElement fe)
{
// Don't use VisualStateManager.GetVisualStateManager to avoid creating an instance
if (child.GetValue(VisualStateManager.VisualStateManagerProperty) is VisualStateManager vsm)
{
TrackDependencyObject(vsm);

if (VisualStateManager.GetVisualStateGroups(fe) is { } groups)
{
foreach (var group in groups)
{
TrackDependencyObject(group);

foreach (var transition in group.Transitions)
{
TrackDependencyObject(transition);

foreach (var timeline in transition.Storyboard.Children)
{
TrackDependencyObject(timeline);
}
}

foreach (var state in group.States)
{
TrackDependencyObject(state);

foreach (var setter in state.Setters)
{
TrackDependencyObject(setter);
}

foreach (var trigger in state.StateTriggers)
{
TrackDependencyObject(trigger);
}
}
}
}
}
}
}

rootContainer.Content = null;
GC.Collect();
GC.WaitForPendingFinalizers();

// Waiting for idle is required for collection of
// DispatcherConditionalDisposable to be executed
await TestServices.WindowHelper.WaitForIdle();
}
}

[TestMethod]
Expand Down

0 comments on commit 56245e6

Please sign in to comment.