From 82a409278062bd8f5a5bf59d77dd2e66853017a8 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 22 Aug 2023 09:24:46 -0500 Subject: [PATCH] [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (#8172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/xamarin/monodroid/commit/388bf4b392c59fc35f51c5eadd0a6218a37a394c Context: 59ec488b4005a09fc0e5f330f217e78c3fa14724 Context: c929289a7b77ecbc90c3e10aa55f9f3027ed3345 Context: 88215f93fdcf14fa1a2aebbb499c09c6bdb8fa8c Context: https://github.com/dotnet/runtime/pull/86722 Context: https://github.com/dotnet/runtime/issues/44855 Once Upon A Time™ we had a brilliant thought: if AOT pre-compiles C# methods, do we need the managed method anymore? Removing the C# method body would allow assemblies to be smaller. ("Even better", iOS does this too! Why Can't Android™?!) While the idea is straightforward, implementation was not: iOS uses ["Full" AOT][0], which AOT's *all* methods into a form that doesn't require a runtime JIT. This allowed iOS to run [`cil-strip`][1], removing all method bodies from all managed types. At the time, Xamarin.Android only supported "normal" AOT, and normal AOT requires a JIT for certain constructs such as generic types and generic methods. This meant that attempting to run `cil-strip` would result in runtime errors if a method body was removed that was actually required at runtime. (This was particularly bad because `cil-strip` could only remove *all* method bodies, not some!) This limitation was relaxed with the introduction of "Hybrid" AOT, which is "Full AOT while supporting a JIT". This meant that *all* methods could be AOT'd without requiring a JIT, which allowed method bodies to be removed; see xamarin/monodroid@388bf4b3. Unfortunately, this wasn't a great long-term solution: 1. Hybrid AOT was restricted to Visual Studio Enterprise customers. 2. Enabling Hybrid AOT would slow down Release configuration builds. 3. Hybrid AOT would result in larger apps. 4. As a consequence of (1), it didn't get as much testing 5. `cil-strip` usage was dropped as part of the .NET 5+ migration (c929289a) Re-intoduce IL stripping for .NET 8. Add a new `$(AndroidStripILAfterAOT)` MSBuild property. When true, the `` task will track which method bodies were actually AOT'd, storing this information into `%(_MonoAOTCompiledAssemblies.MethodTokenFile)`, and the new `` task will update the input assemblies, removing all method bodies that can be removed. By default setting `$(AndroidStripILAfterAOT)`=true will *override* the default `$(AndroidEnableProfiledAot)` setting, allowing all trimmable AOT'd methods to be removed. Profiled AOT and IL stripping can be used together by explicitly setting both within the `.csproj`: true true `.apk` size results for a `dotnet new android` app: | `$(AndroidStripILAfterAOT)` | `$(AndroidEnableProfiledAot)` | `.apk` size | | --------------------------- | ----------------------------- | ------------- | | true | true | 7.7MB | | true | false | 8.1MB | | false | true | 7.7MB | | false | false | 8.4MB | Note that `$(AndroidStripILAfterAOT)`=false and `$(AndroidEnableProfiledAot)`=true is the *default* Release configuration environment, for 7.7MB. A project that *only* sets `$(AndroidStripILAfterAOT)`=true implicitly sets `$(AndroidEnableProfiledAot)`=false, resulting in an 8.1MB app. Co-authored-by: Fan Yang [0]: https://www.mono-project.com/docs/advanced/aot/#full-aot [1]: https://github.com/mono/mono/tree/2020-02/mcs/tools/cil-strip --- .../guides/building-apps/build-properties.md | 14 +++++++ .../targets/Microsoft.Android.Sdk.Aot.targets | 14 +++++++ ...soft.Android.Sdk.DefaultProperties.targets | 2 +- .../Xamarin.Android.Common.targets | 2 +- .../MSBuildDeviceIntegration.csproj | 3 ++ .../Tests/InstallAndRunTests.cs | 39 +++++++++++++++++++ 6 files changed, 72 insertions(+), 2 deletions(-) diff --git a/Documentation/guides/building-apps/build-properties.md b/Documentation/guides/building-apps/build-properties.md index 7743863813b..afe06d94074 100644 --- a/Documentation/guides/building-apps/build-properties.md +++ b/Documentation/guides/building-apps/build-properties.md @@ -1303,6 +1303,20 @@ This is only used when building `system` applications. Support for this property was added in Xamarin.Android 11.3. +## AndroidStripILAfterAOT + +A bool property that specifies whether or not the *method bodies* of AOT compiled methods will be removed. + +The default value is `false`, and the method bodies of AOT compiled methods will *not* be removed. + +When set to `true`, [`$(AndroidEnableProfiledAot)`](#androidenableprofiledaot) is set to `false` by default. +This means that in Release configuration builds -- in which +[`$(RunAOTCompilation)`](#runaotcompilation) is `true` by default -- AOT is enabled for *everything*. +This can result in increased app sizes. This behavior can be overridden by explicitly setting +`$(AndroidEnableProfiledAot)` to `true` within your project file. + +Support for this property was added in .NET 8. + ## AndroidSupportedAbis A string property that contains a diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Aot.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Aot.targets index db621d58ecf..7e2c2fd97f8 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Aot.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Aot.targets @@ -123,11 +123,25 @@ They run in a context of an inner build with a single $(RuntimeIdentifier). LLVMPath="$(_LLVMPath)" LdName="$(_LdName)" LdFlags="$(_LdFlags)" + CollectTrimmingEligibleMethods="$(AndroidStripILAfterAOT)" + TrimmingEligibleMethodsOutputDirectory="$(IntermediateOutputPath)tokens" WorkingDirectory="$(MSBuildProjectDirectory)" AotArguments="$(AndroidAotAdditionalArguments)"> + + + + true <_AndroidXA1030 Condition=" '$(RunAOTCompilation)' == 'true' and '$(PublishTrimmed)' == 'false' ">true $(RunAOTCompilation) - true + true diff --git a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj index 8f6b1ecd0b8..70616562284 100644 --- a/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj +++ b/tests/MSBuildDeviceIntegration/MSBuildDeviceIntegration.csproj @@ -37,6 +37,9 @@ + + $(MicrosoftAndroidSdkOutDir)Xamarin.Android.Cecil.dll + diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index fc60f88a1b7..1672b36b7cb 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Xml.Linq; using System.Xml.XPath; +using Mono.Cecil; using NUnit.Framework; using Xamarin.ProjectTools; @@ -1075,5 +1076,43 @@ public void SupportDesugaringStaticInterfaceMethods () ); } + [Test] + public void EnableAndroidStripILAfterAOT ([Values (false, true)] bool profiledAOT) + { + var proj = new XamarinAndroidApplicationProject { + ProjectName = nameof (EnableAndroidStripILAfterAOT), + RootNamespace = nameof (EnableAndroidStripILAfterAOT), + IsRelease = true, + EnableDefaultItems = true, + }; + proj.SetProperty("AndroidStripILAfterAOT", "true"); + proj.SetProperty("AndroidEnableProfiledAot", profiledAOT.ToString ()); + // So we can use Mono.Cecil to open assemblies directly + proj.SetProperty ("AndroidEnableAssemblyCompression", "false"); + + var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed"); + + var apk = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{proj.PackageName}-Signed.apk"); + FileAssert.Exists (apk); + var helper = new ArchiveAssemblyHelper (apk); + Assert.IsTrue (helper.Exists ($"assemblies/{proj.ProjectName}.dll"), $"{proj.ProjectName}.dll should exist in apk!"); + using (var stream = helper.ReadEntry ($"assemblies/{proj.ProjectName}.dll")) { + stream.Position = 0; + using var assembly = AssemblyDefinition.ReadAssembly (stream); + var type = assembly.MainModule.GetType ($"{proj.RootNamespace}.MainActivity"); + var method = type.Methods.FirstOrDefault (p => p.Name == "OnCreate"); + Assert.IsNotNull (method, $"{proj.RootNamespace}.MainActivity.OnCreate should exist!"); + Assert.IsTrue (!method.HasBody || method.Body.Instructions.Count == 0, $"{proj.RootNamespace}.MainActivity.OnCreate should have no body!"); + } + + RunProjectAndAssert (proj, builder); + + WaitForPermissionActivity (Path.Combine (Root, builder.ProjectDirectory, "permission-logcat.log")); + bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity", + Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), 30); + Assert.IsTrue(didLaunch, "Activity should have started."); + } + } }