Skip to content

Commit

Permalink
Runtime startup performance improvements
Browse files Browse the repository at this point in the history
The goal of this commit is to make Xamarin.Android apps start faster. The
changes are focused only around the very first stage of application startup -
between Android invoking our Java startup code (MonoPackageManager) and the end
of our `Runtime.init` (`Java_mono_android_Runtime_init` in the native runtime)
which is when the user application is fully initialized and ready to start is
launcher Activity.

In order to achieve the goal, the following changes were made:

 * Java class lookup ("reflection").
   We used to call the `FindClass` JNI function as part of the startup at a cost
   of several milliseconds. We now put the class handles (accessed with the
   `.class` Java accessor) in the `Runtime` class and initialize them from the
   static constructor. We then read those fields from within `Runtime.init`,
   which is passed a reference to the Java instance of the Runtime class.
   Additonally, a handful of class field/method lookups were moved out of the
   init code so that the code that doesn't use them doesn't have to pay the tax.

 * Android API level is passed to `Runtime.init` from Java instead of using JNI
   from the native code.

 * Limit logging.
   Previously whenever any of the `log_{info,debug}` functions were called we'd
   spend time preparing all the parameters to pass to the function, sometimes
   involving memory allocation, function calls, etc - only to discard all of
   that work **inside** the `log_*` call because the logging category used in
   that call was disabled. Now we check whether the category is enabled before
   we set out to construct the parameters.

 * Java/JNI type wrappers for string and array of strings.
   This both a convenience/correctness as well as a performance change.
   Introduced are two C++ wrapper classes for the `jstring` and `object array`
   types (specialized for object == jstring) which take care of efficiently
   caching the retrieved strings as well as of correctly deleting local
   references to the obtained objects. Both classes, `jstring_wrapper` and
   `jstring_array_wrapper` are optimized so that they compile into the
   equivalent of the current, hand-written, code. They also take care to make
   the minimum necessary number of calls in order to access the strings, both
   standalone and from arrays, as well as to release the resources.
   The string and array wrappers are passed around as references, thus using the
   minimum amount of memory.

 * Do not preload managed assemblies.
   We used to preload all of the application assemblies in order to find and
   invoke type initializers. This resulted in the list of assemblies being
   processed twice at the great expense of time. We now don't call the type
   initializers at all and the assemblies are loaded on demand.

 * Do not store application environment variables in a file inside the apk.
   The textual file used to be read from the apk(s) early in the process,
   requiring iteration over all the application apk files, opening each of them,
   browsing through the ZIP entries and, finally, reading the file line by line,
   parsing into the name and value parts to create either a
   property (`mono.aot`, `mono.llvm`) or any environment variables requested by
   the application developer (or the XA runtime).
   To speed the process up, this commit replaces the text file with a Java class
   generated during application build which contains an array of `"name",
   "value"` pairs. The class is passed to `Java_mono_android_Runtime_init` and
   its elements are used to create the requested environment variables. A
   handful of variables is special-cased in that they are not placed in the
   environment but rather to set flags in the `AndroidSystem` class. The
   variables are `mono.aot`, `mono.llvm` and `__XA_DSO_IN_APK`. This allowed to
   remove calls to create (fake) system properties as well as `getenv` in the
   init native function.

 * Don't try load LLVM.so when it won't be there because we're not using llvm

 * Convert package name to hash using optimized code without calling snprintf

 * Desktop build is determined on compile time instead of dynamically

 * xamarin_getifaddrs are initialized on demand, not at the startup.

Startup time improvements for the XF integration test app (average, Pixel 3 XL):

  * Debug mode:
    Old: 1s 440ms
    New: 1s 100ms

  * Release mode:
    Old: 650ms
    New: 270ms
  • Loading branch information
