Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Avoid redundant Java GC bridge graph computation by steveisok · Pull Request #131764 · dotnet/runtime · GitHub
Skip to content

Avoid redundant Java GC bridge graph computation - #131764

Merged
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops
Aug 11, 2026
Merged

Avoid redundant Java GC bridge graph computation#131764
BrzVlad merged 2 commits into
mainfrom
steveisok-android-coreclr-frame-drops

Conversation

@steveisok

@steveisoksteveisok commented Aug 3, 2026

Copy link
Copy Markdown
Member

On Android the GC bridge hands a set of cross references to the client on every collection that finds at least one dead Java peer. The client answers each of those with an explicit ART collection, and while that runs the UI thread blocks creating JNI global references, which shows up as dropped frames.

This skips the SCC computation when it is known to be discarded. The VM already frees the MarkCrossReferencesArgs without forwarding them when the client is still processing a previous set, so the Tarjan pass and the allocations feeding it were pure waste. The GC can now ask the client first via the new IGCToCLR::IsClientBridgeProcessingActive, and skips the work instead. This is behavior neutral: every registered bridge object is promoted whether or not the cross references were computed, so skipping only delays reporting a dead peer, it never collects one early.

Adding a method to IGCToCLR is a breaking interface change, so EE_INTERFACE_MAJOR_VERSION goes from 4 to 5.

Part of the investigation in #131370.

Scoped down from the original version of this PR at @BrzVlad's request — the timing heuristic that was bundled here has been removed and is not part of this change.

Validation

Built clr clean (0 errors, 0 warnings) for:

  • android-arm64 Release — the configuration where FEATURE_JAVAMARSHAL is on by default
  • osx-arm64 Checked — non-Android, FEATURE_JAVAMARSHAL also on, covers the interface plumbing outside Android

gcbridge.cpp was confirmed compiled into both the CoreCLR VM and the NativeAOT workstation/server GC objects. The standalone GC sample under src/coreclr/gc/sample is not part of either build and was not compiled.

Note

Portions of this pull request description were generated with GitHub Copilot.

On Android the GC bridge hands a set of cross references to the client on
every collection that finds at least one dead Java peer. The client answers
each of those with an explicit ART collection, and while that runs the UI
thread blocks creating JNI global references, which shows up as dropped
frames.
Two changes:
Skip the SCC computation when it is known to be discarded. The VM already
frees the arguments without forwarding them when the client is still
processing a previous set, so the Tarjan pass and the allocations feeding it
were pure waste. The GC can now ask the client via the new
IGCToCLR::IsClientBridgeProcessingActive, so it skips the work instead. This
is behavior neutral.
Throttle how often a fresh set of cross references is produced, controlled by
the new GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob, defaulting
to 50 ms. Only gen0 collections are throttled: bridge objects are promoted
whether or not they were handed to the client, so a deferred object cannot be
registered again by a later gen0 collection, and leaving gen1 and gen2
unthrottled bounds how long a dead peer can go unreported to the next gen1
collection. Deferring in this way is the same shape as what already happens
when a collection lands while the client is busy.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@jkotas@janvorli@vitek-karas I pushed this as a draft to get your reactions. It seems reasonable but you may want something else.

CopilotAI 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 updates the CoreCLR GC↔EE bridge plumbing (Java marshal bridge) to (1) avoid computing bridge cross-reference work that would be discarded anyway, and (2) introduce a gen0-only throttle for how frequently bridge processing requests are produced.

Changes:

  • Adds IGCToCLR::IsClientBridgeProcessingActive() (EE interface major version bump 4→5) so the GC can skip building cross references when the client is already busy.
  • Introduces GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs and uses it to throttle gen0 bridge request production.
  • Wires the new method across CoreCLR VM, NativeAOT, standalone GC forwarding, and the GC sample stub.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
src/coreclr/vm/gcenv.ee.cppImplements GCToEEInterface::IsClientBridgeProcessingActive() for CoreCLR VM builds.
src/coreclr/nativeaot/Runtime/interoplibinterface.hExtends NativeAOT Java marshal interface with IsGCBridgeActive().
src/coreclr/nativeaot/Runtime/interoplibinterface_java.cppImplements JavaMarshalNative::IsGCBridgeActive() by reading g_GCBridgeActive.
src/coreclr/nativeaot/Runtime/gcenv.ee.cppImplements NativeAOT GCToEEInterface::IsClientBridgeProcessingActive().
src/coreclr/gc/sample/gcenv.ee.cppAdds a stub IsClientBridgeProcessingActive() implementation for the GC sample.
src/coreclr/gc/objecthandle.cppGates ProcessBridgeObjects() behind ShouldProcessBridgeObjects(condemned).
src/coreclr/gc/gcinterface.hBumps EE_INTERFACE_MAJOR_VERSION to 5.
src/coreclr/gc/gcinterface.ee.hAdds new IGCToCLR pure virtual IsClientBridgeProcessingActive().
src/coreclr/gc/gcenv.ee.standalone.inlVersion-guards and forwards IsClientBridgeProcessingActive() for standalone GC.
src/coreclr/gc/gcconfig.hAdds the new GCBridgeMinIntervalMs configuration knob (with public name).
src/coreclr/gc/gcbridge.hDeclares ShouldProcessBridgeObjects(uint32_t condemned).
src/coreclr/gc/gcbridge.cppImplements throttling + “client busy” skip logic and timestamps last request time.
src/coreclr/gc/env/gctoeeinterface.standalone.inlImplements the new IGCToCLR method in the standalone shim class.
src/coreclr/gc/env/gcenv.ee.hDeclares GCToEEInterface::IsClientBridgeProcessingActive().

