From 22cc39234b5e9c1b42c8c5d1abde67ab9b2f3342 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 11:41:58 +0200 Subject: [PATCH 1/3] [tests] Stabilize JavaSideActivation GREF check Warm and amplify the Java-side activation path so process-wide GREF noise does not cause exact-count flakes while per-call leaks remain strongly detectable.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../BindingTests.cs | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs index 33132c9479e..67d386eb780 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs @@ -97,23 +97,54 @@ public void Arrays () [Test] public void JavaSideActivation () { - using (var i = new ConstructorTest ()) { - // To ensure that CallMethodFromCtor.class_ref is initialized - } using (var c = Java.Lang.Class.FromType (typeof (ConstructorTest))) { + const int iterations = 100; + const int allowedGrefIncrease = 10; + + // GlobalReferenceCount is process-wide. Warm the exact activation path before + // measuring so one-time runtime caches are not mistaken for per-call leaks. + for (int i = 0; i < iterations; i++) { + AssertJavaSideActivation (c); + } + CollectGarbage (); + int initGref = Java.Interop.Runtime.GlobalReferenceCount; - using (var j = Com.Xamarin.Android.CallMethodFromCtor.NewInstance (c)) { - var instance = j.JavaCast(); - Assert.AreSame (j, instance); - Assert.IsTrue (instance.DefaultConstructorInvoked); - Assert.IsTrue (instance.ActivationConstructorInvoked); + for (int i = 0; i < iterations; i++) { + AssertJavaSideActivation (c); } + CollectGarbage (); int finiGref = Java.Interop.Runtime.GlobalReferenceCount; - Assert.AreEqual (initGref, finiGref, - string.Format ("Initial grefc={0}; final gref={1}; No GREFs should be lost!", initGref, finiGref)); + int delta = finiGref - initGref; + + // A known activation regression leaked three GREFs per call. Repeating the + // operation amplifies that to 300 while tolerating the observed ambient drift. + Assert.LessOrEqual (delta, allowedGrefIncrease, + string.Format ("GREF count increased by {0} after {1} Java-side activations. Initial grefc={2}; final grefc={3}.", + delta, iterations, initGref, finiGref)); } } + static void AssertJavaSideActivation (Java.Lang.Class c) + { + using (var j = Com.Xamarin.Android.CallMethodFromCtor.NewInstance (c)) { + var instance = j.JavaCast(); + Assert.AreSame (j, instance); + Assert.IsTrue (instance.DefaultConstructorInvoked); + Assert.IsTrue (instance.ActivationConstructorInvoked); + } + } + + static void CollectGarbage () + { + for (int i = 0; i < 3; i++) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + + Java.Interop.JniEnvironment.Runtime.ValueManager.CollectPeers (); + Java.Interop.JniEnvironment.Runtime.ValueManager.WaitForGCBridgeProcessing (); + } + // // https://bugzilla.xamarin.com/show_bug.cgi?id=17630 // https://bugzilla.xamarin.com/show_bug.cgi?id=17750#c6 @@ -391,4 +422,3 @@ public class B : Java.Lang.Object { } } } - From 350be958de710bf18852ba4e9190ef6f0bfdf2ac Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 21:51:10 +0200 Subject: [PATCH 2/3] [tests] Wait for peer collection finalizers Ensure the legacy Android value manager's CollectPeers GC completes finalization before sampling the process-wide GREF count.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs index 67d386eb780..aec677a5fab 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs @@ -142,6 +142,7 @@ static void CollectGarbage () } Java.Interop.JniEnvironment.Runtime.ValueManager.CollectPeers (); + GC.WaitForPendingFinalizers (); Java.Interop.JniEnvironment.Runtime.ValueManager.WaitForGCBridgeProcessing (); } From 8b5956ab8857d8e11de75f0508b0feea13c0bcf2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 10 Sep 2026 12:11:46 +0200 Subject: [PATCH 3/3] [tests] Isolate JavaSideActivation GREF check Run the process-wide GREF assertion in a dedicated category-filtered instrumentation process, matching the isolation strategy in #12709. Require no positive net growth after amplification and retain a control that proves the helper detects live global references. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../yaml-templates/stage-package-tests.yaml | 10 +++ .../BindingTests.cs | 76 +++++++++++++------ .../TestInstrumentation.cs | 12 ++- .../Xamarin.Android.JcwGen-Tests.csproj | 1 + 4 files changed, 73 insertions(+), 26 deletions(-) diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index caf98d3d1e4..2a31ca28cee 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -184,6 +184,16 @@ stages: artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab artifactFolder: $(DotNetTargetFramework)-NativeAOT + # Process-wide JNI reference counts need a fresh process without unrelated JcwGen tests. + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml + parameters: + configuration: $(XA.Build.Configuration) + testName: Xamarin.Android.JcwGen_Tests-JniReferenceLeaks + project: tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj + extraBuildArgs: -p:IncludeCategories=JniReferenceLeak + artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Xamarin.Android.JcwGen_Tests-Signed.apk + artifactFolder: $(DotNetTargetFramework)-JcwGen-JniReferenceLeaks + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml parameters: configuration: $(XA.Build.Configuration) diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs index aec677a5fab..fed5fe8c5d1 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs @@ -15,6 +15,9 @@ namespace Xamarin.Android.JcwGenTests { [TestFixture] public class BindingTests { + const string JniReferenceLeakCategory = "JniReferenceLeak"; + const int LeakCheckIterations = 100; + [Test] public void TestTimingCreateTimingIsCorrectType () { @@ -95,32 +98,32 @@ public void Arrays () } [Test] + [Explicit ("Run only in isolated JniReferenceLeak test runs.")] + [Category (JniReferenceLeakCategory)] public void JavaSideActivation () { using (var c = Java.Lang.Class.FromType (typeof (ConstructorTest))) { - const int iterations = 100; - const int allowedGrefIncrease = 10; - - // GlobalReferenceCount is process-wide. Warm the exact activation path before - // measuring so one-time runtime caches are not mistaken for per-call leaks. - for (int i = 0; i < iterations; i++) { - AssertJavaSideActivation (c); - } - CollectGarbage (); + AssertNoSustainedGlobalReferenceGrowth (() => AssertJavaSideActivation (c)); + } + } - int initGref = Java.Interop.Runtime.GlobalReferenceCount; - for (int i = 0; i < iterations; i++) { - AssertJavaSideActivation (c); + [Test] + [Explicit ("Run only in isolated JniReferenceLeak test runs.")] + [Category (JniReferenceLeakCategory)] + public void AssertNoSustainedGlobalReferenceGrowth_DetectsRetainedGlobalReference () + { + var objectClass = Java.Interop.JniEnvironment.Types.FindClass ("java/lang/Object"); + var retainedReferences = new List (); + try { + Assert.Throws (() => AssertNoSustainedGlobalReferenceGrowth (() => { + retainedReferences.Add (objectClass.NewGlobalRef ()); + })); + } finally { + foreach (var retainedReference in retainedReferences) { + var reference = retainedReference; + Java.Interop.JniObjectReference.Dispose (ref reference); } - CollectGarbage (); - int finiGref = Java.Interop.Runtime.GlobalReferenceCount; - int delta = finiGref - initGref; - - // A known activation regression leaked three GREFs per call. Repeating the - // operation amplifies that to 300 while tolerating the observed ambient drift. - Assert.LessOrEqual (delta, allowedGrefIncrease, - string.Format ("GREF count increased by {0} after {1} Java-side activations. Initial grefc={2}; final grefc={3}.", - delta, iterations, initGref, finiGref)); + Java.Interop.JniObjectReference.Dispose (ref objectClass); } } @@ -134,16 +137,39 @@ static void AssertJavaSideActivation (Java.Lang.Class c) } } + static void AssertNoSustainedGlobalReferenceGrowth (Action action) + { + for (int i = 0; i < LeakCheckIterations; i++) { + action (); + } + CollectPeers (); + + int grefsBefore = Java.Interop.Runtime.GlobalReferenceCount; + for (int i = 0; i < LeakCheckIterations; i++) { + action (); + } + CollectGarbage (); + int grefsAfter = Java.Interop.Runtime.GlobalReferenceCount; + + Assert.LessOrEqual (grefsAfter, grefsBefore, + $"Operation should not leak global references after {LeakCheckIterations} iterations. " + + $"Before={grefsBefore}, After={grefsAfter}, Delta={grefsAfter - grefsBefore}"); + } + + static void CollectPeers () + { + CollectGarbage (); + Java.Interop.JniEnvironment.Runtime.ValueManager.CollectPeers (); + Java.Interop.JniEnvironment.Runtime.ValueManager.WaitForGCBridgeProcessing (); + CollectGarbage (); + } + static void CollectGarbage () { for (int i = 0; i < 3; i++) { GC.Collect (); GC.WaitForPendingFinalizers (); } - - Java.Interop.JniEnvironment.Runtime.ValueManager.CollectPeers (); - GC.WaitForPendingFinalizers (); - Java.Interop.JniEnvironment.Runtime.ValueManager.WaitForGCBridgeProcessing (); } // diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/TestInstrumentation.cs b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/TestInstrumentation.cs index e08c9e053c4..7088fce9182 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/TestInstrumentation.cs +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/TestInstrumentation.cs @@ -13,10 +13,20 @@ public TestInstrumentation (IntPtr handle, JniHandleOwnership transfer) { } + protected override IEnumerable? IncludedCategories { + get { + var value = AppContext.GetData ("IncludeCategories") as string; + if (string.IsNullOrWhiteSpace (value)) + return null; + + var categories = value.Split (new [] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return categories.Length > 0 ? categories : null; + } + } + protected override IEnumerable GetTestAssemblies () { return [Assembly.GetExecutingAssembly ()]; } } } - diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj index 661356f3ab2..15d30a5f612 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj @@ -33,6 +33,7 @@ +