From 395ae0b0a101f5f0f187939b6867bf95040034f6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 14:41:06 +0200 Subject: [PATCH 1/4] [tests] Disable flaky TryFindClass UTF-8 gref test Temporarily ignore TryFindClass_Utf8_DoesNotLeakGlobalRefs while the frequent CI failure is tracked in dotnet/android#12031. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs index c5e0fc0d6c0..feba304c0a5 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs @@ -91,6 +91,7 @@ public void TryFindClass_Utf8 () } [Test] + [Ignore ("Frequently failing: https://github.com/dotnet/android/issues/12031")] public void TryFindClass_Utf8_DoesNotLeakGlobalRefs () { int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; From 8ea7c8835a527804064ca072cd32d742bd163931 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 14:49:53 +0200 Subject: [PATCH 2/4] [tests] Avoid first-use gref noise in TryFindClass tests Warm up the failed TryFindClass fallback path before measuring global reference counts so the assertion does not include one-time runtime caches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs index feba304c0a5..b335c0fa7c9 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs @@ -91,9 +91,12 @@ public void TryFindClass_Utf8 () } [Test] - [Ignore ("Frequently failing: https://github.com/dotnet/android/issues/12031")] public void TryFindClass_Utf8_DoesNotLeakGlobalRefs () { + // Prime the fallback miss path so this assertion doesn't include one-time runtime caches. + Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist/Warmup"u8, out var warmup)); + Assert.IsFalse (warmup.IsValid); + int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out _); int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; @@ -104,6 +107,10 @@ public void TryFindClass_Utf8_DoesNotLeakGlobalRefs () [Test] public void TryFindClass_String_DoesNotLeakGlobalRefs () { + // Prime the fallback miss path so this assertion doesn't include one-time runtime caches. + Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist/Warmup", out var warmup)); + Assert.IsFalse (warmup.IsValid); + int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; JniEnvironment.Types.TryFindClass ("does/not/Exist", out _); int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; From 9e8950e0de92e9ae98575f6e4410b9a7c19e2628 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 15:03:23 +0200 Subject: [PATCH 3/4] [tests] Tolerate Android gref count noise Android's global reference count is process-wide, so exact equality can fail when unrelated runtime or test infrastructure work allocates persistent refs during the assertion. Repeat failed TryFindClass calls and allow bounded Android noise so per-call leaks are still amplified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniTypeUtf8Test.cs | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs index b335c0fa7c9..8f89474def4 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs @@ -93,29 +93,44 @@ public void TryFindClass_Utf8 () [Test] public void TryFindClass_Utf8_DoesNotLeakGlobalRefs () { - // Prime the fallback miss path so this assertion doesn't include one-time runtime caches. - Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist/Warmup"u8, out var warmup)); - Assert.IsFalse (warmup.IsValid); - - int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; - JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out _); - int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; - Assert.AreEqual (grefsBefore, grefsAfter, + AssertDoesNotLeakGlobalRefs (() => { + Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out var notFound)); + Assert.IsFalse (notFound.IsValid); + }, "TryFindClass for non-existent classes should not leak global references"); } [Test] public void TryFindClass_String_DoesNotLeakGlobalRefs () { - // Prime the fallback miss path so this assertion doesn't include one-time runtime caches. - Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist/Warmup", out var warmup)); - Assert.IsFalse (warmup.IsValid); + AssertDoesNotLeakGlobalRefs (() => { + Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist", out var notFound)); + Assert.IsFalse (notFound.IsValid); + }, + "TryFindClass for non-existent classes should not leak global references"); + } + + static void AssertDoesNotLeakGlobalRefs (Action action, string message) + { +#if __ANDROID__ + // Android's gref count is process-wide, and the on-device runner/runtime can + // create persistent grefs on background threads while a test is executing. + // Repeat the operation to amplify a per-call leak above ambient noise. + const int iterations = 200; + const int tolerance = 100; +#else + const int iterations = 1; + const int tolerance = 0; +#endif + action (); int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; - JniEnvironment.Types.TryFindClass ("does/not/Exist", out _); + for (int i = 0; i < iterations; ++i) + action (); int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; - Assert.AreEqual (grefsBefore, grefsAfter, - "TryFindClass for non-existent classes should not leak global references"); + int delta = grefsAfter - grefsBefore; + Assert.LessOrEqual (delta, tolerance, + $"{message}. Before={grefsBefore}, After={grefsAfter}, Delta={delta}, Iterations={iterations}, Tolerance={tolerance}"); } [Test] From e8f137ba757e14c270c13c8da9f7f44efafe11d3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Jul 2026 15:06:05 +0200 Subject: [PATCH 4/4] [tests] Return TryFindClass gref test to ignore Disable the flaky UTF-8 TryFindClass gref assertion again while the tracking issue investigates a more reliable way to test JNI reference leaks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniTypeUtf8Test.cs | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs index 8f89474def4..feba304c0a5 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs @@ -91,46 +91,24 @@ public void TryFindClass_Utf8 () } [Test] + [Ignore ("Frequently failing: https://github.com/dotnet/android/issues/12031")] public void TryFindClass_Utf8_DoesNotLeakGlobalRefs () { - AssertDoesNotLeakGlobalRefs (() => { - Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out var notFound)); - Assert.IsFalse (notFound.IsValid); - }, + int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; + JniEnvironment.Types.TryFindClass ("does/not/Exist"u8, out _); + int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; + Assert.AreEqual (grefsBefore, grefsAfter, "TryFindClass for non-existent classes should not leak global references"); } [Test] public void TryFindClass_String_DoesNotLeakGlobalRefs () { - AssertDoesNotLeakGlobalRefs (() => { - Assert.IsFalse (JniEnvironment.Types.TryFindClass ("does/not/Exist", out var notFound)); - Assert.IsFalse (notFound.IsValid); - }, - "TryFindClass for non-existent classes should not leak global references"); - } - - static void AssertDoesNotLeakGlobalRefs (Action action, string message) - { -#if __ANDROID__ - // Android's gref count is process-wide, and the on-device runner/runtime can - // create persistent grefs on background threads while a test is executing. - // Repeat the operation to amplify a per-call leak above ambient noise. - const int iterations = 200; - const int tolerance = 100; -#else - const int iterations = 1; - const int tolerance = 0; -#endif - action (); - int grefsBefore = JniEnvironment.Runtime.GlobalReferenceCount; - for (int i = 0; i < iterations; ++i) - action (); + JniEnvironment.Types.TryFindClass ("does/not/Exist", out _); int grefsAfter = JniEnvironment.Runtime.GlobalReferenceCount; - int delta = grefsAfter - grefsBefore; - Assert.LessOrEqual (delta, tolerance, - $"{message}. Before={grefsBefore}, After={grefsAfter}, Delta={delta}, Iterations={iterations}, Tolerance={tolerance}"); + Assert.AreEqual (grefsBefore, grefsAfter, + "TryFindClass for non-existent classes should not leak global references"); } [Test]