dotnet / dotnet/android

Investigation: defer ART collection in the CoreCLR GC bridge

Open
#12,436 1 comment 0 reactions 0 assignees View on GitHub
Area: CoreCLR Area: Performance needs-triage
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 19h
Merged PRs (30d)
252

Description

## Summary

This issue records an experiment evaluating whether the CoreCLR Android GC bridge can avoid synchronously invoking `java.lang.Runtime.gc()` and instead defer Java peer reclamation until ART runs collection naturally.

The result is promising but not yet production-ready:

- Deferred collection worked for an 80-minute stress run containing approximately 349,000 Android Callable Wrappers (ACWs).
- ART naturally reclaimed JNI weak global references (WREFs), allowing the corresponding managed handles to be retired.
- The WREF table remained below ART's 51,200-entry limit.
- Avoiding synchronous `Runtime.gc()` reduced typical bridge time and improved frame pacing in a separate rendering benchmark.
- The trade-offs were higher memory use, larger retained WREF populations, and worse bridge-time outliers.

This is an investigation report, not yet a proposal to change the product implementation.

Related context:

- https://github.com/dotnet/runtime/issues/131370
- https://github.com/dotnet/android/pull/12263

## Prototype design

The prototype changes the bridge lifecycle as follows:

1. Unreachable Java peers are converted from JNI strong global references (GREFs) to WREFs.
2. The bridge completes without calling `Runtime.gc()`.
3. WREFs remain registered across subsequent bridge rounds.
4. Later rounds probe each WREF using `NewLocalRef()`.
5. If ART cleared the WREF, the WREF is deleted and its managed handle is released.
6. Live WREFs are temporarily strengthened only when needed to rebuild the current bridge graph.

A process-wide sweep handles WREFs that are no longer part of the current CoreCLR cross-reference graph.

For comparison, an adb property can enable the old behavior before each sweep:

```text
debug.net.gcbridge.explicit_gc=0 # natural ART collection
debug.net.gcbridge.explicit_gc=1 # invoke Runtime.gc()
```

The property is read during every bridge round, so both modes use the same APK and runtime implementation.

## Stress workload

The test application repeatedly:

- creates a random component containing 1–1,000 custom ACWs;
- connects the component into a strongly connected ring with additional random edges;
- waits 10 ms between individual ACW allocations;
- drops the only root after completing the component;
- records managed GC counts, managed heap data, Java heap data, JNI GREF count, RSS, bridge population, reclamation, and phase timings.

Both configurations ran for 10, 20, 40, and finally 80 minutes on the same host-accelerated arm64 emulator using its default ART heap policy. All runs completed without a WREF overflow or fatal runtime error.

## 80-minute A/B result

| Metric | Natural ART collection | Explicit `Runtime.gc()` |
|---|---:|---:|
| ACWs allocated | 348,659 | 347,124 |
| Bridge rounds | 134 | 147 |
| Maximum WREFs | 35,510 | 7,616 |
| Final WREFs | 27,052 | 2,437 |
| Handles reclaimed | 321,609 | 343,266 |
| Median bridge algorithm time | **9.9 ms** | 17.3 ms |
| 95th-percentile bridge algorithm time | 45.8 ms | **21.3 ms** |
| Maximum bridge algorithm time | 77.4 ms | **22.6 ms** |
| Median `Runtime.gc()` time | — | 12.8 ms |
| Final managed heap size | 73.5 MiB | **8.8 MiB** |
| Final RSS | 255.0 MiB | **182.1 MiB** |

Throughput was intentionally paced by the test and was effectively identical.

Natural collection produced a repeating sawtooth pattern: WREFs accumulated and then fell when ART independently collected their Java referents. The bridge observed 21 reclamation cycles during the natural 80-minute run.

![80-minute natural ART timeline](https://github.com/user-attachments/assets/010e0c2f-0c52-4928-8821-c342fac2f60b)

![80-minute natural versus explicit ART collection](https://github.com/user-attachments/assets/640ea5f7-1962-4f47-9a3a-32fe54fc36c3)

Natural ART collection is marked at the bridge round that first observed and reclaimed cleared WREFs. Explicit collection timestamps come directly from ART's `Explicit concurrent mark compact GC` log entries.

## Rendering benchmark result

A separate five-minute host-GPU emulator comparison used the application from runtime#131370:

| Metric | Natural ART collection | Explicit `Runtime.gc()` |
|---|---:|---:|
| Average bridge time | **3.1 ms** | 11.3 ms |
| Maximum bridge time | **18 ms** | 37 ms |
| Frames below 55 FPS | **2** | 7 |
| Maximum WREFs | 2,465 | **641** |

This indicates that removing synchronous ART collection can provide a meaningful typical-latency and frame-pacing improvement. A physical Samsung S23 test also confirmed that ART can naturally clear deferred bridge WREFs under a real 60 FPS workload, although collection was infrequent when Java heap pressure was low.

## What we learned

### The lifecycle is technically feasible

Keeping peers as WREFs across bridge rounds works. ART can clear them naturally, and the bridge can later detect that state and safely retire the associated managed handles.

### It removes a significant synchronous cost

`Runtime.gc()` directly adds ART collection time to the bridge operation. Avoiding it improved typical bridge duration in both the stress workload and the rendering benchmark.

### It exchanges pause time for memory and tail latency

Without explicit collection, more WREFs and managed contexts remain registered. Process-wide probing becomes more expensive as that population grows. The natural run therefore had a better median but substantially worse outliers and higher RSS.

The current diagnostics deliberately scan every registered reference and emit detailed logcat records. They contribute to measured latency, so production measurements without this instrumentation should be better. The underlying sweep is still proportional to the retained WREF population, however.

### Natural ART scheduling is not a safety guarantee

ART collected often enough in this stress workload, but applications with little Java allocation pressure can retain WREFs for much longer. The prototype cannot guarantee that ART will collect before its fixed 51,200-entry WREF table fills.

## Requirements before considering a product change

- Provide a hard overflow-prevention strategy instead of relying exclusively on ART scheduling.
- Make WREF cleanup incremental or otherwise bound the work performed during one bridge round.
- Validate resurrection and peer-reconnection behavior.
- Repeat realistic application and physical-device measurements without detailed diagnostic scanning/logging.
- Evaluate memory and frame-time percentiles across a wider range of workloads and ART implementations.

## Current conclusion

Deferred ART collection is worth pursuing. It successfully moves expensive Java collection out of the normal GC bridge path and improves typical bridge latency and frame pacing.

It is not yet safe to ship as-is because reclamation timing is controlled by ART, memory retention is higher, and the current design has no guaranteed protection against WREF-table exhaustion.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the bridge-round lifecycle described in the prototype and compare natural versus explicit collection using debug.net.gcbridge.explicit_gc=0/1. Reproduce the stress and rendering measurements, then evaluate overflow prevention, incremental cleanup, resurrection and peer reconnection, and realistic device behavior. Done requires evidence that these risks are addressed before a product change is proposed.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, csharp, java
Domain
mobile-dev, performance
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.