From 7b2f00da0979cd0e945bdf10f159514994ee5bdb Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 17:11:37 -0500 Subject: [PATCH] [native] Instrument GC bridge gref churn (dotnet/runtime#131370) Adds one unconditional per-round summary line to both GC bridge implementations to test whether CoreCLR issues far more JNI weak/strong global-reference transitions per bridge round than Mono: GCBRIDGE_CHURN runtime=<> sccs=<> peers=<> xrefs=<> jni_transitions=<> Both runtimes use the identical algorithm: flip every peer strong->weak before Runtime.gc() (NewWeakGlobalRef + DeleteGlobalRef) and weak->strong after (NewGlobalRef + DeleteWeakGlobalRef) = 4 JNI global-ref ops/peer. Measured on Pixel 5 / Android 14 / arm64, 60Hz pinned, two reversed rounds: Arm peers/rd transitions/rd ART GC avg ART GC max FPS CoreCLR ~442 ~1767 ~21.9 ms ~26 ms 59.77 Mono ~502 ~2011 ~10.6 ms ~11 ms 59.85 NEGATIVE RESULT: CoreCLR issues ~12% FEWER gref transitions per round than Mono, yet its ART concurrent-copying GC costs ~2.1x more. The relationship is inverted, so per-round native global-ref churn does NOT explain the ~13 ms concurrent-phase delta. Also: sccs==peers and xrefs==0 on both, so the SCC circular-ref and monodroidAddReference paths are never exercised by this benchmark. This hypothesis is dead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a9da32e8-e6a5-47bb-b145-3d683378e69b --- src/native/clr/host/bridge-processing.cc | 11 +++++++++++ src/native/mono/monodroid/osbridge.cc | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/src/native/clr/host/bridge-processing.cc b/src/native/clr/host/bridge-processing.cc index 2489346b15e..157407c822f 100644 --- a/src/native/clr/host/bridge-processing.cc +++ b/src/native/clr/host/bridge-processing.cc @@ -165,6 +165,17 @@ void BridgeProcessingShared::process () noexcept GCBridge::trigger_java_gc (env); cleanup_after_java_collection (); log_gc_summary (); + + // GC bridge gref churn instrumentation (dotnet/runtime#131370): one line per round. + // Every peer is flipped strong->weak in prepare (NewWeakGlobalRef + DeleteGlobalRef) and + // weak->strong in cleanup (NewGlobalRef + DeleteWeakGlobalRef): 4 JNI global-ref ops per peer. + size_t churn_peers = 0; + for (size_t i = 0; i < cross_refs->ComponentCount; i++) { + churn_peers = Helpers::add_with_overflow_check (churn_peers, cross_refs->Components [i].Count); + } + log_warnf (LOG_GC, "GCBRIDGE_CHURN runtime=coreclr sccs=%zu peers=%zu xrefs=%zu jni_transitions=%zu", + static_cast (cross_refs->ComponentCount), churn_peers, + static_cast (cross_refs->CrossReferenceCount), churn_peers * 4); } void BridgeProcessingShared::prepare_for_java_collection () noexcept diff --git a/src/native/mono/monodroid/osbridge.cc b/src/native/mono/monodroid/osbridge.cc index f6a111a6885..6fa7853b889 100644 --- a/src/native/mono/monodroid/osbridge.cc +++ b/src/native/mono/monodroid/osbridge.cc @@ -1035,6 +1035,15 @@ OSBridge::gc_cross_references (int num_sccs, MonoGCBridgeSCC **sccs, int num_xre gc_cleanup_after_java_collection (env, num_sccs, sccs); set_bridge_processing_field (domains_list, 0); + + // GC bridge gref churn instrumentation (dotnet/runtime#131370): one line per round. + // Every peer is flipped strong->weak in prepare (NewWeakGlobalRef + DeleteGlobalRef) and + // weak->strong in cleanup (NewGlobalRef + DeleteWeakGlobalRef): 4 JNI global-ref ops per peer. + int churn_peers = 0; + for (int i = 0; i < num_sccs; i++) + churn_peers += sccs [i]->num_objs; + log_warn (LOG_GC, "GCBRIDGE_CHURN runtime=mono sccs={} peers={} xrefs={} jni_transitions={}", + num_sccs, churn_peers, num_xrefs, churn_peers * 4); } void