Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@
<PropertyGroup>
<!-- Controls DGML-based trimming of Java Callable Wrappers on the trimmable NativeAOT path.
When true, ILC emits a DGML dependency graph and _GenerateTrimmableTypeMapProguardConfiguration
turns it into R8 -keep rules so R8 can shrink the unused JCWs from classes.dex. Those DGML files
are very large (hundreds of MB) and dominate build time, so this defaults to false until ILC/illink
expose a leaner typemap dump: R8 still runs, but every JCW is kept (a few hundred kB of extra dex)
and the DGML is never generated. -->
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>
<_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' ">false</_AndroidTrimmableTypemapTrimJavaCode>
<!-- Each NativeAOT RID can emit a several-hundred-megabyte dependency graph. Running multiple
graph-producing ILC instances concurrently can exhaust the system file table before javac/R8.
Serialize the inner builds while Java trimming or full ILC DGML diagnostics are active. -->
<_AndroidBuildRuntimeIdentifiersInParallel
Condition=" ('$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' or '$(IlcGenerateDgmlFile)' == 'true') and '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">false</_AndroidBuildRuntimeIdentifiersInParallel>
<_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider</_TrimmableRuntimeProviderJavaName>
<AndroidLinkTool Condition=" '$(AndroidLinkTool)' == '' ">r8</AndroidLinkTool>
<AndroidDexTool Condition=" '$(AndroidLinkTool)' == 'r8' ">d8</AndroidDexTool>
<AndroidEnableProguard Condition=" '$(AndroidLinkTool)' != '' ">True</AndroidEnableProguard>
<AndroidCreateProguardMappingFile Condition=" '$(AndroidCreateProguardMappingFile)' == '' and '$(AndroidLinkTool)' == 'r8' ">True</AndroidCreateProguardMappingFile>
<!-- Only ask ILC for the DGML when JCW trimming is enabled; it is only consumed to compute the keep rules. -->
<IlcGenerateDgmlFile Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' and '$(AndroidLinkTool)' != '' and '$(IlcGenerateDgmlFile)' == '' ">true</IlcGenerateDgmlFile>
<_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true</_UseTrimmableNativeAotProguardConfiguration>
<_TrimmableNativeAotProguardConfigurationInputsStamp>$(_AndroidStampDirectory)_GenerateTrimmableTypeMapProguardConfiguration.inputs</_TrimmableNativeAotProguardConfigurationInputsStamp>
<_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration</_CompileToDalvikDependsOnTargets>
Expand Down Expand Up @@ -55,6 +59,16 @@
<_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Remove="@(_TrimmableTypeMapFrameworkIlcAssemblyNames)" />
<IlcReference Include="@(_TrimmableTypeMapIlcAssemblies)" />
<IlcArg Include="--typemap-entry-assembly:$(_TypeMapAssemblyName)" />
<!-- $(IlcGenerateDgmlFile)=true asks ILC for both the scan and codegen graphs. We need only
the scan graph in optimized builds, or the codegen graph in unoptimized builds where the
scanner does not run. Avoid writing the other several-hundred-megabyte graph unless the
user explicitly requested the full ILC diagnostics. -->
<IlcArg
Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' and '$(AndroidLinkTool)' != '' and '$(IlcGenerateDgmlFile)' != 'true' and '$(Optimize)' == 'true' "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 💡 MSBuild targets — These explicit --scandgmllog/--dgmllog args are only added when IlcGenerateDgmlFile != 'true'. When a user does set IlcGenerateDgmlFile=true, generation falls back to ILC's own DGML output, but _CollectTrimmableNativeAotDgmlFiles only looks for $(TargetName).scan.dgml.xml / $(TargetName).codegen.dgml.xml. Are you sure ILC's IlcGenerateDgmlFile path emits files with those exact names/locations? If it uses a different name (e.g. $(TargetName).dgml.xml), the collect step could come up empty and trip XA4319 — most likely in an unoptimized build where only the codegen graph exists. Worth a quick confirmation since that path is less exercised than the default.

Include="--scandgmllog:$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" />
<IlcArg
Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' and '$(AndroidLinkTool)' != '' and '$(IlcGenerateDgmlFile)' != 'true' and '$(Optimize)' != 'true' "
Include="--dgmllog:$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml" />
<UnmanagedEntryPointsAssembly Include="@(_TrimmableTypeMapUnmanagedEntryPointAssemblyNames)" />
</ItemGroup>
</Target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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 ();
Expand All @@ -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;
}
Expand Down Expand Up @@ -101,7 +101,18 @@ HashSet<string> LoadRetainedTypeKeysFromDgml ()
XmlResolver = null,
});

bool readingNodes = false;
while (reader.Read ()) {
if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "Nodes") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 💡 JNI interop / Robustness — Scoping the scan to the <Nodes> section is a nice perf win, but a self-closing <Nodes /> element never raises an EndElement, so readingNodes would stay true and any later <Node> (e.g. inside <Links>) would be treated as a real node — exactly the case the new test guards against, just for the empty-section variant. Consider if (reader.IsEmptyElement) continue; (leaving readingNodes false) when you set readingNodes = true. ILC always emits a populated <Nodes> today, so this is defensive only.

readingNodes = true;
continue;
}
if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == "Nodes") {
break;
}
if (!readingNodes) {
continue;
}
if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Node") {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata
<Node Id="4" Label="Type metadata: [Xamarin.AndroidX.Activity]AndroidX.Activity.Result.Contract.ActivityResultContracts+TakePicture" />
<Node Id="5" Label="Unrelated node" />
</Nodes>
<Links>
<Node Id="6" Label="Type metadata: [Other.Assembly]Other.Type" />
</Links>
</DirectedGraph>
""");
File.WriteAllText (acwMapFile, """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand All @@ -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.");
}
Expand Down
Loading