From e23016268ca5236f369d65fcda43779fbdd58e31 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Mon, 11 Feb 2019 21:21:13 +0100 Subject: [PATCH] [runtime] Optionally preload all the assemblies from the apk [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]: https://github.com/xamarin/xamarin-android/commit/b90d3ab717cbfd38b2e6a307b4387d2d6cf04af8 --- Documentation/guides/BuildProcess.md | 12 ++++ .../Xamarin.Android.Common.targets | 20 +++++- src/monodroid/jni/android-system.cc | 10 +++ src/monodroid/jni/android-system.h | 7 ++ src/monodroid/jni/monodroid-glue.cc | 66 +++++++++++++++++-- .../App.xaml.cs | 2 + .../Global.css | 5 ++ .../Views/MainPage.cs | 48 -------------- .../Views/MainPage.xaml | 7 ++ .../Views/MainPage.xaml.cs | 50 ++++++++++++++ 10 files changed, 173 insertions(+), 54 deletions(-) create mode 100644 tests/Xamarin.Forms-Performance-Integration/Global.css delete mode 100644 tests/Xamarin.Forms-Performance-Integration/Views/MainPage.cs create mode 100644 tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml create mode 100644 tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml.cs diff --git a/Documentation/guides/BuildProcess.md b/Documentation/guides/BuildProcess.md index f9c31f33cc2..ba82e8c81c0 100644 --- a/Documentation/guides/BuildProcess.md +++ b/Documentation/guides/BuildProcess.md @@ -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 diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 317d546f4c8..868617d5388 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -317,6 +317,9 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidDesignTimeBuildPropertiesCache>$(_AndroidIntermediateDesignTimeBuildDirectory)build.props False False + + + True @@ -2386,8 +2389,23 @@ because xbuild doesn't support framework reference assemblies. + + + + + + + + diff --git a/src/monodroid/jni/android-system.cc b/src/monodroid/jni/android-system.cc index 13e0689856f..112d727bec5 100644 --- a/src/monodroid/jni/android-system.cc +++ b/src/monodroid/jni/android-system.cc @@ -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); diff --git a/src/monodroid/jni/android-system.h b/src/monodroid/jni/android-system.h index d83607140b2..60292395cce 100644 --- a/src/monodroid/jni/android-system.h +++ b/src/monodroid/jni/android-system.h @@ -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 @@ -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 { diff --git a/src/monodroid/jni/monodroid-glue.cc b/src/monodroid/jni/monodroid-glue.cc index 719c783de51..d3fd1e3bc39 100644 --- a/src/monodroid/jni/monodroid-glue.cc +++ b/src/monodroid/jni/monodroid-glue.cc @@ -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;; @@ -1779,6 +1779,58 @@ _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) { @@ -1786,14 +1838,16 @@ monodroid_Mono_UnhandledException_internal (MonoException *ex) } 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 (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); @@ -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 (); @@ -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); @@ -2070,7 +2125,7 @@ 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); @@ -2078,6 +2133,7 @@ JNICALL Java_mono_android_Runtime_createNewContext (JNIEnv *env, jclass klass, j 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); diff --git a/tests/Xamarin.Forms-Performance-Integration/App.xaml.cs b/tests/Xamarin.Forms-Performance-Integration/App.xaml.cs index 6a821006fef..09d63f63160 100644 --- a/tests/Xamarin.Forms-Performance-Integration/App.xaml.cs +++ b/tests/Xamarin.Forms-Performance-Integration/App.xaml.cs @@ -2,7 +2,9 @@ using Xamarin.Forms; using Xamarin.Forms.Xaml; +#if !DEBUG [assembly: XamlCompilation (XamlCompilationOptions.Compile)] +#endif namespace Xamarin.Forms.Performance.Integration { diff --git a/tests/Xamarin.Forms-Performance-Integration/Global.css b/tests/Xamarin.Forms-Performance-Integration/Global.css new file mode 100644 index 00000000000..e25f536375d --- /dev/null +++ b/tests/Xamarin.Forms-Performance-Integration/Global.css @@ -0,0 +1,5 @@ +.featureHeader { + font-size: 20; + font-style: bold; + margin: 0,20,0,10; +} \ No newline at end of file diff --git a/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.cs b/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.cs deleted file mode 100644 index 42242fc1e59..00000000000 --- a/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -using Xamarin.Forms; - -namespace Xamarin.Forms.Performance.Integration -{ - public class MainPage : TabbedPage - { - public MainPage () - { - 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; - } - } -} diff --git a/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml b/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml new file mode 100644 index 00000000000..00ebf6b73ef --- /dev/null +++ b/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml.cs b/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml.cs new file mode 100644 index 00000000000..0c94b746b0f --- /dev/null +++ b/tests/Xamarin.Forms-Performance-Integration/Views/MainPage.xaml.cs @@ -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; + } + } +} \ No newline at end of file