grendello committed Dec 11, 2018
1 parent c7c755c commit 8e0de65
Show file tree
Hide file tree
Showing 25 changed files with 796 additions and 596 deletions.
41 changes: 20 additions & 21 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,16 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
{
Logger.Categories = (LogCategories) args->logCategories;

var __start = new DateTime ();
Stopwatch stopper = null;
long elapsed, totalElapsed = 0;
if (Logger.LogTiming) {
__start = DateTime.UtcNow;
Logger.Log (LogLevel.Info,
"monodroid-timing",
"JNIEnv.Initialize start: " + (__start - new DateTime (1970, 1, 1)).TotalMilliseconds);
Logger.Log (LogLevel.Info,
"monodroid-timing",
"JNIEnv.Initialize: Logger JIT/etc. time: " + (DateTime.UtcNow - new DateTime (1970, 1, 1)).TotalMilliseconds + " [elapsed: " + (DateTime.UtcNow - __start).TotalMilliseconds + " ms]");
stopper = new Stopwatch ();
stopper.Start ();
Logger.Log (LogLevel.Info, "monodroid-timing", "JNIEnv.Initialize start");
elapsed = stopper.ElapsedMilliseconds;
totalElapsed += elapsed;
Logger.Log (LogLevel.Info, "monodroid-timing", $"JNIEnv.Initialize: Logger JIT/etc. time: elapsed {elapsed} ms]");
stopper.Restart ();
}

gref_gc_threshold = args->grefGcThreshold;
Expand Down Expand Up @@ -195,16 +196,14 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
#endif // JAVA_INTEROP

if (Logger.LogTiming) {
var __end = DateTime.UtcNow;
Logger.Log (LogLevel.Info,
"monodroid-timing",
"JNIEnv.Initialize: time: " + (__end - new DateTime (1970, 1, 1)).TotalMilliseconds + " [elapsed: " + (__end - __start).TotalMilliseconds + " ms]");
__start = DateTime.UtcNow;
elapsed = stopper.ElapsedMilliseconds;
totalElapsed += elapsed;
Logger.Log (LogLevel.Info, "monodroid-timing", $"JNIEnv.Initialize: managed runtime init time: elapsed {elapsed} ms]");
stopper.Restart ();
var _ = Java.Interop.TypeManager.jniToManaged;
__end = DateTime.UtcNow;
Logger.Log (LogLevel.Info,
"monodroid-timing",
"JNIEnv.Initialize: TypeManager init time: " + (__end - new DateTime (1970, 1, 1)).TotalMilliseconds + " [elapsed: " + (__end - __start).TotalMilliseconds + " ms]");
elapsed = stopper.ElapsedMilliseconds;
totalElapsed += elapsed;
Logger.Log (LogLevel.Info, "monodroid-timing", $"JNIEnv.Initialize: TypeManager init time: elapsed {elapsed} ms]");
}

AllocObjectSupported = androidSdkVersion > 10;
Expand Down Expand Up @@ -238,10 +237,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
Java.Lang.Thread.DefaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
}

if (Logger.LogTiming)
Logger.Log (LogLevel.Info,
"monodroid-timing",
"JNIEnv.Initialize end: " + (DateTime.UtcNow - new DateTime (1970, 1, 1)).TotalMilliseconds);
if (Logger.LogTiming) {
totalElapsed += stopper.ElapsedMilliseconds;
Logger.Log (LogLevel.Info, "monodroid-timing", $"JNIEnv.Initialize end: elapsed {totalElapsed} ms");
}
}

internal static void Exit ()
Expand Down
15 changes: 14 additions & 1 deletion src/Mono.Android/java/mono/android/Runtime.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package mono.android;

public class Runtime {
static java.lang.Class java_lang_Class;
static java.lang.Class java_lang_System;
static java.lang.Class java_util_TimeZone;
static java.lang.Class mono_android_IGCUserPeer;
static java.lang.Class mono_android_GCUserPeer;

static {
java_lang_Class = java.lang.Class.class;
java_lang_System = java.lang.System.class;
java_util_TimeZone = java.util.TimeZone.class;
mono_android_IGCUserPeer = mono.android.IGCUserPeer.class;
mono_android_GCUserPeer = mono.android.GCUserPeer.class;
}

private Runtime ()
{
}

public static native void init (String lang, String[] runtimeApks, String runtimeDataDir, String[] appDirs, ClassLoader loader, String[] externalStorageDirs, String[] assemblies, String packageName);
public static native void init (String lang, String[] runtimeApks, String runtimeDataDir, String[] appDirs, ClassLoader loader, String[] externalStorageDirs, String[] assemblies, String packageName, int apiLevel, String[] environmentVariables);
public static native void register (String managedType, java.lang.Class nativeClass, String methods);
public static native void notifyTimeZoneChanged ();
public static native int createNewContext (String[] runtimeApks, String[] assemblies, ClassLoader loader);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public static void LoadApplication (Context context, ApplicationInfo runtimePack
externalLegacyDir
},
MonoPackageManager_Resources.Assemblies,
context.getPackageName ());
context.getPackageName (),
android.os.Build.VERSION.SDK_INT,
mono.android.app.XamarinAndroidEnvironmentVariables.Variables);

