public async Task When_Add_Remove(object controlTypeRaw, int count) { #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; var maxActiveControls = 0; var rootContainer = new ContentControl(); TestServices.WindowHelper.WindowContent = rootContainer; await TestServices.WindowHelper.WaitForIdle(); for (int i = 0; i < count; i++) { await MaterializeControl(controlType, _holders, maxCounter, rootContainer); } TestServices.WindowHelper.WindowContent = null; void HolderUpdate(int value) { _ = rootContainer.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => { maxCounter = Math.Max(value, maxCounter); activeControls = value; maxActiveControls = maxCounter; } ); } var sw = Stopwatch.StartNew(); var endTime = TimeSpan.FromSeconds(30); var maxTime = TimeSpan.FromMinutes(1); var lastActiveControls = activeControls; while (sw.Elapsed < endTime && sw.Elapsed < maxTime && activeControls != 0) { GC.Collect(); GC.WaitForPendingFinalizers(); // Waiting for idle is required for collection of // DispatcherConditionalDisposable to be executed await TestServices.WindowHelper.WaitForIdle(); if (lastActiveControls != activeControls) { // Expand the timeout if the count has changed, as the // GC may still be processing levels of the hierarcy on iOS endTime += TimeSpan.FromMilliseconds(500); } lastActiveControls = activeControls; } #if TRACK_REFS Uno.UI.DataBinding.BinderReferenceHolder.LogInactiveViewReferencesStatsDiff(initialInactiveStats); Uno.UI.DataBinding.BinderReferenceHolder.LogActiveViewReferencesStatsDiff(initialActiveStats); #endif var retainedMessage = ""; #if NET5_0 || __IOS__ || __ANDROID__ if (activeControls != 0) { var retainedTypes = _holders.AsEnumerable().Select(ExtractTargetName).JoinBy(";"); Console.WriteLine($"Retained types: {retainedTypes}"); retainedMessage = $"Retained types: {retainedTypes}"; } #endif #if __IOS__ // On iOS, the collection of objects does not seem to be reliable enough // to always go to zero during runtime tests. If the count of active objects // is arbitrarily below the half of the number of top-level objects. // created, we can assume that enough objects were collected entirely. Assert.IsTrue(activeControls < count, retainedMessage); #else Assert.AreEqual(0, activeControls, retainedMessage);