Repository navigation
[tests] Complete GC bridge accounting in JNI leak measurements - #13014
Conversation
Use a shared bounded completion protocol for both baseline and measured samples, preserve strict retained-reference controls, and cover asynchronous bridge phases and worker failures. Refs #13010 (finding 3). Co-authored-by: Copilot App <[email protected]>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Collection retries can reuse stale completion evidence, and timeout handling can mask completed worker faults.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
This test-only change addresses the GC-bridge measurement concern in #13010 by sharing collection and sampling logic across JNI leak checks.
Changes:
- Adds bounded collection sampling for Android and host JVM tests.
- Strengthens retained-reference controls and adds protocol regressions.
- Documents isolated test runs and timeout recovery.
| File | Description |
|---|---|
| tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj | Links the shared measurement helper. |
| tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs | Uses shared sampling and strengthens the leak control. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeUtf8Test.cs | Uses shared sampling and verifies expected growth. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniReferenceLeakMeasurementTests.cs | Adds completion, timeout, and worker-fault regressions. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniReferenceLeakMeasurement.cs | Implements bounded collection and reference sampling. |
| Documentation/building/unix/instructions.md | Documents isolated execution and sampling behavior. |
Wait for the previous bridge before retrying collection and require a new completion generation for the latest worker. Give retries their own unreachable peer cycle so a witness-finalization race cannot leave a request with no bridge work. Observe completed worker faults after the final poll while keeping timeout poisoning irreversible. Add deterministic stale-round and late-worker-fault regressions. Host 19 cases pass in five fresh processes; CoreCLR Debug and NativeAOT Release arm64 isolated suites pass five times each, with 16 protocol cases passing on each runtime. Refs #13010 (finding 3). Co-authored-by: Copilot App <[email protected]>
|
@dalexsoto review |
dalexsoto
left a comment
There was a problem hiding this comment.
The complete six-file protocol and runtime-integration review found no blocking issue. Baseline and final samples use the same bounded completion protocol; retries require the latest collection's worker and bridge completion, and late worker faults preserve their original exception while permanently poisoning reuse. The 100/100 iteration counts, strict non-growth threshold, actual Delta=100 retained controls, and test isolation remain intact. This approves the measurement fix, not a reproduction or root-cause claim for the historical intermittent leak.
Core JNI interop tests now run exclusively in the Android device suite. Keeping their sources in the former standalone test directory leaves misleading ownership, external source globs, and separate desktop/Android Java fixture selection. This Android-only relocation puts the sources beside their actual test project and simplifies fixture compilation without changing supported Android test behavior. Follow-up to the merged #13018. This PR now targets `main`; its diff contains only the Android test relocation and cleanup. It preserves the removal of the desktop harness, reflection-backed JNI managers, and obsolete `JavaVMFixture` scaffolding. - Move core C# sources and Java fixtures to `tests/Mono.Android-Tests/Java.Interop-Tests/`; use the project's default `Compile` glob and a local `TestJarEntry` glob. Preserve the existing exclusion for tests of the internal `JNIEnv` layout. - Remove `__ANDROID__` branches while retaining their Android assertions. Remove unsupported desktop-only custom-type-manager remapping tests and unused helper fixtures. Preserve `NO_MARSHAL_MEMBER_BUILDER_SUPPORT`, `NO_GC_BRIDGE_SUPPORT`, and applicable runtime/AOT exclusions. - Consolidate `GetThis.java` onto the Android-safe implementation for every typemap. Remove duplicate `java-trimmable` selection and unused desktop test targets, and prevent implicit `AndroidJavaSource` compilation from duplicating fixture classes in the JAR. - Update directly related documentation, instructions, and test catalog/skill guidance. Unrelated Java.Interop tooling suites are unchanged. **Migration guidance:** Core JNI test additions belong in the new Android directory, including C# fixtures such as `ThrowableInputCleanupTests.cs` and `JavaPeerControlBlockTests.cs`, and Java fixtures under `java/net/dot/jni/test/`. Preserve coverage introduced by #13014 and #13015 when moving or updating these files. Other branches touching the old directory, including #13016, should rebase into the new location. Do not recreate the removed standalone desktop test project. **Validation** The following results were recorded during implementation. The installed-SDK build passed, but repository-SDK/device coverage remains blocked; marking this PR ready for review does not imply that coverage passed. | Command / check | Outcome | |---|---| | `dotnet build tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj -c Release` | Passed using the installed Android SDK, including the local Java fixture JAR and AAR; 0 warnings, 0 errors. Repeated after rebasing onto the parent. This is compile coverage, not validation against a newly built repository SDK. | | `dotnet msbuild tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj -p:Configuration=Release -getItem:Compile,TestJarEntry,AndroidJavaSource` | Passed during implementation: verified 40 local Compile items, all 12 Java fixtures, and no duplicate AndroidJavaSource items. | | `javac --release 17 -d <session-artifacts>/java-fixture-classes tests/Mono.Android-Tests/Java.Interop-Tests/java/net/dot/jni/test/*.java external/Java.Interop/src/Java.Interop/java/net/dot/jni/GCUserPeerable.java external/Java.Interop/src/Java.Interop/java/net/dot/jni/ManagedPeer.java` | Passed for all 12 fixtures at the configured Java target version. | | Android-preprocessed source comparison against parent | Passed for 39 migrated C# files during implementation; the remaining file intentionally removes desktop-only remapping coverage and its unused helpers. Capability guards are unchanged. | | `jar tf tests/Mono.Android-Tests/Java.Interop-Tests/Jars/Mono.Android-Test-classes-trimmable.jar` and `javap -c -classpath tests/Mono.Android-Tests/Java.Interop-Tests/Jars/Mono.Android-Test-classes-trimmable.jar net.dot.jni.test.GetThis` | Passed: one GetThis class, implements GCUserPeerable, no desktop native registration/constructor calls. | | `git diff HEAD^ HEAD --check` | Passed during implementation. | | `make prepare CONFIGURATION=Release` | Blocked by NU1102: `Microsoft.NETCore.App.Ref` version `10.0.13` was unavailable in the configured feeds. The corresponding nuget.org package URL also returned HTTP 404. | | `make all CONFIGURATION=Release` | Blocked by MSB4062: missing `bin/BuildRelease/net10.0/xa-prep-tasks.dll` after preparation failed. | | `./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:AndroidTypeMapImplementation=llvm-ir` | Blocked by NETSDK1147: local SDK had no Android workload because repository SDK preparation/build could not complete. | | `./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:AndroidTypeMapImplementation=trimmable` | Blocked by the same NETSDK1147 prerequisite. | No on-device tests were run because the required local SDK/install builds failed, despite a connected arm64 emulator. Mono/CoreCLR/NativeAOT runtime behavior and both typemap device lanes remain unvalidated. No desktop JVM validation is required. ---- Pull Request [title](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md#commit-summary) and [description](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md#commit-body) should follow the [`commit-messages.md` workflow documentation](https://github.com/dotnet/android/blob/main/Documentation/workflow/commit-messages.md), and in particular should include: - [x] Useful description of *why the change is necessary*. - [ ] Links to issues fixed - [ ] Unit tests No issue is closed by this relocation. Existing Android tests compile; on-device test execution is blocked by the local SDK prerequisites described above.

