[TrimmableTypeMap] Generate NativeAOT ProGuard rules from DGML#11449
[TrimmableTypeMap] Generate NativeAOT ProGuard rules from DGML#11449simonrozsival wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a trimmable NativeAOT + R8 workflow where ProGuard/R8 keep rules are generated from NativeAOT ILC DGML scan output intersected with acw-map.txt, allowing R8 to remove unused generated Java wrappers and significantly reduce classes.dex size.
Changes:
- Generate NativeAOT-specific ProGuard rules after
IlcCompilefrom*.scan.dgml.xml+acw-map.txt, and adjust R8 configuration generation to avoid broad keep rules in this mode. - Extend the trimmable typemap pipeline to classify framework peers, selectively emit array typemap entries only when referenced from non-framework assemblies, and persist the list of generated typemap assemblies to stabilize incremental builds.
- Add runtime feature plumbing for
IsNativeAotRuntimeand adjust NativeAOT JNI initialization ordering/inputs to provide required Java peer marker classes.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs | Adds framework-like SSL and network peer types to exercise framework JCW/peer scanning scenarios. |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs | Adds test coverage for framework peer marking and array-entry emission behavior. |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs | Tests framework ACW conditionality and array-entry emission rules; updates anchor visibility assertions. |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs | Verifies generator can emit expected framework JCW Java sources. |
| src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets | Plumbs UseTrimmableNativeAotProguardConfiguration into the R8 task invocation. |
| src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | Adds _AndroidTrimmableTypeMapMaxArrayRank to the property cache for incremental invalidation. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.MonoVM.apkdesc | Updates expected APK contents/sizes after build output changes. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc | Updates expected APK contents/sizes after build output changes. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs | Adds incremental/build validation for array-rank changes and NativeAOT/CoreCLR typemap behaviors. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs | Adds task-level tests for generated typemap assembly list file and framework JCW emission. |
| src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | Adds a trimmable NativeAOT mode that alters generated ProGuard config inputs and common rules. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs | Adds framework assembly classification and writes a generated-assemblies list file. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs | Adds DGML+ACW-map based ProGuard rule generation for NativeAOT trimmable builds. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | Persists typemap-generated assembly list and uses it for downstream item population/incremental correctness. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets | Hooks DGML-based ProGuard rule generation and adjusts ILC inputs for trimmable typemap NativeAOT. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | Adds opt-out switch to skip linked-assembly ProGuard configuration generation when replaced by DGML flow. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets | Enables DGML generation for R8 builds, configures skip/alternate ProGuard generation, and sets runtime feature. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets | Sets IsNativeAotRuntime=false runtime feature for MonoVM. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets | Sets IsNativeAotRuntime=false runtime feature for CoreCLR. |
| src/native/nativeaot/host/host.cc | Ensures NativeAOT host provides global refs for required Java peer marker classes during init. |
| src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs | Adds RuntimeFeature.IsNativeAotRuntime AppContext switch. |
| src/Mono.Android/Android.Runtime/JNIEnvInit.cs | Refactors JNI initialization to share common state init and add NativeAOT-specific runtime initialization entrypoint. |
| src/Mono.Android/Android.Runtime/JNIEnv.cs | Routes unhandled exception propagation consistently for NativeAOT. |
| src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs | Treats NativeAOT unhandled exceptions like the CoreCLR path. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs | Passes framework assembly names into scanning and tightens JCW generation filtering logic. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs | Tracks framework peers and controls array-entry emission based on cross-assembly references. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs | Adds IsFrameworkAssembly and GenerateArrayEntries to peer model. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs | Indexes referenced types by referenced assembly to support framework peer reference detection. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs | Updates documentation around __ArrayMapRank{N} anchors. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs | Adds no-array-map initialization paths when max array rank is 0. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs | Adds additional unconditional types and prevents unconditional rooting for framework ACWs; gates array emission. |
| src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs | Uses the refactored JNIEnvInit initialization path and reuses common type/value manager creation helpers. |
7b41d33 to
158bce0
Compare
Use NativeAOT scan DGML to identify retained managed types, intersect them with acw-map.txt, and emit concrete R8 keep rules for trimmable typemap NativeAOT builds. This avoids keeping the broad generated Java wrapper set when R8 is enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move DGML-based NativeAOT ProGuard generation into a separate MSBuild task so GenerateProguardConfiguration remains focused on linked assembly scanning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep NativeAOT.targets free of ProGuard coordination properties. Let LlvmIr targets hardcode linked-assembly ProGuard timing for ILLink and NativeAOT, and keep the trimmable NativeAOT R8 mode flag with the trimmable NativeAOT typemap targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the trimmable NativeAOT common R8 rules to an embedded ProGuard resource, keep GenerateProguardConfiguration unchanged, add a shared trimmable typemap ProGuard target name with a CoreCLR stub, and restore the existing linked-assembly ProGuard target wiring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Default NativeAOT trimmable typemap builds to AndroidLinkTool=r8 so the generated DGML-based keep rules are active without requiring users to opt in manually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tolerate the no-input typemap generation case by creating an empty assembly list, avoid nested framework reference scans, and disable XML resolver use when reading ILC DGML. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
158bce0 to
3d4e1cd
Compare
|
@copilot there is a failure in CI, fix it: |
Agent-Logs-Url: https://github.com/dotnet/android/sessions/91b096b3-9ab7-4980-8234-097a0c3caca5 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Collect NativeAOT DGML files from per-RID intermediate output paths and merge their retained type metadata into the single ProGuard configuration consumed by R8/D8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
⚠️ Needs Changes
Issue counts: 0 ❌ errors · 3
Summary
The approach is solid — intersecting ILC DGML metadata with acw-map.txt to generate focused ProGuard rules is well-designed, and the size reduction results are impressive (classes.dex from 259 kB → 15 kB).
Key items to address:
- O(n2) dedup in
LoadJavaTypesFromAcwMap—List.Contains()for dedup is quadratic; use aHashSetalongside the list. - Uncoded error messages —
Log.LogErrorcalls inGenerateNativeAotProguardConfigurationshould useLog.LogCodedErrorwithXA####codes andProperties.Resources. - Null-unsafe
GetManifestResourceStream— the new code path in R8.cs calls.CopyTo()on a potentially null stream.
Positive callouts:
- Clean separation: DGML-based logic stays in a dedicated task, leaving the existing
GenerateProguardConfigurationpath untouched. - Good use of
XmlReader(streaming) over LINQ-to-XML for parsing the large DGML files. - Proper
DtdProcessing.ProhibitandXmlResolver = nullfor XML security. - The MSBuild targets have correct
Inputs/Outputsfor incremental build support. - The
_ReadGeneratedTrimmableTypeMapAssembliesfallback withMakeDir+Touchis a nice resilience improvement.
Generated by Android PR Reviewer for issue #11449 · ● 18.8M
| } | ||
| var managedTypeName = line.Substring (0, separator); | ||
| var javaTypeName = line.Substring (separator + 1); | ||
| if (retainedTypeKeys.Contains (managedTypeName) && !javaTypes.Contains (javaTypeName)) { |
There was a problem hiding this comment.
🤖 !javaTypes.Contains(javaTypeName) performs a linear scan on every iteration, making this loop O(n2) in the number of matched types. With 27K-line acw-map.txt inputs mentioned in the PR description, this can be noticeable.
Use a HashSet<string> for deduplication alongside the List<string> for ordering, or switch to a LinkedHashSet-style pattern:
var seen = new HashSet<string> (StringComparer.Ordinal);
var javaTypes = new List<string> ();
// ...
if (retainedTypeKeys.Contains (managedTypeName) && seen.Add (javaTypeName)) {
javaTypes.Add (javaTypeName);
}Rule: Watch for O(n2) (Postmortem #41)
| continue; | ||
| } | ||
|
|
||
| var assemblyStart = "Type metadata: [".Length; |
There was a problem hiding this comment.
🤖 💡 Performance — "Type metadata: [".Length is recomputed on every loop iteration. Consider extracting it to a const or static readonly field:
const string TypeMetadataPrefix = "Type metadata: [";
// ...
if (!label.StartsWith (TypeMetadataPrefix, StringComparison.Ordinal)) {
continue;
}
var assemblyStart = TypeMetadataPrefix.Length;This also improves readability by avoiding the duplicated string literal between lines 92 and 96.
Rule: Cache repeated accessor calls
| using (var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration)) { | ||
| GetType ().Assembly.GetManifestResourceStream ("proguard_xamarin.cfg").CopyTo (xamcfg.BaseStream); | ||
| if (UseTrimmableNativeAotProguardConfiguration) { | ||
| GetType ().Assembly.GetManifestResourceStream ("proguard_trimmable_nativeaot.cfg").CopyTo (xamcfg.BaseStream); |
There was a problem hiding this comment.
🤖 GetManifestResourceStream returns Stream?. If the resource name is wrong (e.g., the LogicalName metadata differs from the bare filename), this will throw a NullReferenceException on .CopyTo(). Consider adding a null check:
var stream = GetType ().Assembly.GetManifestResourceStream ("proguard_trimmable_nativeaot.cfg")
?? throw new InvalidOperationException ("Missing embedded resource 'proguard_trimmable_nativeaot.cfg'");
stream.CopyTo (xamcfg.BaseStream);(The pre-existing line 119 has the same issue, but since this PR adds a new code path, it's worth getting right here.)
Rule: Never use ! (null-forgiving operator) — and don't call .Method() on nullable returns without checking
| </PropertyGroup> | ||
|
|
||
| <!-- TODO: Generate a focused ProGuard configuration from the ILLink-trimmed typemap. --> | ||
| <Target Name="_GenerateTrimmableTypeMapProguardConfiguration" |
There was a problem hiding this comment.
🤖 💡 Documentation — The TODO comment should reference a tracking issue so it doesn't get forgotten. Per repo conventions, // TODO hidden in code should have a filed issue referenced in the comment.
Rule: Track TODOs as issues (Postmortem #60)
| } | ||
|
|
||
| if (NativeAotDgmlFiles.Length == 0) { | ||
| Log.LogError ("No NativeAOT DGML files were provided."); |
There was a problem hiding this comment.
🤖 Log.LogError without coded error codes (XA####) and without going through Properties.Resources. Per repo conventions, errors should use Log.LogCodedError("XA####", Properties.Resources.XA####, ...) so they are documented and localizable. This applies to lines 33, 38, and 43.
Rule: Use Log.LogCoded* methods (Postmortem #10)

Summary
IlcCompileby intersecting retained managed type metadata from ILC scan DGML withacw-map.txtGenerateNativeAotProguardConfigurationtask, leaving the existing linked-assemblyGenerateProguardConfigurationpath unchangedStacked on #11292
Related to dotnet/runtime#120204
Part of #10790
Part of #11052
Results
HelloWorld NativeAOT
android-arm64Release with trimmable typemap. Before is the existing default without R8 Java shrinking; after is the new default R8 behavior from this PR.classes.dexsizeclasses.dexin APKThe HelloWorld ILC scan DGML input is 24,044,488 bytes and
acw-map.txthas 27,234 lines. The generated ProGuard configuration has 49 rules.20-run measurement on Apple M1 for the isolated
GenerateNativeAotProguardConfigurationtask:Validation
MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj -c Debug -nr:false --nologo -v minimalMSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj -c Debug -nr:false --nologo -v minimalMSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj -t:SignAndroidPackage -c Release -p:PublishAot=true -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -p:AndroidPackageFormat=apk -nr:false --nologo -v minimal