Comment threadsrc/coreclr/gc/gcconfig.h Outdated
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @anicka-net, @dotnet/gc
See info in area-owners.md if you want to be subscribed.

@jkotas

Copy link
Copy Markdown
Member

answers with an explicit ART Runtime.gc(). That request is unthrottled, so a gen0 collection is enough to trigger a full ART collection.

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

@steveisok

Copy link
Copy Markdown
MemberAuthor

Are we going to do something about this part?

With this PR, we can be still triggering full ART collection every 50ms. Full GC every 50ms still sounds like a problem.

We definitely should do something and that would need to happen in dotnet/android. It's very possible we could do enough there and not need any runtime changes.

@agocke

agocke commented Aug 3, 2026

Copy link
Copy Markdown
Member

What if we just don't build SCCs or invoke ART on gen0 collections? Gen0 collections tend to be small anyway -- maybe it's fine to float? We could conservatively mark the closure and promote to gen1 even after it's CLR-unreachable, so we would implicitly put more pressure on gen1 and that would basically put generational-style pressure into the system.

@vitek-karas

Copy link
Copy Markdown
Member

@BrzVlad please take a look as well

@vitek-karas

Copy link
Copy Markdown
Member

Seemingly mono has this problem a lot less - do we understand why? How is mono behaving differently from CoreCLR in this case?

@BrzVlad

Copy link
Copy Markdown
Member

Was this tested on the actual repro provided by the user ? Locally I'm seeing a collection every 6 seconds or so. So this change would have no effect.

@BrzVlad

Copy link
Copy Markdown
Member

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

@steveisok

Copy link
Copy Markdown
MemberAuthor

@steveisok Could you reduce this PR so it only addresses the redundant computation of bridge graph. It is independent of the timing heuristic and we could have it merged right away.

Sure, I was waiting to see where our collective analysis lands but happy to reduce it now.

Reduce this change to only the redundant bridge graph computation fix.
Removes the GCBridgeMinIntervalMs / System.GC.BridgeMinIntervalMs knob and
the elapsed-time check in ShouldProcessBridgeObjects, leaving only the
IsClientBridgeProcessingActive() query that skips the Tarjan pass when the
client would discard its result anyway. ShouldProcessBridgeObjects no longer
needs the condemned generation, so the parameter is gone.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@steveisoksteveisok changed the title Avoid redundant and over-frequent Java GC bridge requestsAvoid redundant Java GC bridge graph computationAug 10, 2026
CopilotAI review requested due to automatic review settings August 10, 2026 15:43
@steveisok
steveisok marked this pull request as ready for review August 10, 2026 15:44
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI 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.

Review details

  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BrzVlad
BrzVlad merged commit 522b95f into mainAug 11, 2026
102 of 104 checks passed
@BrzVlad
BrzVlad deleted the steveisok-android-coreclr-frame-drops branch August 11, 2026 05:13
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
On Android the GC bridge hands a set of cross references to the client
on every collection that finds at least one dead Java peer. The client
answers each of those with an explicit ART collection, and while that
runs the UI thread blocks creating JNI global references, which shows up
as dropped frames.
This skips the SCC computation when it is known to be discarded. The VM
already frees the `MarkCrossReferencesArgs` without forwarding them when
the client is still processing a previous set, so the Tarjan pass and
the allocations feeding it were pure waste. The GC can now ask the
client first via the new `IGCToCLR::IsClientBridgeProcessingActive`, and
skips the work instead. This is behavior neutral: every registered
bridge object is promoted whether or not the cross references were
computed, so skipping only delays reporting a dead peer, it never
collects one early.
Adding a method to `IGCToCLR` is a breaking interface change, so
`EE_INTERFACE_MAJOR_VERSION` goes from 4 to 5.
Part of the investigation in dotnet#131370.
Scoped down from the original version of this PR at @BrzVlad's request —
the timing heuristic that was bundled here has been removed and is not
part of this change.
### Validation
Built `clr` clean (0 errors, 0 warnings) for:
- `android-arm64` Release — the configuration where
`FEATURE_JAVAMARSHAL` is on by default
- `osx-arm64` Checked — non-Android, `FEATURE_JAVAMARSHAL` also on,
covers the interface plumbing outside Android
`gcbridge.cpp` was confirmed compiled into both the CoreCLR VM and the
NativeAOT workstation/server GC objects. The standalone GC sample under
`src/coreclr/gc/sample` is not part of either build and was not
compiled.
> [!NOTE]
> Portions of this pull request description were generated with GitHub
Copilot.
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91dd8b98-73db-41b0-8d8c-9e1c21b53f5f
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 12, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@steveisok@jkotas@agocke@vitek-karas@BrzVlad