diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 2d1201e2cfc..6ff354ee634 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -7,18 +7,22 @@ + turns it into R8 -keep rules so R8 can shrink the unused JCWs from classes.dex. Optimized builds + default this on because large binding closures can otherwise add several MB of compressed DEX. + Unoptimized builds keep it off: their codegen graph is larger and provides no useful Java trimming + when the NativeAOT compilation itself retains the complete managed closure. --> + <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' and '$(Optimize)' == 'true' ">true <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' ">false + + <_AndroidBuildRuntimeIdentifiersInParallel + Condition=" ('$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' or '$(IlcGenerateDgmlFile)' == 'true') and '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">false <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider r8 d8 True True - - true <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true <_TrimmableNativeAotProguardConfigurationInputsStamp>$(_AndroidStampDirectory)_GenerateTrimmableTypeMapProguardConfiguration.inputs <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration @@ -55,6 +59,16 @@ <_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Remove="@(_TrimmableTypeMapFrameworkIlcAssemblyNames)" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs index 08e768d315f..b8369374be9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs @@ -24,9 +24,9 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask public string OutputFile { get; set; } = ""; // When false, the ILC DGML is not consulted (it may not have been generated at all) and a - // -keep rule is emitted for every Java Callable Wrapper in the ACW map, so R8 keeps them all - // instead of shrinking the unused ones. This trades a small amount of dex size for skipping the - // very large DGML files and the DGML parsing/scan, which dominate NativeAOT build time. + // -keep rule is emitted for every Java type in the ACW map, so R8 keeps them all instead of + // shrinking the unused ones. Large binding closures can add several MB of compressed DEX, but + // this avoids generating and processing the very large ILC dependency graph. public bool TrimJavaCallableWrappers { get; set; } = true; public override bool RunTask () @@ -56,7 +56,7 @@ public override bool RunTask () retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); } - // A null retainedTypeKeys means "keep every ACW" (Java trimming disabled). + // A null retainedTypeKeys means "keep every Java type in the ACW map" (Java trimming disabled). var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys); using var writer = new StringWriter (); @@ -69,7 +69,7 @@ public override bool RunTask () if (TrimJavaCallableWrappers) { Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); } else { - Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT ProGuard rules keeping all ACWs (Java Callable Wrapper trimming is disabled).", javaTypes.Count); + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT ProGuard rules keeping every Java type in the ACW map (Java trimming is disabled).", javaTypes.Count); } return !Log.HasLoggedErrors; } @@ -101,7 +101,18 @@ HashSet LoadRetainedTypeKeysFromDgml () XmlResolver = null, }); + bool readingNodes = false; while (reader.Read ()) { + if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "Nodes") { + readingNodes = true; + continue; + } + if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "Nodes") { + break; + } + if (!readingNodes) { + continue; + } if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Node") { continue; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index ef849d57c91..a3bc8f275a9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -257,6 +257,9 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata + + + """); File.WriteAllText (acwMapFile, """ diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 310eaafa3f2..c7d62e1ddc2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -47,6 +47,7 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease }; proj.SetRuntime (runtime); proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + bool trimNativeAotJavaCode = isRelease && runtime == AndroidRuntime.NativeAOT; using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); @@ -56,11 +57,26 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease var typemapDlls = Directory.GetFiles (intermediateDir, "*.dll"); Assert.IsNotEmpty (typemapDlls, "First build should have generated typemap DLL(s)."); + string scanDgml = ""; + DateTime scanDgmlTimestamp = default; + if (trimNativeAotJavaCode) { + var ridIntermediateDir = builder.Output.GetIntermediaryPath ("android-arm64"); + scanDgml = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.scan.dgml.xml"); + var codegenDgml = Path.Combine (ridIntermediateDir, "native", $"{proj.ProjectName}.codegen.dgml.xml"); + FileAssert.Exists (scanDgml); + FileAssert.DoesNotExist (codegenDgml, "Optimized builds should emit only the scan DGML needed for Java trimming."); + scanDgmlTimestamp = File.GetLastWriteTimeUtc (scanDgml); + } + Assert.IsTrue (builder.Build (proj), "Second build should have succeeded."); Assert.IsTrue ( builder.Output.IsTargetSkipped ("_GenerateJavaStubs"), "_GenerateJavaStubs should be skipped on incremental build."); + if (trimNativeAotJavaCode) { + builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMapProguardConfiguration"); + Assert.AreEqual (scanDgmlTimestamp, File.GetLastWriteTimeUtc (scanDgml), "No-op builds should not rewrite the scan DGML."); + } foreach (var typemapDll in typemapDlls) { FileAssert.Exists (typemapDll, $"No-op builds should preserve generated typemap assembly {typemapDll} when _GenerateTrimmableTypeMap is skipped."); }