dotnet / dotnet/android

[TrimmableTypeMap] Reduce trimmable typemap data initialization startup cost

Open
#11,501 0 comments 0 reactions 0 assignees View on GitHub
needs-triage
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 19h
Merged PRs (30d)
252

Description

## Summary

Trimmable typemap startup currently spends most of the managed `JNIEnvInit.Initialize` budget loading and initializing generated managed typemap data.

Using the OTEL instrumentation from draft PR #11502 and viewing the data through Aspire, a Release/CoreCLR/trimmable `HelloWorld` run shows:

- median `jnienv.initialize`: **99,591 us**
- median `typemap.data.initialize`: **62,103 us**
- `typemap.data.initialize` is roughly **2/3 of the observed `JNIEnvInit.Initialize` budget**

This should be tracked separately from the broader trimmable typemap runtime performance work because it points at the generated managed typemap initialization path itself, not just app activation or steady-state lookup cost.

Subtask of #10788.

## 10x trimmable data

From 10 Release/CoreCLR/trimmable launches of `samples/HelloWorld`:

| Metric | Count | Min | Median | Max | Mean |
| --- | ---: | ---: | ---: | ---: | ---: |
| `TotalTime` ms | 10 | 528.0 | 544.5 | 557.0 | 543.5 |
| `WaitTime` ms | 10 | 531.0 | 549.5 | 564.0 | 548.8 |
| Host elapsed us | 10 | 624504.0 | 661617.0 | 746665.0 | 667727.7 |
| `jnienv.initialize` us | 10 | 96694.2 | 99591.25 | 122651.1 | 101759.32 |
| `typemap.data.initialize` us | 10 | 60933.0 | 62103.2 | 82455.6 | 64033.28 |

## 10x LLVM-IR comparison data

After extending the same OTEL instrumentation to the existing LLVM-IR/CoreCLR typemap path, 10 Release/CoreCLR/LLVM-IR launches show:

| Metric | Count | Min | Median | Max | Mean |
| --- | ---: | ---: | ---: | ---: | ---: |
| `TotalTime` ms | 10 | 470.0 | 480.5 | 492.0 | 480.2 |
| `WaitTime` ms | 10 | 473.0 | 485.0 | 494.0 | 484.4 |
| Host elapsed us | 10 | 577463.0 | 605775.0 | 757226.0 | 635601.4 |
| LLVM span count | 10 | 36.0 | 103.5 | 141.0 | 96.0 |
| `jnienv.initialize` us | 10 | 37423.2 | 38970.45 | 40533.8 | 39009.79 |
| `typemap.llvm.activation` us | 10 | 900.6 | 986.5 | 1096.3 | 994.73 |
| `typemap.llvm.lookup_jni_name.uncached` us | 10 | 78.7 | 93.3 | 127.9 | 99.24 |

## Paired comparison

The directly paired startup boundary is `jnienv.initialize`. Trimmable is slower by about **60.6 ms** median, and `typemap.data.initialize` accounts for about **62.1 ms** median. That means the measured trimmable startup delta is almost entirely explained by generated typemap data initialization.

| Metric | Trimmable median | LLVM-IR median | Delta | Ratio |
| --- | ---: | ---: | ---: | ---: |
| `TotalTime` | 544.5 ms | 480.5 ms | +64.0 ms | 1.13x |
| `WaitTime` | 549.5 ms | 485.0 ms | +64.5 ms | 1.13x |
| Host elapsed | 661,617 us | 605,775 us | +55,842 us | 1.09x |
| `jnienv.initialize` | 99,591.25 us | 38,970.45 us | +60,620.8 us | 2.56x |

## Paired / analogous spans

| Concept | Trimmable span | Trimmable median | LLVM-IR span | LLVM-IR median | Notes |
| --- | --- | ---: | --- | ---: | --- |
| JNI runtime init boundary | `jnienv.initialize` | 99,591.25 us | `jnienv.initialize` | 38,970.45 us | Directly comparable boundary. |
| Typemap data setup | `typemap.data.initialize` | 62,103.2 us | none | - | LLVM-IR has no managed generated typemap assembly load equivalent. This is the main trimmable-only cost. |
| Runtime typemap object setup | `typemap.initialize` | 4,249.3 us | none | - | Trimmable wraps generated dictionaries in runtime lookup structures. |
| Native registration setup | `typemap.register_native_methods` | 169.9 us | built-in/native typemap path | - | Not currently separated in LLVM-IR; native path is implicit. |
| Java object -> managed peer creation | `typemap.peer.create` | 1,005.4 us | `typemap.llvm.activation` | 986.5 us | Similar order of magnitude for first activity-related activation. |
| Java object lookup | `typemap.lookup.java_object` | 583.95 us | `typemap.llvm.activation.peek_object` + activation subspans | 8.3 us for `peek_object`; 986.5 us full activation | Trimmable lookup includes more proxy resolution work; LLVM activation breakdown is more granular. |
| JNI-name lookup | `typemap.lookup.jni_name` | 74.25 us | `typemap.llvm.lookup.jni_name` | 201.1 us | Direct lookup concept, but implementation differs. LLVM-IR calls into native typemap metadata; trimmable uses managed dictionaries/proxy attributes. |
| Uncached JNI-name lookup | `typemap.lookup.jni_name.uncached` | 64.95 us | `typemap.llvm.lookup.jni_name.uncached` | 93.3 us | Directly comparable cache-miss path; both are sub-millisecond. |
| Managed-type lookup | `typemap.lookup.managed_type` | 63.2 us | no direct measured equivalent yet | - | LLVM-IR managed-to-Java lookup exists elsewhere (`clr_typemap_managed_to_java`) and should be instrumented separately if needed. |
| Uncached managed-type lookup | `typemap.lookup.managed_type.uncached` | 57.55 us | no direct measured equivalent yet | - | Same as above. |
| Register natives for generated proxy | `typemap.on_register_natives` | 844.55 us | no direct measured equivalent | - | Trimmable-specific native registration callback. |

## Interpretation

The lookup and activation paths are not where the big delta is:

- Trimmable uncached JNI-name lookup median: **64.95 us**
- LLVM-IR uncached JNI-name lookup median: **93.3 us**
- Trimmable peer creation median: **1,005.4 us**
- LLVM-IR activation median: **986.5 us**

The large difference is trimmable-only startup work:

- `typemap.data.initialize`: **62.1 ms median**
- `typemap.initialize`: **4.25 ms median**

Those two values line up with the observed `JNIEnvInit.Initialize` delta vs LLVM-IR.

## Raw data

The raw local capture is in `trimmable-typemap-otel-data.md` on draft PR #11502.

Local artifacts used while preparing the issue:

```text
artifacts/trimmable-typemap-otel-runs/20260526-115846
artifacts/llvm-typemap-otel-runs/20260526-130206
```

Each run directory contains `am-start.txt`, host timing, logcat, and Aspire trace/span JSON snapshots.

## Related work that may change the results

- #11473 packages ReadyToRun trimmable typemap assemblies. Once that lands, rerun the same measurements because missing R2R packaging may inflate typemap initialization cost.
- Framework ACW trimming and ProGuard generator improvements may reduce payload size and generated wrapper pressure, but this issue is specifically about the cost of `typemap.data.initialize` during `JNIEnvInit.Initialize`.

## Acceptance criteria

- Re-run startup measurements after #11473 lands.
- Identify the dominant cost inside `typemap.data.initialize`.
- Decide whether generated typemap data shape, loading strategy, R2R coverage, or lazy initialization should change.
- Reduce the share of `JNIEnvInit.Initialize` spent in `typemap.data.initialize`, or document why the current cost is expected.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.