mono.android.app.ApplicationRegistration.registerApplications ();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mono.android.app;

public class XamarinAndroidEnvironmentVariables
{
// Variables are specified the in "name", "value" pairs
public static final String[] Variables = new String[] {
//@ENVVARS@
};
}
101 changes: 0 additions & 101 deletions src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ public class BuildApk : Task

public ITaskItem[] BundleNativeLibraries { get; set; }

public ITaskItem[] Environments { get; set; }

public ITaskItem[] TypeMappings { get; set; }

[Required]
Expand Down Expand Up @@ -74,37 +72,24 @@ public class BuildApk : Task

public bool PreferNativeLibrariesWithDebugSymbols { get; set; }

public string AndroidAotMode { get; set; }

public string AndroidSequencePointsMode { get; set; }

public bool EnableLLVM { get; set; }

public bool EnableSGenConcurrent { get; set; }

public string AndroidEmbedProfilers { get; set; }
public string HttpClientHandlerType { get; set; }
public string TlsProvider { get; set; }
public string UncompressedFileExtensions { get; set; }


static readonly string MSBuildXamarinAndroidDirectory = Path.GetDirectoryName (typeof (BuildApk).Assembly.Location);

[Output]
public ITaskItem[] OutputFiles { get; set; }

[Output]
public string BuildId { get; set; }

bool _Debug {
get {
return string.Equals (Debug, "true", StringComparison.OrdinalIgnoreCase);
}
}

SequencePointsMode sequencePointsMode = SequencePointsMode.None;

Guid buildId = Guid.NewGuid ();

public ITaskItem[] LibraryProjectJars { get; set; }
string [] uncompressedFileExtensions;
Expand All @@ -125,7 +110,6 @@ void ExecuteWithAbi (string supportedAbis, string apkInputPath, string apkOutput
if (EmbedAssemblies && !BundleAssemblies)
AddAssemblies (apk);

AddEnvironment (apk);
AddRuntimeLibraries (apk, supportedAbis);
apk.Flush();
AddNativeLibraries (files, supportedAbis);
Expand Down Expand Up @@ -206,11 +190,9 @@ public override bool Execute ()
Log.LogDebugMessage (" Debug: {0}", Debug ?? "no");
Log.LogDebugMessage (" PreferNativeLibrariesWithDebugSymbols: {0}", PreferNativeLibrariesWithDebugSymbols);
Log.LogDebugMessage (" EmbedAssemblies: {0}", EmbedAssemblies);
Log.LogDebugMessage (" AndroidAotMode: {0}", AndroidAotMode);
Log.LogDebugMessage (" AndroidSequencePointsMode: {0}", AndroidSequencePointsMode);
Log.LogDebugMessage (" CreatePackagePerAbi: {0}", CreatePackagePerAbi);
Log.LogDebugMessage (" UncompressedFileExtensions: {0}", UncompressedFileExtensions);
Log.LogDebugTaskItems (" Environments:", Environments);
Log.LogDebugTaskItems (" ResolvedUserAssemblies:", ResolvedUserAssemblies);
Log.LogDebugTaskItems (" ResolvedFrameworkAssemblies:", ResolvedFrameworkAssemblies);
Log.LogDebugTaskItems (" NativeLibraries:", NativeLibraries);
Expand All @@ -220,8 +202,6 @@ public override bool Execute ()
Log.LogDebugTaskItems (" JavaLibraries:", JavaLibraries);
Log.LogDebugTaskItems (" LibraryProjectJars:", LibraryProjectJars);
Log.LogDebugTaskItems (" AdditionalNativeLibraryReferences:", AdditionalNativeLibraryReferences);
Log.LogDebugTaskItems (" HttpClientHandlerType:", HttpClientHandlerType);
Log.LogDebugMessage (" TlsProvider: {0}", TlsProvider);

Aot.TryGetSequencePointsMode (AndroidSequencePointsMode, out sequencePointsMode);

Expand All @@ -246,10 +226,6 @@ public override bool Execute ()
}
}

