dotnet / dotnet/android

Investigate Android interop overhead during .NET MAUI startup, especially PeekPeer

Open
#12,764 5 comments 0 reactions 0 assignees View on GitHub
Area: Performance
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 19h
Merged PRs (30d)
252

Description

### Android framework version

net11.0-android (Preview)

### Affected platform version

Current .NET for Android trimmable type-map path. The analyzed capture was an optimized, trimmable .NET MAUI startup build using a custom MIBC. Exact deployed .NET MAUI, AndroidX, Syncfusion, device, and Android versions were not recorded with the Speedscope file and should be captured in follow-up measurements.

### Description

A sampled startup profile of an optimized, trimmable .NET MAUI application suggests that Android interop contributes meaningfully to first-layout/startup overhead. The most interesting signal is `Microsoft.Android.Runtime.JavaMarshalRegisteredPeers.PeekPeer()`, which appears beneath Java-to-managed callbacks and property mapping while MAUI is creating and binding native views.

The profile's nested `OnMeasure` stacks are structurally legitimate: MAUI enters Java to measure a child, Java synchronously calls back into managed layout/adapter code, and RecyclerView may create and bind item views during measurement. Counting nested measurement frames only once gives approximately **396 ms** attributed beneath `OnMeasure`.

Within that interval:

| Stack attribution | Approximate time | Notes |
|---|---:|---|
| `TemplatedItemViewHolder.Bind` | 250 ms | About 63% of measurement-attributed time; includes template creation, binding, handler creation, and native realization |
| `Element.SetHandler` | 204 ms | Overlaps `Bind`; do not add these values |
| `JavaMarshalRegisteredPeers.PeekPeer` | 63 ms | Existing-peer lookup path |
| `TrimmableTypeMapValueManager.CreatePeer` | 1.6 ms | New peer resolution through the trimmable type map was small specifically within measurement |
| `Thread.PollGC` | 33 ms | Sample attribution associated with GC/safepoint activity, not necessarily a 33 ms pause |

The largest outer measurement region was approximately 102 ms. About 89 ms was beneath item binding, while approximately 48 ms included `PeekPeer`.

`PeekPeer()` currently performs several potentially relevant operations:

1. Calls Java `System.identityHashCode()` through JNI for every incoming reference.
2. Acquires the process-wide registered-peer lock.
3. Looks up the identity-hash bucket.
4. Reads weak-reference targets.
5. Uses JNI `IsSameObject()` to disambiguate hash collisions.

Source: https://github.com/dotnet/android/blob/26e3d80f396cda438e2458a45ca6b3a29c1a956b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs#L195-L218

One approximately 31 ms `PeekPeer` interval marked `UNMANAGED_CODE_TIME` overlaps GC-bridge processing on another thread. This makes GC interaction, suspension, or lock/coordination effects worth investigating. It does **not** prove that the dictionary lookup, identity hash, or JNI transition itself consumed 31 ms: the Speedscope output is reconstructed from samples, and `CPU_TIME`/`UNMANAGED_CODE_TIME` are not precise scheduler or method-duration measurements.

The goal of this issue is to quantify the interop contribution accurately and identify changes that reduce startup overhead without weakening Java peer identity or GC-bridge correctness.

Suggested investigation areas:

1. Add focused instrumentation or counters around `GetPeer()`/`PeekPeer()` to measure call count, hit/miss rate, identity-hash cost, lock wait/hold time, bucket sizes, weak-reference access, and `IsSameObject()` calls during startup.
2. Correlate peer lookup with CLR GC, Java GC, and GC-bridge phases using the original `.nettrace` plus Android scheduler/native tracing.
3. Determine whether repeated Java-to-managed callbacks or MAUI property mappings perform avoidable peer lookups for the same object during initial handler creation and layout.
4. Evaluate whether the peer registry can reduce or avoid repeated JNI `System.identityHashCode()` and `IsSameObject()` calls, while preserving identity across local/global references and hash collisions.
5. Evaluate contention-reduction approaches for the process-wide peer registry, such as partitioning or a read-optimized representation, while accounting for peer reconciliation and bridge collection.
6. Compare the trimmable type-map path with other type-map/runtime configurations using the same application and startup scenario.
7. Separate the cost of the JNI transition from native/ART work, GC waits, runtime suspension, and Speedscope sample-gap attribution before selecting an optimization.

Relevant MAUI behavior: `PlatformInterop.measureAndGetWidthAndHeight()` synchronously calls `view.measure()`, so its inclusive duration contains all descendant Java and managed callback work rather than just JNI overhead: https://github.com/dotnet/maui/blob/b96aa036b89fe41fe1ce6cae63a3f2d1550e5682/src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java#L437-L442

### Steps to Reproduce

1. Build an optimized, trimmable .NET MAUI Android application using the trimmable type-map path. The investigated build also used a custom MIBC.
2. Use a startup page containing nested MAUI layouts and a RecyclerView-backed items control with non-trivial item templates. The analyzed application also used Syncfusion controls.
3. Collect startup tracing with `dotnet-trace`/EventPipe and retain the original `.nettrace` in addition to exporting Speedscope JSON.
4. Inspect the first-layout call stacks beneath `ContentViewGroup.OnMeasure`, `LayoutViewGroup.OnMeasure`, `TemplatedItemViewHolder.Bind`, `Java.Lang.Object.GetObject`, `JniValueManager.GetPeer`, and `JavaMarshalRegisteredPeers.PeekPeer`.
5. Correlate those samples with GC and GC-bridge events and, ideally, Perfetto or simpleperf data.
6. Repeat with targeted instrumentation and alternate type-map/runtime configurations to quantify call frequency and actual operation latency.

### Did you find any workaround?

No runtime workaround has been established. Reducing initial item-template complexity or avoiding excess item realization may reduce the number of interop operations, but that does not address the underlying peer-lookup cost and has not been validated as a general workaround.

### Relevant log output

```shell
OnMeasure union (nested frames counted once): ~395.749 ms
TemplatedItemViewHolder.Bind under OnMeasure: ~249.674 ms
Element.SetHandler under OnMeasure: ~203.691 ms (overlaps Bind)
JavaMarshalRegisteredPeers.PeekPeer under measure: ~63.105 ms
TrimmableTypeMapValueManager.CreatePeer: ~1.639 ms

Largest outer OnMeasure region: ~101.656 ms
TemplatedItemViewHolder.Bind: ~89.284 ms
JavaMarshalRegisteredPeers.PeekPeer: ~47.865 ms

Suspicious overlap:
main thread PeekPeer -> UNMANAGED_CODE_TIME: 1162.593-1193.455 ms
GC bridge BridgeProcessingStarted: 1162.669-1191.913 ms
ProcessCollectedContexts begins: 1191.913 ms
```

Important profiling limitation: TraceEvent assigns elapsed time until the next relevant observation to the previous sampled stack. These values identify areas to investigate but should not be interpreted as instrumented method timings. The original `.nettrace` and correlated native/scheduler tracing are needed for causal attribution.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with JavaMarshalRegisteredPeers.cs lines 195-218 and the MAUI PlatformInterop.measureAndGetWidthAndHeight() entry point. Run the startup scenario while retaining the original .nettrace, then instrument GetPeer()/PeekPeer() and correlate results with GC-bridge and native tracing. Done means a measured attribution of lookup costs and an evidence-based direction that preserves peer identity and GC-bridge correctness.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, csharp, java
Domain
mobile-dev, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.