Skip to content

[TrimmableTypeMap] Generate NativeAOT ProGuard rules from DGML#11449

Open
simonrozsival wants to merge 8 commits into
mainfrom
dev/simonrozsival/11052-nativeaot-proguard-ilc-metadata
Open

[TrimmableTypeMap] Generate NativeAOT ProGuard rules from DGML#11449
simonrozsival wants to merge 8 commits into
mainfrom
dev/simonrozsival/11052-nativeaot-proguard-ilc-metadata

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 22, 2026

Summary

  • enable R8 by default for NativeAOT trimmable typemap builds
  • generate trimmable NativeAOT ProGuard keep rules after IlcCompile by intersecting retained managed type metadata from ILC scan DGML with acw-map.txt
  • keep the DGML-based logic in a dedicated GenerateNativeAotProguardConfiguration task, leaving the existing linked-assembly GenerateProguardConfiguration path unchanged
  • use a trimmable NativeAOT ProGuard resource for the common Xamarin rules so R8 can remove unused generated Java wrappers

Stacked on #11292
Related to dotnet/runtime#120204
Part of #10790
Part of #11052

Results

HelloWorld NativeAOT android-arm64 Release with trimmable typemap. Before is the existing default without R8 Java shrinking; after is the new default R8 behavior from this PR.

Metric Before After
APK size 3,638.4 kB 3,572.8 kB
classes.dex size 259.4 kB 15.2 kB
compressed classes.dex in APK 72.2 kB 7.0 kB
Java class count 364 21
Java method count 3,035 155

The HelloWorld ILC scan DGML input is 24,044,488 bytes and acw-map.txt has 27,234 lines. The generated ProGuard configuration has 49 rules.

20-run measurement on Apple M1 for the isolated GenerateNativeAotProguardConfiguration task:

Metric Task time Isolated MSBuild wall time
Average 377 ms 918 ms
Median 244 ms 837 ms
Min 152 ms 518 ms
Max 856 ms 1,565 ms
P90 803 ms 1,327 ms
Std dev 261 ms 287 ms

Validation

  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj -c Debug -nr:false --nologo -v minimal
  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj -c Debug -nr:false --nologo -v minimal
  • MSBUILDDISABLENODEREUSE=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

Copilot AI review requested due to automatic review settings May 22, 2026 08:14
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels May 22, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IlcCompile from *.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 IsNativeAotRuntime and 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.

Comment thread src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Outdated
Comment thread src/Xamarin.Android.Build.Tasks/Tasks/GenerateProguardConfiguration.cs Outdated
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/11052-nativeaot-typemap-init May 22, 2026 08:20
Base automatically changed from dev/simonrozsival/11052-nativeaot-typemap-init to main May 22, 2026 14:26
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/11052-nativeaot-proguard-ilc-metadata branch from 7b41d33 to 158bce0 Compare May 22, 2026 15:24
simonrozsival and others added 6 commits May 23, 2026 00:27
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>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/11052-nativeaot-proguard-ilc-metadata branch from 158bce0 to 3d4e1cd Compare May 22, 2026 22:28
@simonrozsival
Copy link
Copy Markdown
Member Author

@copilot there is a failure in CI, fix it:
image

Agent-Logs-Url: https://github.com/dotnet/android/sessions/91b096b3-9ab7-4980-8234-097a0c3caca5

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 23, 2026

@copilot there is a failure in CI, fix it:
image

Fixed in a5277f4. The NativeAOT trimmable typemap target now filters unmanaged-entrypoint roots by assembly name instead of full generated paths, so framework typemap assemblies stay as ILC references without being exported.

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>
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label May 25, 2026
@simonrozsival
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

Android PR Reviewer completed successfully!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes

Issue counts: 0 ❌ errors · 3 ⚠️ warnings · 2 💡 suggestions

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:

  1. O(n2) dedup in LoadJavaTypesFromAcwMapList.Contains() for dedup is quadratic; use a HashSet alongside the list.
  2. Uncoded error messagesLog.LogError calls in GenerateNativeAotProguardConfiguration should use Log.LogCodedError with XA#### codes and Properties.Resources.
  3. 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 GenerateProguardConfiguration path untouched.
  • Good use of XmlReader (streaming) over LINQ-to-XML for parsing the large DGML files.
  • Proper DtdProcessing.Prohibit and XmlResolver = null for XML security.
  • The MSBuild targets have correct Inputs/Outputs for incremental build support.
  • The _ReadGeneratedTrimmableTypeMapAssemblies fallback with MakeDir+Touch is 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)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Performance!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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ NullableGetManifestResourceStream 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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 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.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Error handling — Error messages use bare 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants