-
Notifications
You must be signed in to change notification settings - Fork 533
Build Performance Ideas
Xamarin.Android build times are a key pain point for developers.
Parts of the build Xamarin.Android does not have control over:
-
aapt
to process Android resources -
javac
to compile Java code to*.class
files -
dx
(or soond8
) to convert compiled Java code to Android dex format
But there are still quite a few places we can improve, so we should do that!
For an in-depth comparison between Visual Studio 2017 15.8.4 and what will ship in 15.9 or 16.0, see results here.
The SmartHotel360 app was originally using Xamarin.Forms 2.5.x and Xamarin.Build.Download 0.4.7.
We have MSBuild improvements in both of these packages. After the changes here, I saw drastic improvements to incremental build times:
Build | Before | Logs (Before) | After | Logs (After) |
---|---|---|---|---|
First build (fresh) | 01:24.93 | binlog | 01:11.05 | binlog |
First package | 00:28.81 | binlog | 00:10.47 | binlog |
First install | 00:34.09 | binlog | 00:15.64 | binlog |
Second build (no changes) | 00:22.20 | binlog | 00:03.41 | binlog |
Second package | 00:28.70 | binlog | 00:03.42 | binlog |
Second install | 00:34.27 | binlog | 00:03.42 | binlog |
Third build (change XAML) | 00:34.45 | binlog | 00:11.05 | binlog |
Third package | 00:33.12 | binlog | 00:07.91 | binlog |
Third install | 00:40.62 | binlog | 00:08.29 | binlog |
Changes that made this possible:
- Xamarin.Forms PR #2230: XamlC builds incrementally
- Xamarin.Forms PR #2755: XamlG builds incrementally
- General XamlC perf improvements in Xamarin.Forms: PR #1875, PR #1899, PR #2025
-
Xamarin.Build.Download commit fe5b0aeba:
Added stamp files to
FileWrites
, otherwise caused targets to always re-run
Update your NuGet packages!
We are working on a CI setup. More info to come when that is available.
The idea being:
- On dedicated hardware benchmark builds on a few apps.
- We generate some nice-looking graphs in PowerBI.
- We can have a benchmark of where things stand across different Visual Studio versions.
- Eventually this could run for CI, PR builds, etc.
@pjcollins is working on this. @jonathanpeppers to assist with getting additional data from MSBuild when we get there.
Until then, we will continuing using our Jenkins Plots and do custom measurements locally.
Some of the well-known targets that take up time are:
These are purely under our control, and we should improve them!
Visual Studio 2019 (15.9):
- PR 1957: Design-time builds were causing full builds to "always build" and not building incrementally.
-
PR 1938:
ResolveSdks
caches the output ofjava -version
andjavac -version
in memory, speeding up builds with multiple Xamarin.Android projects. - PR 2088: Fix incremental builds for Xamarin.Forms projects.
-
PR 2093:
Improve LINQ usage in
ConvertResourcesCases
- PR 2130: Move inline C# MSBuild task to a compiled assembly
-
PR 2131:
Remove unnecessary MSBuild target, simplify inputs to
_CompileToDalvik
-
PR 2132:
The
_BuildLibraryImportsCache
target was always running -
PR 2140
Leave
classes.zip
uncompressed, to speed upjavac
anddx
- PR 2105: Java.Interop is no longer a PCL.
Release Notes for Xamarin.Android 9.1
Visual Studio 2019 RC (16.0):
-
PR 2128:
_CopyIntermediateAssemblies
improvements -
PR 2129:
Split up the work in
ConvertResourcesCases
, so some can be skipped -
PR 2150:
More cleanup in
ConvertResourcesCases
-
PR 2148:
Improve Mono.Cecil usage in
BuildApk
task -
PR 2162:
Consolidate
StripEmbeddedLibraries
task with the linker, helps release builds -
PR 2174:
Merge the
CheckTargetFrameworks
task intoResolveAssemblies
-
PR 2223:
Optimize MSBuild
$(AssemblySearchPaths)
-
PR 2309:
Remove unused
Inputs
andOutputs
from MSBuild targets - PR 2019: D8/R8 integration
-
PR 2328:
ConvertResourcesCases
task,RegexOptions.Compiled
and removed more LINQ usage -
PR 2348:
Whitelist support libraries, so
ConvertResourcesCases
will not run against them -
PR 2367:
Fix an issue on first build when enabling
$(AndroidUseAapt2)
-
PR 2535:
Remove all usage of temp files in
GenerateJavaStubs
-
PR 2540:
Use less temp files in
ResolveLibraryProjectImports
Release Notes for Xamarin.Android 9.2
16.1:
PR 2590: Linker improvements in Debug mode.-
PR 2612:
Use System.Reflection.Metadata in the
<ResolveAssemblies/>
MSBuild task -
PR 2624:
Use System.Reflection.Metadata in the
<GetAdditionalResourcesFromAssemblies/>
MSBuild task. -
PR 2626:
Install
can skipBuild
inside of IDEs. -
PR 2643:
<GenerateJavaStubs/>
should only load/evaluateTargetFrameworkIdentifier=MonoAndroid
assemblies.
Release Notes for Xamarin.Android 9.3
16.2:
-
PR 2896:
We don't need to invoke
aapt
to createR.java
for libraries. -
PR 2930:
Perf improvements for
<GetImportedLibraries/>
-
PR 2934:
Filter for
TargetFrameworkIdentifer=MonoAndroid
for two MSBuild targets - ~~PR 2945:~
_LinkAssembliesNoShrink
MSBuild target can skip non-MonoAndroid assemblies in Debug mode.~~ - PR 2952: Removed lots of unnecessary log messages.
-
PR 2975:
Cache
aapt2 version
call across builds
We should investigate using a system-wide Aapt2
cache on NuGet
packages. This would speed up <Aapt />
build times across projects.
See the Github issue for details.
There is a general perf issue, posted here.
We should optimize <GenerateJavaStubs/>
so that it takes assemblies
into consideration, so that it skips processing of assemblies which
have not changed.
We should likewise split up <GenerateJavaStubs/>
because it just
does "too much". Portions of it could be skipped when small C#-code
changes are made to a single assembly.
<GenerateJavaStubs/>
also heavily relies on Mono.Cecil and opens
each assembly that was previously opened by other steps in the build,
such as the linker. We would like to avoid this and/or use
System.Reflection.Metadata instead if possible.
This plan is a decent amount of work, but should have benefits with initial build times, incremental build times, and even help APK sizes. Parts of it could be accomplished in different phases--it would not have to be a single, enormous PR.
- Any data needed by
<GenerateJavaStubs/>
we generate from the linker, which already has every assembly opened. The thought is we would use some XML format, generated for each assembly in a separate file. - We could move RemoveRegisterAttribute into the linker. Have it run against all assemblies. This is somewhat corollary to the goals here, but this would now be possible.
-
<GenerateJavaStubs/>
will be split up: one portion generatesAndroidManifest.xml
, one typemaps, and one the java stubs. - The different steps should build incrementally: a small .NET
assembly change shouldn't "rebuild the world". For example, we
would not regenerate
AndroidManifest.xml
if a netstandard assembly changed, and we would only generate java stubs forMonoAndroid
assemblies that have changed. - Java.Interop could generate Java stubs without using Mono.Cecil at all. It should be able to reuse the XML files (and object model) generated by the linker.
Various bits of our build system produce .java
files which are then
compiled with javac
, then "re-compiled" into .dex
files. (Such
parts include <GenerateJavaStubs/>
and
<GeneratePackageManagerJava/>
, among others.)
It should be possible to directly emit .dex
files within some
contexts. (Not easy, mind, but possible.) This would avoid the
overhead of invoking javac
and dx
.
@(AndroidJavaSource)
will still require the javac
and dx
invocations, and the
@(AndroidJavaLibrary)
/@(EmbeddedJar)
/@(EmbeddedReferenceJar)
build actions will still require dx
invocations.
NOTE: this is likely not a viable option. The Android designer needs
.class
files, it does not operate against Android-specific .dex
files.
- Are there any MSBuild tasks that can run in the background? So other tasks can run in parallel while the work is done?
- One to mention is
GetPrimaryCpuAbi
, which is in the proprietary source of Xamarin.Android.
MSBuild and Roslyn support generating reference assemblies by setting the $(ProduceReferenceAssembly)
MSBuild property to True.
Why do we care? What's this mean?
Assume you have a solution with two projects. Project Referenced.csproj
has no further references. Project Referencer.csproj
has a @(ProjectReference)
to Referenced.csproj
.
The developer makes a change to something within Referenced.csproj
.
Question: Does Referencer.csproj
need to be rebuilt?
In the "original" MSBuild world -- the world that Xamarin.Android still lives in -- the answer is yes, Referencer.csproj
must always be rebuilt, because the change to Referenced.csproj
may contain an API breaking change which would prevent Referencer.csproj
from building.
In the new $(ProduceReferenceAssembly)=True
world order, the answer is instead maybe: Referencer.csproj
only needs to be built if the reference assembly produced as part of the Referenced.csproj
build is updated, which in turn only happens when the public API changes. Meaning if a change doesn't alter the public API -- adding comments, fixing a method implementation, adding private
/internal
members, etc. -- then Referencer.csproj
need not be rebuilt at all.
In more concrete terms, assume you have a Xamarin.Forms solution containing a Xamarin.Forms PCL project and a referencing Android App project. Currently, whenever the PCL project is changed, the App project must always be rebuilt. In a $(ProduceReferenceAssembly)=True
order, the App project would need to be rebuilt less often.
So let's just export $(ProduceReferenceAssembly)=True
! What's holding us back?
The problem is that our current build model is that a .csproj
has only one output: the assembly. The assembly contains everything useful: native libraries (embedded .zip
resource), Android Resources (embedded .zip
resource), environment files, etc. This means that a reference assembly would be unusable: everything requires that the assembly be a full assembly, not some stubbed out reference assembly. Updates to Android Resources would thus be ignored, etc.
Thus, to support $(ProduceReferenceAssembly)=True
, we need to change our build system's view of .csproj
files. They can no longer "just" produce a .dll
. Instead, they need to produce lots of things: native libraries, Android resources, etc.
Then we can update our build system to use reference assemblies for compilation, and go "around" the reference assemblies to pull in referenced assets, as needed.
@jonathanpeppers found out that the following scenario wasn't working as expected:
- Create a Xamarin.Forms project + NetStandard
- Add
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
to the NetStandard library - Build
- Modify XAML, Build Again
It turns out that the reference assembly contains EmbeddedResource
inside it! So modifying XAML causes the Xamarin.Android head to rebuild! This prevents the feature from helping us at all...
Issue is being fixed in the next Dev16 release: https://github.com/dotnet/roslyn/issues/31197
One of the idea we have had is to kick off some long running tasks in the background while the build is running.
Examples might be
- Fast Deployment of items to device
- Patching of Resources.
- Parallelise Building of the Base apk and compilation of Java code.
The problem is MSBuild does not really support building Tasks or Targets in Parallel only projects.
So we need a way to kick off a background task and wait for it to complete later in the build
process. The idea is to use GetRegisteredTaskObject
to register a global TaskManager
which
can be used to register background tasks with. We can then use the AsyncTask
in conjunction
with Task.WhenAll
to wait later in the build for those tasks to complete.
public class TaskManager : IDisposable {
SynchronizedCollection<TPL.Task> tasks = new SynchronizedCollection<TPL.Task> ();
CancellationTokenSource tcs = new CancellationTokenSource ();
public void RegisterTask (TPL.Task task)
{
lock (tasks.SyncRoot)
tasks.Add (task);
}
public TPL.Task [] Tasks {
get {
return tasks.ToArray ();
}
}
public void Dispose ()
{
tcs.Cancel ();
}
public CancellationToken Token { get { return tcs.Token; } }
public int Count => tasks.Count;
}
We can then use code like this within a MSBuild Task
to run a background task and
instantly return back to MSBuild.
var manager = (TaskManager)BuildEngine4.GetRegisteredTaskObject ("TaskManager", RegisteredTaskObjectLifetime.Build);
if (manager == null) {
manager = new TaskManager ();
BuildEngine4.RegisterTaskObject ("TaskManager", manager, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false);
}
var task = TPL.Task.Run (async () => {
await TPL.Task.Delay (10000);
});
manager.RegisterTask (task);
The in the Execute
method of an AsyncTask
derived task we can do something like
manager = (TaskManager)BuildEngine4.GetRegisteredTaskObject ("TaskManager", RegisteredTaskObjectLifetime.Build);
TPL.Task task;
if (manager == null || manager.Count == 0) {
task = TPL.Task.CompletedTask;
} else {
task = TPL.Task.WhenAll (manager.Tasks);
}
task.ContinueWith(Complete);
base.Execute ();
This can either be in a specific task, say WhenAll
or bolted into an existing task like InstallPackagedAssemblies
.
One this we need to figure out is how to deal with Logging. Since its a background task we
cannot access the normal MSBuild Log.LogXXXX
methods. So we will need some way to collect
all the logging from the task and then emit the messages, warnings and errors when the
task completes.
One of the other problems we thought of was "What if the user cancels the build when a Task is not running". Or if the build is NOT in the WaitAll task.. The solution there is to implement IDisposable
on the TaskManager
. Because we are registering it as part of RegisteredTaskObjectLifetime.Build
, it should be disposed of if the user cancels or when the build completes. If the build completes then we should have got through the Wait
Task already so all the registered tasks will be complete. On Cancellation, the CancellationTokenSource
on the TaskManager
should be Canceled.
Currently the fast dev .__override__
directory can only be used by our build system. Should we look at
expanding it to allow users to fast deploy custom files to the fast deployment directory. Files such as test sqlite databases, game textures and models or Json files. This could be handled via the @(Content)
item group or via a new @(FastDevUserFiles)
item group.
We could then perhaps provide support for loading such files by overriding the AppContext.BaseDirectory
to point at the .__override__
directory so users can use a normal File.Open
.
Currently the ManagedResourceParser
has two modes. First as the quickest is it will
parse the R.txt
from the $(IntermediateOutputPath)
which aapt(2)
produces.
The second mode is to open and parser ALL the resources in both the app and any
support/referenced libraries.
One of the things that happens is if the user updates a layout, we can not use
the R.txt
file as it could be out of date. So we need to run the second mode.
But do we really need to parse the ALL the resources again? No!!!
In order to do this we need to know which id's came from which files. Once we have that information we can figure out which layout file changed and then make sure those id's are all correct (add/remove etc).
Should we move the linker to a separate process?
On Windows, dotnet build
, etc. long-lived processes are used to improve build performance. MSBuild.exe
and VBCSCompiler.exe
(Roslyn) stay running, so their startup time is avoided during incremental builds. Can we use this idea in Xamarin.Android?
aapt2
has a new "daemon mode" feature in the latest 2.19 release.
In order to use this for aapt2
we would need to:
Download the latest bits foraapt2
from Maven, and ship them (their license should be OK). The Android SDK ships outdated versions ofaapt2
, and customers can use different versions of the Android SDK anyway...- Add daemon-mode support for
<Aapt2Compile/>
and<Aapt2Link/>
MSBuild tasks PR 4190
aapt2 compile
now has a --zip
argument. This takes a zip file which can then be used to
build the flata
archive. We should investigate if we can pass .aar
files to this directly
AND also rework our internal AndroidManagedArchive.zip file thingy to contain a folder structure
which is compatible. That way we might be able to save some ms
by not unpacking this data to
disk.
Can we keep a Java daemon running all the time?
We call java.exe
to run various jar files:
apksigner.jar
desugar_deploy.jar
-
dx.jar
(orr8.jar
) proguard.jar
Can we use a daemon to reduce JVM startup time? gradle does this. Can we use an existing tool to do this?
@jonathanpeppers did an experiment using Facebook's nailgun:
https://github.com/facebook/nailgun
It is a Java daemon used by Facebook's Buck build system.
A comparison of running d8.jar
through a running nailgun server:
- 1st (cold): ~12 seconds
- 2nd: ~7 seconds
- 3rd: ~5 seconds
Beyond the 3rd run, it was stable at ~5 seconds.
Unfortunately nailgun opens a TCP port on localhost to function. We may need to avoid doing this and implement something ourselves instead.
- APK Tests on the Hyper V Emulator
- Design Time Build System
- Profile MSBuild Tasks
- Diagnose Fast Deployment Issues
- Preview layout XML files with Android Studio
- Documentation