Skip to content

[Mono.Android] Reduce JNI overhead in the CoreCLR GC bridge#12040

Open
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/coreclr-gc-bridge-jni-cleanup
Open

[Mono.Android] Reduce JNI overhead in the CoreCLR GC bridge#12040
simonrozsival wants to merge 1 commit into
mainfrom
dev/simonrozsival/coreclr-gc-bridge-jni-cleanup

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Note

This pull request was created with the assistance of GitHub Copilot. The code and description were AI-generated and reviewed by the author.

Summary

Reduces JNI overhead and fixes local-reference hygiene in the CoreCLR GC
bridge host (src/native/clr/host, shared with the NativeAOT host). No
behavioral change to which objects survive a collection — these are
efficiency/robustness improvements to how bridge processing talks to JNI.

Changes

  • Cache mono.android.IGCUserPeer method IDs. add_reference /
    clear_references previously did a GetObjectClass + GetMethodID
    lookup (plus a transient local reference) for every reference edge
    and every cleared node, on every bridge cycle. The interface method IDs
    (monodroidAddReference / monodroidClearReferences) are now resolved
    once at runtime init and dispatched via an IsInstanceOf guard. This
    mirrors how the NativeAOT host already caches the
    net.dot.jni.GCUserPeerable method IDs, roughly halves the JNI calls
    per edge, and removes a per-edge local reference. The IsInstanceOf
    guard preserves the previous "missing method" fallback and the
    add_circular_references abort-on-failure semantics.

  • Reserve JNI local capacity for empty-SCC temporary peers. Each
    strongly connected component with no IGCUserPeers is represented by a
    temporary peer held as a JNI local reference until all cross references
    are added. prepare_for_java_collection now reserves local reference
    capacity up front so a large number of such SCCs cannot overflow the
    JNI local reference table (which only guarantees 16 slots by default).

  • Fix a local-reference leak in GCBridge::log_handle_context. The
    jclass from GetObjectClass was never deleted, leaking one local per
    logged handle on the long-lived, attached bridge thread whenever GC
    spew is enabled.

  • Fix the inverted GC-spew guard in log_weak_ref_collected. The
    "was collected by a Java GC" message was emitted when GC spew was
    disabled and suppressed when it was enabled — the opposite of every
    sibling logging helper (e.g. log_gc_summary).

Testing

Built the CoreCLR android-arm64 runtime with these changes and ran the
full Mono.Android.NET-Tests suite on an API 36 arm64-v8a emulator with
-p:UseMonoRuntime=false (CoreCLR):

  • 915 passed, 0 failed, 55 skipped.
  • With debug.mono.log=gc enabled, the GC bridge ran 12 times during the
    suite (graphs up to ~389 objects, tens of objects resurrected per cycle),
    exercising the cached-method-ID add_reference/clear_references paths
    and the temporary-peer capacity path, with no CheckJNI / JNI errors /
    local-reference-table overflows
    (CheckJNI is active for the debuggable
    test app).
  • One unrelated timing-sensitive test (JavaObjectTest.UnregisterFromRuntime,
    a registered-peer count assertion) flaked once under heavy GC-spew logging
    and passed on re-run; it is not affected by these changes.

Make JNI usage during CoreCLR GC bridge processing cheaper and more
robust. All changes are in the CoreCLR/NativeAOT shared native host
(`src/native/clr/host`).

* Cache the `mono.android.IGCUserPeer` interface method IDs
  (`monodroidAddReference` / `monodroidClearReferences`) once at runtime
  init instead of doing a `GetObjectClass` + `GetMethodID` lookup for
  every reference edge and every cleared node during bridge processing.
  This mirrors how the NativeAOT host already caches the
  `net.dot.jni.GCUserPeerable` method IDs, roughly halves the JNI calls
  per edge, and removes a transient local reference per edge. The
  `IsInstanceOf` guard preserves the previous "missing method" fallback
  and the existing abort-on-failure behaviour.

* Reserve JNI local reference capacity up front for the temporary peers
  created for empty strongly connected components, so that a large number
  of such SCCs cannot overflow the JNI local reference table (which only
  guarantees 16 slots by default).

* Delete the `jclass` local reference obtained in
  `GCBridge::log_handle_context`. It was leaked on the long-lived,
  attached bridge thread on every logged handle when GC spew is enabled.

* Fix the inverted GC-spew guard in `log_weak_ref_collected` so the
  "was collected by a Java GC" message is emitted when GC spew is enabled
  (matching every sibling logging helper) rather than when it is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e3188a8-82e3-4a21-bf4c-257b7ad9752c
Copilot AI review requested due to automatic review settings July 11, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces JNI overhead and improves local-reference hygiene in the CoreCLR GC bridge host (shared with the NativeAOT host), focusing on making bridge processing more efficient and robust without changing GC liveness semantics.

Changes:

  • Cache mono.android.IGCUserPeer interface method IDs once at runtime init, avoiding per-edge GetObjectClass/GetMethodID and associated local refs during bridge cycles.
  • Proactively call EnsureLocalCapacity() to accommodate temporary peers created for SCCs with Count == 0, mitigating JNI local-ref-table overflow risk.
  • Fix local-ref management and GC-spew logging guards (DeleteLocalRef() for GetObjectClass() in log_handle_context, and correct the gc_spew_enabled() early-return logic).
Show a summary per file
File Description
src/native/clr/include/host/bridge-processing-shared.hh Adds cached IGCUserPeer JNI handles (class + method IDs) to support per-edge JNI call reductions.
src/native/clr/host/gc-bridge.cc Deletes the jclass local ref created by GetObjectClass() in log_handle_context, preventing a local-ref leak on the bridge thread when logging.
src/native/clr/host/bridge-processing.cc Initializes cached IGCUserPeer method IDs, reserves local-ref capacity for temporary peers, and switches reference add/clear to cached interface dispatch with an IsInstanceOf guard; fixes GC-spew guard inversion in log_weak_ref_collected.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants