Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (#8172)
Browse files Browse the repository at this point in the history
Context: xamarin/monodroid@388bf4b
Context: 59ec488
Context: c929289
Context: 88215f9
Context: dotnet/runtime#86722
Context: dotnet/runtime#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
    (c929289)

Re-intoduce IL stripping for .NET 8.

Add a new `$(AndroidStripILAfterAOT)` MSBuild property.  When true,
the `<MonoAOTCompiler/>` task will track which method bodies
were actually AOT'd, storing this information into
`%(_MonoAOTCompiledAssemblies.MethodTokenFile)`, and the new
`<ILStrip/>` 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`:

	<PropertyGroup>
	  <AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
	  <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
	</PropertyGroup>

`.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 <yangfan@microsoft.com>

[0]: https://www.mono-project.com/docs/advanced/aot/#full-aot
[1]: https://github.com/mono/mono/tree/2020-02/mcs/tools/cil-strip
  • Loading branch information
jonathanpeppers committed Aug 22, 2023
1 parent 5a1b9b1 commit 4e86e66
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 2 deletions.
14 changes: 14 additions & 0 deletions Documentation/guides/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
<Output TaskParameter="CompiledAssemblies" ItemName="_MonoAOTCompiledAssemblies" />
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
</MonoAOTCompiler>
<ILStrip
Condition=" '$(AndroidStripILAfterAOT)' == 'true' "
TrimIndividualMethods="true"
Assemblies="@(_MonoAOTCompiledAssemblies)"
DisableParallelStripping="$(_DisableParallelAot)">
<Output TaskParameter="TrimmedAssemblies" ItemName="_ILStripTrimmedAssemblies" />
</ILStrip>
<Move
Condition=" '$(AndroidStripILAfterAOT)' == 'true' "
SourceFiles="@(_ILStripTrimmedAssemblies->'%(TrimmedAssemblyFileName)')"
DestinationFiles="@(_ILStripTrimmedAssemblies)"
/>
<WriteLinesToFile
File="$(_AndroidStampDirectory)_AndroidAot.stamp"
Lines="@(_MonoAOTCompiledAssemblies->'%(LibraryFile)')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
<_AndroidXA1029 Condition=" '$(AotAssemblies)' != '' ">true</_AndroidXA1029>
<_AndroidXA1030 Condition=" '$(RunAOTCompilation)' == 'true' and '$(PublishTrimmed)' == 'false' ">true</_AndroidXA1030>
<AotAssemblies>$(RunAOTCompilation)</AotAssemblies>
<AndroidEnableProfiledAot Condition=" '$(AndroidEnableProfiledAot)' == '' and '$(RunAOTCompilation)' == 'true' ">true</AndroidEnableProfiledAot>
<AndroidEnableProfiledAot Condition=" '$(AndroidEnableProfiledAot)' == '' and '$(RunAOTCompilation)' == 'true' and '$(AndroidStripILAfterAOT)' != 'true' ">true</AndroidEnableProfiledAot>

<!--
Runtime libraries feature switches defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1973,7 +1973,7 @@ because xbuild doesn't support framework reference assemblies.

<!-- Shrink Mono.Android.dll by removing attribute only needed for GenerateJavaStubs -->
<RemoveRegisterAttribute
Condition="'$(AndroidLinkMode)' != 'None' AND '$(AndroidIncludeDebugSymbols)' != 'true'"
Condition="'$(AndroidLinkMode)' != 'None' AND '$(AndroidIncludeDebugSymbols)' != 'true' AND '$(AndroidStripILAfterAOT)' != 'true'"
ShrunkFrameworkAssemblies="@(_ShrunkAssemblies)" />

<MakeDir Directories="$(MonoAndroidIntermediateAssemblyDir)shrunk" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
</ItemGroup>

<ItemGroup>
<Reference Include="Xamarin.Android.Cecil">
<HintPath>$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Cecil.dll</HintPath>
</Reference>
<PackageReference Include="NodaTime" Version="2.4.5" />
<PackageReference Include="MSBuild.StructuredLogger" Version="2.1.787" />
<PackageReference Include="ICSharpCode.Decompiler" Version="7.2.1.6856" />
Expand Down
39 changes: 39 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.");
}

}
}

0 comments on commit 4e86e66

Please sign in to comment.