From 286d6fe4fc35995055827425caf9452fbf4e9bc0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 16 Apr 2026 13:38:14 -0500 Subject: [PATCH] [tests] Add regression test for global ref leak in LayoutInflater.Inflate (#11112) Fixes: https://github.com/dotnet/android/issues/11101 Fixes: https://github.com/dotnet/android/issues/10989 The fix is in Java.Interop (ConstructPeer + Dispose for Invalid refs), included via the submodule bump. This adds an on-device regression test that inflates 100 custom views and asserts the JNI global reference count does not grow significantly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Android.Widget/CustomWidgetTests.cs | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index 66312f7a41a..33992194b93 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 66312f7a41a78c2e1fba1ea06d114674344c0906 +Subproject commit 33992194b9373e9322244612c94ed9941b9bc2fd diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs index 78d62576a0e..7b549f0061f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs @@ -1,4 +1,5 @@ -using Android.App; +using System; +using Android.App; using Android.Content; using Android.Util; using Android.Views; @@ -44,6 +45,47 @@ public void UpperAndLowerCaseCustomWidget_FromLibrary_ShouldNotThrowInflateExcep inflater.Inflate (Resource.Layout.upper_lower_custom, null); }, "Regression test for widgets with uppercase and lowercase namespace (bug #23880) failed."); } + + // https://github.com/dotnet/android/issues/11101 + [Test] + public void InflateCustomView_ShouldNotLeakGlobalRefs () + { + var inflater = (LayoutInflater) Application.Context.GetSystemService (Context.LayoutInflaterService); + Assert.IsNotNull (inflater); + + // Warm up: inflate once to populate caches and type mappings, + // and let any background thread activity from previous tests settle. + inflater.Inflate (Resource.Layout.lowercase_custom, null); + CollectGarbage (times: 3); + + int grefBefore = Java.Interop.Runtime.GlobalReferenceCount; + + // Use a large number of inflations so that a real leak (3+ global refs + // per inflate) produces a delta far above any background noise from + // Android system services, GC bridge processing, or finalizer threads. + const int inflateCount = 100; + for (int i = 0; i < inflateCount; i++) { + inflater.Inflate (Resource.Layout.lowercase_custom, null); + } + + CollectGarbage (times: 3); + + int grefAfter = Java.Interop.Runtime.GlobalReferenceCount; + int delta = grefAfter - grefBefore; + + // A real leak would produce delta >= 300 (3 leaked refs per inflate). + // Use a generous threshold to tolerate background noise on real devices. + Assert.IsTrue (delta <= 100, + $"Global reference leak detected: {delta} extra global refs after inflating/GC'ing {inflateCount} custom views. Before={grefBefore}, After={grefAfter}"); + + static void CollectGarbage (int times) + { + for (int i = 0; i < times; i++) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + } + } } public class CustomButton : Button