Exact process-wide JNI reference and peer-count assertions in the shared on-device test process are failing intermittently.
Although NUnit test execution is serialized (NumberOfTestWorkers=0), runtime and test-infrastructure work can still run on background threads and create persistent references or peers between the before/after snapshots. A changed process-wide count therefore does not necessarily identify a leak in the operation under test.
Affected tests
TryFindClass_Utf8_DoesNotLeakGlobalRefs
Example build:
https://dev.azure.com/dnceng-public/public/_build/results?buildId=1498907&view=results
failed TryFindClass_Utf8_DoesNotLeakGlobalRefs
TryFindClass for non-existent classes should not leak global references
Expected: 492
But was: 546
Source:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs
Temporarily disabled by #12032.
UnregisterFromRuntime
Example build:
https://dev.azure.com/dnceng-public/public/_build/results?buildId=1504514&view=results
failed UnregisterFromRuntime
registeredCount+1 should match!
Expected: 68
But was: 71
Source:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs
This test counts surfaced peers rather than GREFs directly, but has the same isolation problem: it assumes no unrelated process activity changes a shared runtime-wide count while the assertion runs.
Codebase audit candidates
Enabled exact process-wide assertions — high risk
Runtime-global surfaced-peer assertions — medium risk
The Android value-manager contract installs a fresh value manager, which reduces stale state, but installs it globally. Background runtime activity can therefore still perturb exact peer totals.
Source:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs
These tests should prefer assertions about the specific peer under test (PeekPeer, identity, removal) rather than the total registry size.
Already quarantined or noise-tolerant
Lower-risk thread-local LREF assertions
The exact LREF checks in JniEnvironmentTests, TestTypeTests, and the JavaArrayContract fixture family use JniEnvironment.LocalReferenceCount, which is thread-local. The Android runner executes NUnit tests synchronously on one instrumentation thread, so background threads cannot change these counts.
They do not share the GREF failure mechanism, but the fixture-wide checks can still include test-runner activity on the instrumentation thread and should eventually move into the isolated harness with the other leak checks.
Proposed direction
Reference and peer leak checks should run in a dedicated device test app/process rather than in the same test project as unrelated runtime tests. The dedicated harness should:
- start from a fresh process for each leak scenario or tightly controlled group;
- avoid unrelated tests and background test-runner activity;
- control ordering and concurrency explicitly;
- amplify the operation under test where useful, so a per-call leak is distinguishable from setup noise;
- perform any required GC/finalizer synchronization before checking cleanup.
This harness does not need to use NUnit if a simpler dedicated executable provides better lifecycle and concurrency control.
Exact process-wide JNI reference and peer-count assertions in the shared on-device test process are failing intermittently.
Although NUnit test execution is serialized (
NumberOfTestWorkers=0), runtime and test-infrastructure work can still run on background threads and create persistent references or peers between the before/after snapshots. A changed process-wide count therefore does not necessarily identify a leak in the operation under test.Affected tests
TryFindClass_Utf8_DoesNotLeakGlobalRefsExample build:
https://dev.azure.com/dnceng-public/public/_build/results?buildId=1498907&view=results
Source:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.csTemporarily disabled by #12032.
UnregisterFromRuntimeExample build:
https://dev.azure.com/dnceng-public/public/_build/results?buildId=1504514&view=results
Source:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.csThis test counts surfaced peers rather than GREFs directly, but has the same isolation problem: it assumes no unrelated process activity changes a shared runtime-wide count while the assertion runs.
Codebase audit candidates
Enabled exact process-wide assertions — high risk
JniTypeUtf8Test.TryFindClass_String_DoesNotLeakGlobalRefsGlobalReferenceCountequality.external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.csJniEnvironmentTests.References_CreatedReference_GlobalRefGlobalReferenceCountbaseline around direct JNI global-reference calls.external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniEnvironmentTests.csJavaObjectArray_object_ContractTest.EndCheckGlobalRefCountexternal/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.csJniRuntimeJniValueManagerContract.ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefsConstructPeercalls.external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.csBindingTests.JavaSideActivationtests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.csRuntime-global surfaced-peer assertions — medium risk
The Android value-manager contract installs a fresh value manager, which reduces stale state, but installs it globally. Background runtime activity can therefore still perturb exact peer totals.
JniRuntimeJniValueManagerContract.AddPeer_NoDuplicatesJniRuntimeJniValueManagerContract.ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeersJniRuntimeJniValueManagerContract.ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeersSource:
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.csThese tests should prefer assertions about the specific peer under test (
PeekPeer, identity, removal) rather than the total registry size.Already quarantined or noise-tolerant
TryFindClass_Utf8_DoesNotLeakGlobalRefs— disabled by [tests] Disable flaky TryFindClass UTF-8 gref test #12032.UnregisterFromRuntime— disabled by [tests] Disable flaky UnregisterFromRuntime peer-count test #12037.InflateCustomView_ShouldNotLeakGlobalRefs— disabled under CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs test failing in CI #11201; it already amplifies the operation and allows ambient GREF noise.DoNotLeakWeakReferences— disabled under [NativeAOT] Investigate failing DoNotLeakWeakReferences #11690; it already documents process-global peer-count drift and uses a tolerance.Lower-risk thread-local LREF assertions
The exact LREF checks in
JniEnvironmentTests,TestTypeTests, and theJavaArrayContractfixture family useJniEnvironment.LocalReferenceCount, which is thread-local. The Android runner executes NUnit tests synchronously on one instrumentation thread, so background threads cannot change these counts.They do not share the GREF failure mechanism, but the fixture-wide checks can still include test-runner activity on the instrumentation thread and should eventually move into the isolated harness with the other leak checks.
Proposed direction
Reference and peer leak checks should run in a dedicated device test app/process rather than in the same test project as unrelated runtime tests. The dedicated harness should:
This harness does not need to use NUnit if a simpler dedicated executable provides better lifecycle and concurrency control.