BuildId = buildId.ToString ();

Log.LogDebugMessage (" [Output] BuildId: {0}", BuildId);

OutputFiles = outputFiles.Select (a => new TaskItem (a)).ToArray ();

Log.LogDebugTaskItems (" [Output] OutputFiles :", OutputFiles);
Expand Down Expand Up @@ -349,83 +325,6 @@ static string GetTargetDirectory (string path)
return "assemblies";
}

void AddEnvironment (ZipArchiveEx apk)
{
var environment = new StringWriter () {
NewLine = "\n",
};

if (EnableLLVM) {
environment.WriteLine ("mono.llvm=true");
}

AotMode aotMode;
if (AndroidAotMode != null && Aot.GetAndroidAotMode(AndroidAotMode, out aotMode)) {
environment.WriteLine ("mono.aot={0}", aotMode.ToString().ToLowerInvariant());
}

const string defaultLogLevel = "MONO_LOG_LEVEL=info";
const string defaultMonoDebug = "MONO_DEBUG=gen-compact-seq-points";
const string defaultHttpMessageHandler = "XA_HTTP_CLIENT_HANDLER_TYPE=System.Net.Http.HttpClientHandler, System.Net.Http";
const string defaultTlsProvider = "XA_TLS_PROVIDER=btls";
string xamarinBuildId = string.Format ("XAMARIN_BUILD_ID={0}", buildId);

bool haveLogLevel = false;
bool haveMonoDebug = false;
bool havebuildId = false;
bool haveHttpMessageHandler = false;
bool haveTlsProvider = false;
bool haveMonoGCParams = false;

foreach (ITaskItem env in Environments ?? new TaskItem[0]) {
environment.WriteLine ("## Source File: {0}", env.ItemSpec);
foreach (string line in File.ReadLines (env.ItemSpec)) {
var lineToWrite = line;
if (lineToWrite.StartsWith ("MONO_LOG_LEVEL=", StringComparison.Ordinal))
haveLogLevel = true;
if (lineToWrite.StartsWith ("MONO_GC_PARAMS=", StringComparison.Ordinal))
haveMonoGCParams = true;
if (lineToWrite.StartsWith ("XAMARIN_BUILD_ID=", StringComparison.Ordinal))
havebuildId = true;
if (lineToWrite.StartsWith ("MONO_DEBUG=", StringComparison.Ordinal)) {
haveMonoDebug = true;
if (sequencePointsMode != SequencePointsMode.None && !lineToWrite.Contains ("gen-compact-seq-points"))
lineToWrite = line + ",gen-compact-seq-points";
}
if (lineToWrite.StartsWith ("XA_HTTP_CLIENT_HANDLER_TYPE=", StringComparison.Ordinal))
haveHttpMessageHandler = true;
if (lineToWrite.StartsWith ("XA_TLS_PROVIDER=", StringComparison.Ordinal))
haveTlsProvider = true;
environment.WriteLine (lineToWrite);
}
}

if (_Debug && !haveLogLevel) {
environment.WriteLine (defaultLogLevel);
}

if (sequencePointsMode != SequencePointsMode.None && !haveMonoDebug) {
environment.WriteLine (defaultMonoDebug);
}

if (!havebuildId)
environment.WriteLine (xamarinBuildId);

if (!haveHttpMessageHandler)
environment.WriteLine (HttpClientHandlerType == null ? defaultHttpMessageHandler : $"XA_HTTP_CLIENT_HANDLER_TYPE={HttpClientHandlerType.Trim ()}");
if (!haveTlsProvider)
environment.WriteLine (TlsProvider == null ? defaultTlsProvider : $"XA_TLS_PROVIDER={TlsProvider.Trim ()}");
if (!haveMonoGCParams) {
if (EnableSGenConcurrent)
environment.WriteLine ("MONO_GC_PARAMS=major=marksweep-conc");
else
environment.WriteLine ("MONO_GC_PARAMS=major=marksweep");
}

apk.Archive.AddEntry ("environment", environment.ToString (),
new UTF8Encoding (encoderShouldEmitUTF8Identifier:false));
}

class LibInfo
{
public string Path;
Expand Down
Loading

0 comments on commit 8e0de65

Please sign in to comment.