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 33132c9479e..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,22 +98,77 @@ public void Arrays () } [Test] + [Explicit ("Run only in isolated JniReferenceLeak test runs.")] + [Category (JniReferenceLeakCategory)] 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))) { - 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); + AssertNoSustainedGlobalReferenceGrowth (() => 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); } - 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)); + Java.Interop.JniObjectReference.Dispose (ref objectClass); + } + } + + 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 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 (); } } @@ -391,4 +449,3 @@ public class B : Java.Lang.Object { } } } - 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 @@ +