Refs #13010, finding 3. This is an independent, non-stacked test-only change; it does not close the multi-finding tracker.
The existing JNI leak checks collect and drain peers before the baseline, but only collect managed garbage after the measured batch. That does not guarantee equivalent bridge phases: CoreCLR/NativeAOT dispatch bridge work asynchronously, the trimmable value manager's wait is intentionally a no-op, and globals temporarily become weak globals before surviving peers are promoted. Managed collection and peer draining alone are not a completion notification.
Use one shared bounded protocol for both samples. Android creates an unreachable peer-cycle witness and waits for its finalization, completion of the GC/finalizer worker, an advanced CoreCLR/NativeAOT bridge generation, and stable strong/weak counts with no transient weak globals. Drain peers after completion without starting another collection. Host JVM sampling does not require an impossible Android generation change. A ten-second timeout poisons the helper for that process: an uninterruptible worker cannot be followed by another action batch or GC worker.
Keep the 100-iteration warmup, 100 measured iterations, and zero-growth threshold. Both retained-GREF controls require the actual growth assertion and
Delta=100; a timeout cannot masquerade as successful leak detection. PreserveJavaSideActivation/Jcw control Explicit isolation and existing TryFindClass categories. No production runtime APIs or wait semantics change.Review fixes
Follow-up commit
2bed85fda5c7438a83d64ef85b22408e577ae065addresses both inline findings. Each collection request now records its own generation. A retry waits for the previous worker and bridge to complete, and acceptance requires a completion notification newer than the latest request, not the original round. A retry also creates an additional unreachable cycle so it still requests bridge work if the original witness finalizes just before its GC. Timeout handling irreversibly poisons the helper before observing a completed task, preserving late worker faults instead of reporting them as timeout assertions.Added deterministic regressions for a finalized original witness and completed retry worker while the retry bridge callback has not started, and for a worker fault becoming observable after the final polling observation. Existing phase, notification, empty-host, drain, pending-worker, timeout/reuse, and worker-fault coverage remains.
Validation
Final source after the review fixes was rebuilt and retested locally. All outputs and Android toolchain/AVD files were in this worktree. Device runs used dedicated API29
arm64-v8aemulatoremulator-5620with private adb server5051. SDK/runtime:12.0.100-alpha.1.26477.101/.NET 12.0.0-alpha.1.26477.101; Android assemblies targetnet11.0-android. Host assemblies targetnet10.0, roll forward to that runtime, and use Microsoft JDK25.Accepted samples had weak=0 and advancing bridge generations. Final CoreCLR samples typically advanced 2→4 for controls, 6→8 for activation, and 6→8/10→12 for lookups. NativeAOT controls advanced 2→4 and 4→6; activation 6→8; lookups 8→10 and 12→14. Both retained-GREF controls triggered the expected strict
Delta=100assertion on both Android runtimes, as did the host control.Exact commands/results, with
$PWDinitially the independent worktree root:Passed during initial SDK preparation. Bootstrap initially requested unavailable 10.0.13 reference packages; the environment override propagated 10.0.0 into child builds without changing product pins. The same environment with
make allinitially reached workload configuration but lacked API37.1/37.2 references. Built the existing API projects:Both passed. Final initial
make allusing the prepare environment passed, zero warnings/errors in the final summary. Production SDK code is unchanged by the review fix; the affected host/device test sources were rebuilt below. The bootstrap runtime override was not passed to Android application builds.Final host build and repeats:
Build passed with zero warnings/errors. All19 cases passed in each of five processes: 95/95.
For final Android builds/tests, exported this environment at the worktree root:
The following exact matrix cleans/restores/rebuilds before installation, then runs each real leak/control suite five times and the protocol fixture once per runtime:
All six clean/build/install combinations passed. CoreCLR Debug and NativeAOT Release each passed 2/2 Jcw and 3/3 Mono.Android cases in all five runs, plus exactly16/16 protocol cases. Builds reported existing binding/nullability and NUnit/runtime trim/AOT warnings. Final
git diff --checkpassed. Owned emulator/private adb server were stopped after validation.An initial review-fix candidate reset the generation without supplying a fresh retry cycle. Device validation correctly timed out when the original witness finalized immediately before a retry and no new bridge generation was produced. Adding the retry cycle addressed that legitimate no-peer condition without relaxing completion or count requirements; the final results above are from the corrected source. Separately,
-t:Rebuild;Installinitially deleted a referenced project's NuGet assets after restore. Running clean separately before build/restore fixed the build prerequisite. This is documented, along with explicit device selection and NativeAOT category-change test-count verification.Before review, initial validation also passed host17×5=85/85, CoreCLR25/25 +14/14 protocol, and NativeAOT25/25 +14/14 protocol. The final counts above include the two new deterministic regressions.
Limitations
Pinned CoreCLR/NativeAOT contract analysis and deterministic regressions demonstrate the missing completion guarantee, including misleading strong/weak-phase snapshots. The historical intermittent Android failure was not reproduced: this is not a causality claim and not evidence other GREF leaks are absent. Only arm64/API29 device coverage is included. No code dependency on the other independent tracker fixes was identified; the witness relies on normal
Dispose(false)finalization.Pull Request
title and
description
should follow the
commit-messages.mdworkflow documentation, and in particular should include: