Skip to content

Commit

Permalink
[runtime] Optionally preload all the assemblies from the apk
Browse files Browse the repository at this point in the history
[Startup performance][0] commit dispensed with preloading of all assemblies from
the APK since it's no longer needed by Xamarin.Android, thus improving the
overall startup time. However, it turns out that some applications (especially
those using Xamarin.Forms, but not only) rely on the fact that all the
assemblies are loaded by the time the application code is called. They do it by
calling `AppDomain.CurrentDomain.GetAssemblies()` and iterating over the
returned array in order to load resources, inject dependencies etc. This is not
a correct way to do it and nobody should rely on the `GetAssemblies()` call
returning all the assemblies available, but since this is currently required by
the aforementioned applications, we need to hurt performance and restore the
preload behavior albeit this time making it optional.

This commit implements the preload in a similar way as before, however this time
it is controlled by MSBuild property `$(AndroidEnablePreloadAssemblies)`
(which currently defaults to `true`) which the developer can set in their
project file and thus control the runtime behavior.

The commit also updates (courtesy of Jon Peppers @jonathanpeppers) the
Xamarin.Forms performance integration app in Xamarin.Android tests to fail in
case the assemblies are *not* preloaded. Currently, this application yields the
following performance hit on Pixel 3 XL with the preload enabled:

    02-11 21:19:58.647 27410 27410 I monodroid-timing: Assembly load: FormsViewGroup.dll preloaded; elapsed: 0s:1::267136
    02-11 21:19:58.648 27410 27410 I monodroid-timing: Assembly load: Newtonsoft.Json.dll preloaded; elapsed: 0s:0::999584
    02-11 21:19:58.649 27410 27410 I monodroid-timing: Assembly load: Plugin.Connectivity.Abstractions.dll preloaded; elapsed: 0s:0::916459
    02-11 21:19:58.650 27410 27410 I monodroid-timing: Assembly load: Plugin.Connectivity.dll preloaded; elapsed: 0s:0::886146
    02-11 21:19:58.651 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Arch.Core.Common.dll preloaded; elapsed: 0s:0::890208
    02-11 21:19:58.652 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Arch.Lifecycle.Common.dll preloaded; elapsed: 0s:0::918229
    02-11 21:19:58.653 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Arch.Lifecycle.Runtime.dll preloaded; elapsed: 0s:0::880833
    02-11 21:19:58.653 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Animated.Vector.Drawable.dll preloaded; elapsed: 0s:0::894323
    02-11 21:19:58.654 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Annotations.dll preloaded; elapsed: 0s:0::928177
    02-11 21:19:58.655 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Compat.dll preloaded; elapsed: 0s:0::941719
    02-11 21:19:58.656 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Core.UI.dll preloaded; elapsed: 0s:0::936563
    02-11 21:19:58.657 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Core.Utils.dll preloaded; elapsed: 0s:0::938594
    02-11 21:19:58.658 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Design.dll preloaded; elapsed: 0s:0::983334
    02-11 21:19:58.659 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Fragment.dll preloaded; elapsed: 0s:0::946042
    02-11 21:19:58.660 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Media.Compat.dll preloaded; elapsed: 0s:0::927604
    02-11 21:19:58.661 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Transition.dll preloaded; elapsed: 0s:0::959376
    02-11 21:19:58.662 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v4.dll preloaded; elapsed: 0s:0::923750
    02-11 21:19:58.663 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v7.AppCompat.dll preloaded; elapsed: 0s:1::82500
    02-11 21:19:58.664 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v7.CardView.dll preloaded; elapsed: 0s:0::920313
    02-11 21:19:58.665 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v7.MediaRouter.dll preloaded; elapsed: 0s:0::949323
    02-11 21:19:58.666 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v7.Palette.dll preloaded; elapsed: 0s:0::906093
    02-11 21:19:58.667 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.v7.RecyclerView.dll preloaded; elapsed: 0s:0::900782
    02-11 21:19:58.668 27410 27410 I monodroid-timing: Assembly load: Xamarin.Android.Support.Vector.Drawable.dll preloaded; elapsed: 0s:0::960416
    02-11 21:19:58.669 27410 27410 I monodroid-timing: Assembly load: Xamarin.Forms.Core.dll preloaded; elapsed: 0s:0::972396
    02-11 21:19:58.670 27410 27410 I monodroid-timing: Assembly load: Xamarin.Forms.Performance.Integration.dll preloaded; elapsed: 0s:0::898646
    02-11 21:19:58.671 27410 27410 I monodroid-timing: Assembly load: Xamarin.Forms.Platform.Android.dll preloaded; elapsed: 0s:0::932813
    02-11 21:19:58.672 27410 27410 I monodroid-timing: Assembly load: Xamarin.Forms.Platform.dll preloaded; elapsed: 0s:0::935677
    02-11 21:19:58.673 27410 27410 I monodroid-timing: Assembly load: Xamarin.Forms.Xaml.dll preloaded; elapsed: 0s:0::928594
    02-11 21:19:58.673 27410 27410 I monodroid-timing: Assembly load: preloaded 29 assemblies; elapsed: 0s:26::783805

[0]: b90d3ab
  • Loading branch information
grendello committed Feb 12, 2019
1 parent 650b409 commit e230162
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 54 deletions.
12 changes: 12 additions & 0 deletions Documentation/guides/BuildProcess.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,18 @@ when packaing Release applications.

Added in Xamarin.Android 8.3.

- **AndroidEnablePreloadAssemblies** – A boolean property
which changes runtime behavior with relation to the managed assemblies
stored in the application package. When set to `True` (the default) it will
cause the Xamarin.Android runtime to load **all** the assemblies into the
application at the startup, before application code is invoked. This is
required by some Xamarin.Forms applications as well as by various dependency
injection libraries: they depend on `AppDomain.CurrentDomain.GetAssemblies()`
to return the full set of managed assemblies in order to find dependencies,
resources etc. Preloading all the assemblies carries a startup performance
penalty so, whenever possible, developers are encouraged to set this property to
`False` in their project file.
### Binding Project Build Properties
The following MSBuild properties are used with
Expand Down
20 changes: 19 additions & 1 deletion src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<_AndroidDesignTimeBuildPropertiesCache>$(_AndroidIntermediateDesignTimeBuildDirectory)build.props</_AndroidDesignTimeBuildPropertiesCache>
<AndroidGenerateJniMarshalMethods Condition=" '$(AndroidGenerateJniMarshalMethods)' == '' ">False</AndroidGenerateJniMarshalMethods>
<AndroidMakeBundleKeepTemporaryFiles Condition=" '$(AndroidMakeBundleKeepTemporaryFiles)' == '' ">False</AndroidMakeBundleKeepTemporaryFiles>

<!-- If true it will cause all the assemblies in the apk to be preloaded on startup time -->
<AndroidEnablePreloadAssemblies Condition=" '$(AndroidEnablePreloadAssemblies)' == '' ">True</AndroidEnablePreloadAssemblies>
</PropertyGroup>

<Choose>
Expand Down Expand Up @@ -2386,8 +2389,23 @@ because xbuild doesn't support framework reference assemblies.
</GetAddOnPlatformLibraries>
</Target>

<Target Name="_SetupAssemblyPreload"
Condition=" '$(AndroidEnablePreloadAssemblies)' != 'True' "
Inputs="$(IntermediateOutputPath)javastubs.cache"
Outputs="$(IntermediateOutputPath)preloadAssembliesEnvironment.txt">
<WriteLinesToFile
File="$(IntermediateOutputPath)preloadAssembliesEnvironment.txt"
Lines="mono.enable_assembly_preload=0"
Overwrite="True"
/>
<ItemGroup>
<AndroidEnvironment Include="$(IntermediateOutputPath)preloadAssembliesEnvironment.txt" />
<FileWrites Include="$(IntermediateOutputPath)preloadAssembliesEnvironment.txt" />
</ItemGroup>
</Target>

<Target Name="_GeneratePackageManagerJava"
DependsOnTargets="_GetAddOnPlatformLibraries;_AddStaticResources;$(_AfterAddStaticResources);_PrepareAssemblies"
DependsOnTargets="_GetAddOnPlatformLibraries;_AddStaticResources;$(_AfterAddStaticResources);_PrepareAssemblies;_SetupAssemblyPreload"
Inputs="$(MSBuildAllProjects);$(_ResolvedUserAssembliesHashFile);$(MSBuildProjectFile);$(_AndroidBuildPropertiesCache)"
Outputs="$(_AndroidStampDirectory)_GeneratePackageManagerJava.stamp">
<!-- Create java needed for Mono runtime -->
Expand Down
10 changes: 10 additions & 0 deletions src/monodroid/jni/android-system.cc
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,16 @@ AndroidSystem::setup_environment (jstring_wrapper& name, jstring_wrapper& value)
knownEnvVars.MonoLLVM = true;
return;
}

if (strcmp (k, "mono.enable_assembly_preload") == 0) {
if (*v == '\0')
knownEnvVars.EnableAssemblyPreload = KnownEnvironmentVariables::AssemblyPreloadDefault;
else if (v[0] == '1')
knownEnvVars.EnableAssemblyPreload = true;
else
knownEnvVars.EnableAssemblyPreload = false;
return;
}
}

