[TrimmableTypeMap] Pre-generate Mono.Android/Java.Interop typemaps at SDK build time - #12127
[TrimmableTypeMap] Pre-generate Mono.Android/Java.Interop typemaps at SDK build time#12127simonrozsival wants to merge 23 commits into
Conversation
…root Toward pre-generating Mono.Android's typemap at SDK build time (issue #10792): add a generateRootAssembly flag to TrimmableTypeMapGenerator.Execute (and a GenerateRootAssembly property on the GenerateTrimmableTypeMap task, default true). When false, the per-assembly typemap DLLs (e.g. _Mono.Android.TypeMap) and JCW Java sources are emitted, but the root _Microsoft.Android.TypeMaps assembly (TypeMapAssemblyTarget<T> attributes + TypeMapLoader.Initialize) is not: that root is emitted by the app build, which will reference the pre-generated per-assembly typemap alongside the app's own. Adds a unit test asserting the per-assembly typemap is produced and the root is omitted when generateRootAssembly is false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…nder Java.Lang.Object Special-case pre-generated framework typemaps (e.g. _Mono.Android.TypeMap, built at SDK build time per issue #10792) in the root typemap assembly. Such typemaps always use Java.Lang.Object as their universe anchor, so a single artifact serves both app-build universe modes: - RootTypeMapAssemblyGenerator.Generate gains sharedFrameworkTypeMapNames. They are emitted as [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("name")] regardless of useSharedTypemapUniverse. * Merged (Release): they join the single Java.Lang.Object universe, so GetOrCreateExternalTypeMapping<Java.Lang.Object>() merges them with the app's entries automatically — no Initialize change. * Aggregate (Debug): TypeMapLoader.Initialize adds the Java.Lang.Object universe as element [0] alongside the app's per-assembly __TypeMapAnchor universes. - Threaded through TrimmableTypeMapGenerator.Execute/GenerateTypeMapAssemblies. - Aggregate + array maps (maxArrayRank > 0) with a shared framework universe throws NotSupportedException for now (tracked follow-up). Adds unit tests asserting the framework typemap is referenced under Java.Lang.Object (scope Mono.Android) in both modes, the app typemap keeps its own anchor in aggregate mode, and both produce valid PE. 619 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
Milestone: generate _Mono.Android.TypeMap.dll (issue #10792) once during the SDK/pack build instead of rescanning Mono.Android (~8000 types) on every app build. - GenerateTrimmableTypeMap task: add ForceSharedTypemapUniverse. When true the typemap always uses the Java.Lang.Object universe anchor regardless of Debug, so the single pre-generated artifact is consumable by both app-build universe modes. - build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets: new _GeneratePreBuiltMonoAndroidTypeMap target scans the built Mono.Android.dll (framework assemblies supplied for reference only) with ForceSharedTypemapUniverse=true and GenerateRootAssembly=false, emitting _Mono.Android.TypeMap.dll + JCW Java sources; and _AddPreBuiltMonoAndroidTypeMapToPackage ships the DLL in the SDK pack under data/prebuilt-typemap. Imported by Microsoft.Android.Sdk.proj. Verified locally: the target produces _Mono.Android.TypeMap.dll (5.4 MB) with no __TypeMapAnchor (i.e. anchored on Java.Lang.Object), 320 JCW .java files, and no root _Microsoft.Android.TypeMaps assembly. JCW jar compilation + app-build consumption follow in later milestones. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…ack layout ConfigureLocalWorkload (used by 'make all' and -t:InstallMaui) does not run the NuGet GetFilesToPackage flow, so the FilesToPackage hook alone did not place the pre-generated typemap into the local pack. Write _Mono.Android.TypeMap.dll directly into the pack layout at $(MicrosoftAndroidSdkPackDir)data\prebuilt-typemap\ and run the generation BeforeTargets=ConfigureLocalWorkload as well as _GenerateXASdkContent, so the artifact is present for both local workload testing and the NuGet pack. Verified: after ConfigureLocalWorkload, _Mono.Android.TypeMap.dll (5.4 MB) is present under bin/.../packs/Microsoft.Android.Sdk.Darwin/<ver>/data/prebuilt-typemap/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…pemaps.jar Compile the pre-generated Java Callable Wrappers (issue #10792) into a jar shipped in the SDK pack under data/prebuilt-typemap, so the app build can dex them directly instead of re-generating and re-compiling the ~320 Mono.Android JCWs. - _CompilePreBuiltMonoAndroidTypeMapJcwJar javac-compiles the generated JCW .java into mono.android-typemaps.jar. Classpath mirrors GenerateJavaCallableWrappers: android.jar + mono.android.jar + java_runtime.jar. - Ship the jar alongside _Mono.Android.TypeMap.dll (FilesToPackage + local pack layout). Verified: mono.android-typemaps.jar (403 entries) is produced under bin/.../packs/Microsoft.Android.Sdk.Darwin/<ver>/data/prebuilt-typemap/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…map refs in the task Adds the generator/scanner/task plumbing the app build needs to consume a pre-generated Mono.Android typemap (issue #10792): - AssemblyInput gains ScanForPeers. When false the assembly is indexed for base-type resolution but not scanned for peer emission; JavaPeerScanner skips ScanAssembly for it. - GenerateTrimmableTypeMap task gains PreGeneratedTypeMapAssemblies: those assemblies are marked ScanForPeers=false, and their _<Name>.TypeMap names are passed to the generator as sharedFrameworkTypeMapNames so the generated root references them under Java.Lang.Object. Adds a unit test asserting a reference-only assembly emits no peers/typemap. 620 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
Wire the app build to use the SDK-shipped Mono.Android typemap (issue #10792) instead of rescanning Mono.Android's ~8000 types on every build: - Microsoft.Android.Sdk.TypeMap.Trimmable.targets: when the pre-generated typemap exists in the pack (data/prebuilt-typemap, gated by $(_AndroidUsePreGeneratedMonoAndroidTypeMap)), pass Mono.Android to GenerateTrimmableTypeMap as a PreGeneratedTypeMapAssembly (indexed for resolution, not rescanned); add the pre-built _Mono.Android.TypeMap.dll to the generated typemap set so it is linked/published and referenced by the root under Java.Lang.Object; and add mono.android-typemaps.jar to @(AndroidJavaLibrary) for dexing. - create-packs targets: emit generated JCW .java / .class to obj intermediate so only the _Mono.Android.TypeMap.dll and jar are packaged. Verified (Release NativeAOT, _AndroidTypeMapImplementation=trimmable): the app build no longer generates _Mono.Android.TypeMap (only _Java.Interop/_Microsoft.Android.Runtime.NativeAOT/_NativeAOT + root), and the root references the pre-built _Mono.Android.TypeMap. KNOWN GAP (next milestone): R8 shrinks the pre-built Mono.Android JCWs from classes.dex because their ACW entries are no longer in the app acw-map (Mono.Android excluded from scan), so no keep rules are generated. The pre-generated typemap's ACW keep rules must be shipped and fed to R8 before on-device runs will work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…r R8 keep rules With Mono.Android excluded from the app scan, its ACW entries were absent from the app acw-map, so R8 generated no keep rules and shrank the pre-built Mono.Android JCWs out of classes.dex. Ship the pre-generated typemap's ACW map and merge it back in: - create-packs: GenerateTrimmableTypeMap now also writes mono.android-acw-map.txt, shipped in the pack under data/prebuilt-typemap. - App build: when the pre-generated typemap is used, append mono.android-acw-map.txt to the app's acw-map.txt after generation, so the proguard/R8 keep-rule generators (CoreCLR and NativeAOT) emit -keep rules for the pre-built Mono.Android JCWs. Verified (Release NativeAOT, trimmable): classes.dex returns to ~255 KB (was ~13 KB when the JCWs were shrunk), i.e. the pre-built Mono.Android JCWs are retained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
… fix array rank
On-device (Release NativeAOT, trimmable) the app crashed at startup with BadImageFormatException
in TypeMapLazyDictionary.CreateExternalTypeMap. Two causes, both fixed:
1. Duplicate java/lang/Object in the merged Java.Lang.Object universe: the pre-built Mono.Android
typemap mapped it to Java.Lang.Object while the app-scanned _Java.Interop.TypeMap mapped it to
Java.Interop.JavaObject. MergeCrossAssemblyAliases coordinates these only within a single scan.
Fix: pre-generate Mono.Android AND Java.Interop together (aliases coordinated) and exclude both
from the app scan; ship/reference all pre-built *.TypeMap.dll.
2. Array-rank mismatch: the pre-built typemap used MaxArrayRank=0 while the app root/runtime used
the AOT default (3), so array entries landed in the main universe. Fix: generate the pre-built
typemap with MaxArrayRank=3, and have the root reference the framework typemaps' __ArrayMapRank{N}
universes too.
Verified on a physical arm64 device (API 36): the Release/single-universe trimmable NativeAOT app
launches to MainActivity with no BadImageFormatException / crash. 620 unit tests still pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
…egate + array maps) The aggregate-universe + array-maps (maxArrayRank > 0) combination — i.e. Debug NativeAOT — is not yet wired for pre-generated framework typemaps and previously threw NotSupportedException, which would break those builds. Gracefully disable the pre-generation for that combination so the build falls back to scanning Mono.Android normally (correct, just without the speedup) instead of failing. Verified: Debug NativeAOT trimmable now builds (Mono.Android scanned in-app), while Release NativeAOT (single universe) and CoreCLR Debug (aggregate universe) continue to use the pre-generated typemap and are device-verified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
simonrozsival
left a comment
There was a problem hiding this comment.
⚠️ Needs Changes (self-review via android-reviewer)
Note: posted as a COMMENT review since GitHub disallows REQUEST_CHANGES on one's own PR.
Solid, well-staged implementation of #10792; the design (pre-generate Mono.Android + Java.Interop together under the Java.Lang.Object universe so one artifact serves both universe modes) is sound and device-verified in both modes (Release NativeAOT single-universe; CoreCLR Debug aggregate). A few correctness/robustness items before merge, plus the documented Debug-NativeAOT limitation.
Counts: ❌ 0 ·
Positives:
- Clear milestone-by-milestone commits with informative messages.
- Good unit coverage for the new generator paths (reference-only assemblies,
generateRootAssembly, framework-universe target attributes). - Graceful fallback for the unsupported Debug-NativeAOT combo avoids a hard build regression.
Before merge:
⚠️ Fix incrementalOutputsfor the pre-generation target (now produces two typemaps).⚠️ Document/justify the hardcodedMaxArrayRank="3"coupling.- 💡 File a tracking issue for the arrays-aggregate
NotSupportedExceptionpath. - 💡 Reconsider mutating the app
acw-map.txtin place.
CI hasn't run on this branch yet — not mergeable until the dotnet-android pipeline is green.
- Track both pre-built typemaps in _GeneratePreBuiltMonoAndroidTypeMap Outputs (_Mono.Android.TypeMap.dll + _Java.Interop.TypeMap.dll) so the incremental state is not broken if the Java.Interop typemap is deleted. - Replace the hardcoded MaxArrayRank="3" with a documented _PreBuiltTypeMapMaxArrayRank property explaining it must match the app build's effective rank (BadImageFormatException otherwise). - Reference the tracking issue (#12128) from the arrays-aggregate NotSupportedException and the Debug-NativeAOT fallback comment. - Stop mutating the app's acw-map.txt in place. Instead build a separate deterministic acw-map.prebuilt-merged.txt (app map + pre-built framework map, Overwrite=true) and feed that to the NativeAOT R8 keep-rule generator, so the app's acw-map.txt stays byte-stable and its own incremental checks keep working (no double-append). Verified: 620 TrimmableTypeMap unit tests pass; Release NativeAOT (trimmable, R8) sample keeps the pre-built JCWs (classes.dex ~255KB, 310 mono.android keep rules) and launches on device without crashing, while acw-map.txt is no longer mutated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 867395ec-6705-4ed6-8a68-e25b58d431fc
Resolve trimmable typemap conflicts after array proxy map removal and preserve pre-generated framework typemap support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the obsolete array-rank input, package framework typemap DLLs as SDK runtime data without NU5100, and prevent post-trim CoreCLR generation from rescanning framework assemblies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR moves generation of the large framework trimmable typemaps (Mono.Android + Java.Interop) from app build-time to SDK/pack build-time, then updates the app build to consume the prebuilt artifacts to reduce incremental build cost and startup initialization.
Changes:
- Add SDK-pack build targets to pre-generate and package
_Mono.Android.TypeMap.dll,_Java.Interop.TypeMap.dll,mono.android-typemaps.jar, andmono.android-acw-map.txt. - Update trimmable typemap generation to support (1) skipping peer scanning for “reference-only” assemblies and (2) emitting a root assembly that can also reference prebuilt framework typemaps under
Java.Lang.Object. - Update app build targets/tests to avoid regenerating framework typemaps and to merge ACW maps for NativeAOT keep-rule generation when using prebuilt artifacts.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs | Adds coverage for generateRootAssembly:false and reference-only scanning. |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs | Adds coverage for shared-framework typemap references anchored on Java.Lang.Object. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs | Updates build assertions to reflect prebuilt framework typemap consumption. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs | Adds task inputs to skip scanning prebuilt assemblies and to control root/shared-universe behavior. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | Adds app-build consumption of prebuilt typemap DLLs and precompiled JCW jar; introduces keep-rule ACW map selection. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets | Merges app + framework ACW maps for R8 keep rules when using prebuilt framework typemap. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | Ensures post-trim typemap generation also treats framework assemblies as prebuilt reference-only inputs. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs | Adds options to skip root emission and to pass shared-framework typemap names to the root generator. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs | Supports “reference-only” inputs: index for type resolution but skip peer emission. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs | Adds support for emitting TypeMapAssemblyTarget<Java.Lang.Object> entries for shared framework typemaps and adjusts aggregate loader init. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs | Extends AssemblyInput with ScanForPeers. |
| build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets | Introduces SDK-pack build targets to generate and package prebuilt typemap artifacts + JCW jar. |
| build-tools/create-packs/Microsoft.Android.Sdk.proj | Imports the new SDK-pack typemap generation targets. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
Mark generated typemap publish items as known Mono.Android references so ProcessAssemblies does not reopen and scan every per-assembly typemap DLL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This reverts commit 90fbc3d.
Reuse the framework JCWs already shipped in mono.android.jar instead of packaging a duplicate JAR. Generate CoreCLR R8 keep rules from the linked Mono.Android assembly so trimmed framework implementors can still be removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use pre-generated framework typemaps only for Debug per-assembly universes, generate Release shared-universe maps from the complete application closure, and package mono.android.jar for the retained pre-generated path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ccdb863-10f6-4e6d-8cd4-9085e63fda6c
Resolve mono.android.jar through the established platform-jar path before static resources, Java compilation, and dex compilation. Add coverage that verifies a framework listener implementor reaches classes.dex. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ccdb863-10f6-4e6d-8cd4-9085e63fda6c
Compile the framework wrappers generated with the prebuilt typemap into a dedicated SDK jar. These wrappers use registerNatives and avoid the legacy TypeManager.n_activate path that CoreCLR trimmable apps do not export. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ccdb863-10f6-4e6d-8cd4-9085e63fda6c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0ccdb863-10f6-4e6d-8cd4-9085e63fda6c
|
/review |
|
✅ Android PR Reviewer completed successfully!
|
There was a problem hiding this comment.
❌ Reject
Found 3 errors in the pre-generated framework typemap pipeline:
- valid apps with no managed Java peers do not emit the root typemap assembly needed to activate the prebuilt maps;
- incremental builds can retain a root assembly whose framework-map references reflect the previous configuration;
- SDK pack incrementality can leave
mono.android-typemaps.jarstale relative to newly generated Java sources.
The separation between framework maps and the app root is otherwise clear, and the added build coverage exercises the normal Debug/Release paths well. The latest Azure build is still in progress with no reported failure in the completed checks, so CI does not yet provide a final result.
Generated by Android PR Reviewer for #12127 · gpt56 · 231.9 AIC · ⌖ 9.36 AIC · ⊞ 25.7K
Comment /review to run again
Generate the root for framework-only maps, content-compare root updates, track the generated Java source set, and require every packaged artifact before enabling pre-generated typemaps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Goal
Pre-generate the trimmable type maps for Mono.Android and Java.Interop at SDK build time (issue #10792), so app builds no longer rescan the framework's Java peer types on every build and startup avoids re-initializing those maps.
Approach
The framework typemaps are generated once during the SDK/pack build and shipped in
Microsoft.Android.Sdkunderdata/prebuilt-typemap:_Mono.Android.TypeMap.dlland_Java.Interop.TypeMap.dll— anchored on theJava.Lang.Objectuniverse so one artifact supports both shared and aggregate app typemap modes.mono.android-typemaps.jar— 320 precompiled Java Callable Wrappers.mono.android-acw-map.txt— the framework ACW map, merged with the app map for NativeAOT/R8 keep-rule generation.The app build still indexes Mono.Android and Java.Interop for type resolution, but does not scan them for Java peers (
AssemblyInput.ScanForPeers=false). The app-generated root assembly references the two prebuilt framework typemaps via[assembly: TypeMapAssemblyTarget<Java.Lang.Object>].Build-time measurements
Measured on a clean
dotnet new mauiAndroid app on an Apple Silicon Mac:net11.0-androidAndroidTypeMapImplementation=trimmablebin/Debugandobj/Debugremoved before every build--no-restoreGenerateTrimmableTypeMaptask meanGenerateTrimmableTypeMaptask median_GenerateTrimmableTypeMaptarget mean“Before” explicitly disables the prebuilt artifacts with
_AndroidUsePreGeneratedMonoAndroidTypeMap=false; “after” enables them. The before build generated 82 typemap assemblies, including_Mono.Android.TypeMap.dlland_Java.Interop.TypeMap.dll. The after build generated 80 and consumed both framework typemaps from the SDK pack.A subsequent 5-build measurement found that adding the per-assembly typemap DLLs initially increased
ProcessAssembliesfrom a 0.122s mean on LLVM-IR builds to 2.731s on trimmable builds. The generated publish items are now marked with their knownHasMonoAndroidReference=truemetadata, avoiding 82 redundant PE opens and assembly-reference scans. With that change,ProcessAssemblieshas a 0.122s median (0.162s mean, including a 0.383s cold run), down from a 3.132s median before the optimization.Verification
Microsoft.Android.Sdk.TrimmableTypeMap.Testspass.classes.dex.MainActivity.MainActivity.