add_system_property (k, v);
Expand Down
7 changes: 7 additions & 0 deletions src/monodroid/jni/android-system.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ namespace xamarin { namespace android { namespace internal

struct KnownEnvironmentVariables
{
static constexpr bool AssemblyPreloadDefault = true;

bool DSOInApk = false;
MonoAotMode MonoAOT = MonoAotMode::MONO_AOT_MODE_NONE;
bool MonoLLVM = false;
bool EnableAssemblyPreload = AssemblyPreloadDefault;
};

class AndroidSystem
Expand Down Expand Up @@ -133,6 +136,10 @@ namespace xamarin { namespace android { namespace internal
#if defined (WINDOWS)
int setenv (const char *name, const char *value, int overwrite);
#endif
bool is_assembly_preload_enabled () const
{
return knownEnvVars.EnableAssemblyPreload;
}

bool is_mono_llvm_enabled () const
{
Expand Down
66 changes: 61 additions & 5 deletions src/monodroid/jni/monodroid-glue.cc
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ mono_runtime_init (char *runtime_args)
}

static MonoDomain*
create_domain (JNIEnv *env, jclass runtimeClass, jstring_array_wrapper &runtimeApks, jstring assembly, jobject loader, bool is_root_domain)
create_domain (JNIEnv *env, jclass runtimeClass, jstring_array_wrapper &runtimeApks, jobject loader, bool is_root_domain)
{
MonoDomain *domain;
int user_assemblies_count = 0;;
Expand Down Expand Up @@ -1779,21 +1779,75 @@ _monodroid_counters_dump (const char *format, ...)
monoFunctions.counters_dump (XA_LOG_COUNTERS, counters);
}

static void
load_assembly (MonoDomain *domain, JNIEnv *env, jstring_wrapper &assembly)
{
timing_period total_time;
if (XA_UNLIKELY (utils.should_log (LOG_TIMING)))
total_time.mark_start ();

const char *assm_name = assembly.get_cstr ();
MonoAssemblyName *aname;

aname = monoFunctions.assembly_name_new (assm_name);

if (domain != monoFunctions.domain_get ()) {
MonoDomain *current = monoFunctions.domain_get ();
monoFunctions.domain_set (domain, FALSE);
monoFunctions.assembly_load_full (aname, NULL, NULL, 0);
monoFunctions.domain_set (current, FALSE);
} else {
monoFunctions.assembly_load_full (aname, NULL, NULL, 0);
}

monoFunctions.assembly_name_free (aname);

if (XA_UNLIKELY (utils.should_log (LOG_TIMING))) {
total_time.mark_end ();

timing_diff diff (total_time);
log_info (LOG_TIMING, "Assembly load: %s preloaded; elapsed: %lis:%lu::%lu", assm_name, diff.sec, diff.ms, diff.ns);
}
}

static void
load_assemblies (MonoDomain *domain, JNIEnv *env, jstring_array_wrapper &assemblies)
{
timing_period total_time;
if (XA_UNLIKELY (utils.should_log (LOG_TIMING)))
total_time.mark_start ();

/* skip element 0, as that's loaded in create_domain() */
for (size_t i = 1; i < assemblies.get_length (); ++i) {
jstring_wrapper &assembly = assemblies [i];
load_assembly (domain, env, assembly);
}

if (XA_UNLIKELY (utils.should_log (LOG_TIMING))) {
total_time.mark_end ();

timing_diff diff (total_time);
log_info (LOG_TIMING, "Finished loading assemblies: preloaded %u assemblies; wasted time: %lis:%lu::%lu", assemblies.get_length (), diff.sec, diff.ms, diff.ns);
}
}

static void
monodroid_Mono_UnhandledException_internal (MonoException *ex)
{
// Do nothing with it here, we let the exception naturally propagate on the managed side
}

static MonoDomain*
create_and_initialize_domain (JNIEnv* env, jclass runtimeClass, jstring_array_wrapper &runtimeApks, jobjectArray assemblies, jobject loader, bool is_root_domain)
create_and_initialize_domain (JNIEnv* env, jclass runtimeClass, jstring_array_wrapper &runtimeApks, jstring_array_wrapper &assemblies, jobject loader, bool is_root_domain)
{
MonoDomain* domain = create_domain (env, runtimeClass, runtimeApks, reinterpret_cast <jstring> (env->GetObjectArrayElement (assemblies, 0)), loader, is_root_domain);
MonoDomain* domain = create_domain (env, runtimeClass, runtimeApks, loader, is_root_domain);

// When running on desktop, the root domain is only a dummy so don't initialize it
if (is_running_on_desktop && is_root_domain)
return domain;

if (androidSystem.is_assembly_preload_enabled ())
load_assemblies (domain, env, assemblies);
init_android_runtime (domain, env, runtimeClass, loader);

osBridge.add_monodroid_domain (domain);
Expand All @@ -1804,7 +1858,7 @@ create_and_initialize_domain (JNIEnv* env, jclass runtimeClass, jstring_array_wr
JNIEXPORT void JNICALL
Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobjectArray runtimeApksJava,
jstring runtimeNativeLibDir, jobjectArray appDirs, jobject loader,
jobjectArray externalStorageDirs, jobjectArray assemblies, jstring packageName,
jobjectArray externalStorageDirs, jobjectArray assembliesJava, jstring packageName,
jint apiLevel, jobjectArray environmentVariables)
{
init_logging_categories ();
Expand Down Expand Up @@ -1987,6 +2041,7 @@ Java_mono_android_Runtime_init (JNIEnv *env, jclass klass, jstring lang, jobject
log_info_nocheck (LOG_TIMING, "Runtime.init: Mono runtime init; elapsed: %lis:%lu::%lu", diff.sec, diff.ms, diff.ns);
}

jstring_array_wrapper assemblies (env, assembliesJava);
/* the first assembly is used to initialize the AppDomain name */
create_and_initialize_domain (env, klass, runtimeApks, assemblies, loader, /*is_root_domain:*/ true);

Expand Down Expand Up @@ -2070,14 +2125,15 @@ reinitialize_android_runtime_type_manager (JNIEnv *env)
}

JNIEXPORT jint
JNICALL Java_mono_android_Runtime_createNewContext (JNIEnv *env, jclass klass, jobjectArray runtimeApksJava, jobjectArray assemblies, jobject loader)
JNICALL Java_mono_android_Runtime_createNewContext (JNIEnv *env, jclass klass, jobjectArray runtimeApksJava, jobjectArray assembliesJava, jobject loader)
{
log_info (LOG_DEFAULT, "CREATING NEW CONTEXT");
reinitialize_android_runtime_type_manager (env);
MonoDomain *root_domain = monoFunctions.get_root_domain ();
monoFunctions.jit_thread_attach (root_domain);

jstring_array_wrapper runtimeApks (env, runtimeApksJava);
jstring_array_wrapper assemblies (env, assembliesJava);
MonoDomain *domain = create_and_initialize_domain (env, klass, runtimeApks, assemblies, loader, /*is_root_domain:*/ false);
monoFunctions.domain_set (domain, FALSE);
int domain_id = monoFunctions.domain_get_id (domain);
Expand Down
2 changes: 2 additions & 0 deletions tests/Xamarin.Forms-Performance-Integration/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

#if !DEBUG
[assembly: XamlCompilation (XamlCompilationOptions.Compile)]
#endif

namespace Xamarin.Forms.Performance.Integration
{
Expand Down
5 changes: 5 additions & 0 deletions tests/Xamarin.Forms-Performance-Integration/Global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.featureHeader {
font-size: 20;
font-style: bold;
margin: 0,20,0,10;
}
48 changes: 0 additions & 48 deletions tests/Xamarin.Forms-Performance-Integration/Views/MainPage.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Xamarin.Forms.Performance.Integration.MainPage"
Resources="{StyleSheet Source=../Global.css}">
</TabbedPage>
50 changes: 50 additions & 0 deletions tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;

using Xamarin.Forms;

namespace Xamarin.Forms.Performance.Integration
{
public partial class MainPage : TabbedPage
{
public MainPage ()
{
InitializeComponent ();

Page itemsPage, aboutPage = null;

switch (Device.RuntimePlatform) {
case Device.iOS:
itemsPage = new NavigationPage (new ItemsPage ()) {
Title = "Browse"
};

aboutPage = new NavigationPage (new AboutPage ()) {
Title = "About"
};
itemsPage.Icon = "tab_feed.png";
aboutPage.Icon = "tab_about.png";
break;
default:
itemsPage = new ItemsPage () {
Title = "Browse"
};

aboutPage = new AboutPage () {
Title = "About"
};
break;
}

Children.Add (itemsPage);
Children.Add (aboutPage);

Title = Children [0].Title;
}

protected override void OnCurrentPageChanged ()
{
base.OnCurrentPageChanged ();
Title = CurrentPage?.Title ?? string.Empty;
}
}
}

0 comments on commit e230162

Please sign in to comment.