Skip to content

Commit c82f7d7

Browse files
committed
Startup performance improvements (less reflection)
Java.Interop uses the `JniAddNativeMethodRegistrationAttribute` custom attribute to determine whether to register native methods for a given type. It turns out that in most cases there isn't a single instance of this attribute throughout the application and all the calls to `GetCustomAttribute` end up returning nothing while incurring a large performance penalty on application startup. Eliminate the call after checking that no type in the application is decorated with the above custom attribute. Another observation made while profiling a couple of XA applications was that the installation of the uncaught exception handler with `Java.Lang.Thread` was quite expensive while, in most cases, completely not useful during startup (and, hopefully, during application lifetime). Instead of registering a managed type to handle the exceptions, register the handler in Java which then calls down to Xamarin.Android native runtime which, in turn, propagates the exception to the managed code. Gains in startup time are quite nice: Device name: **Pixel 3 XL** Device architecture: **arm64-v8a** Number of test runs: **10** Test application: **Xamarin.Forms integration test** | | **Native to managed** | **Runtime init** | **Displayed** | **Notes** | |-----------------|------------------------|------------------|---------------|--------------------------------| | **master** | 131.278 | 149.074 | 789.10 | preload enabled; 32-bit build | | **this commit** | 49.446 | 66.989 | 764.30 | | | **master** | 132.315 | 147.187 | 795.60 | preload disabled; 32-bit build | | **this commit** | 48.876 | 63.822 | 754.30 | | | **master** | 121.544 | 137.736 | 728.20 | preload enabled; 64-bit build | | **this commit** | 45.350 | 61.464 | 699.50 | | | **master** | 123.448 | 137.052 | 727.40 | preload disabled; 64-bit build | | **this commit** | 44.765 | 58.047 | 689.00 | | Device name: **Pixel 3 XL** Device architecture: **arm64-v8a** Number of test runs: **10** Test application: Xamarin.Forms "Hello World" app with one label | | **Native to managed** | **Runtime init** | **Displayed** | **Notes** | |-----------------|------------------------|------------------|---------------|--------------------------------| | **master** | 122.090 | 142.004 | 639.00 | preload enabled; 32-bit build | | **this commit** | 44.370 | 63.803 | 586.10 | | | **master** | 121.110 | 134.378 | 634.20 | preload disabled; 32-bit build | | **this commit** | 45.085 | 57.992 | 580.40 | | | **master** | 120.973 | 141.235 | 637.20 | preload enabled; 64-bit build | | **this commit** | 44.767 | 63.846 | 578.50 | | | **master** | 120.785 | 134.588 | 627.00 | preload disabled; 64-bit build | | **this commit** | 44.859 | 57.590 | 575.40 | |
1 parent 2fd7385 commit c82f7d7

File tree

23 files changed

+142
-110
lines changed

23 files changed

+142
-110
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
### App startup performance
2+
3+
* [GitHub PR 4302](https://github.com/xamarin/xamarin-android/pull/4302):
4+
Avoid unneeded calls to `GetCustomAttribute()` during app startup for the
5+
common case where an app has no types decorated with the
6+
`[JniAddNativeMethodRegistration]` attribute. Additionally, instead of
7+
using a managed method to propagate uncaught exceptions from Java, use a
8+
Java method that calls into the unmanaged Xamarin.Android runtime. These
9+
changes reduced the time to display the first screen of a small test
10+
Xamarin.Forms app from about 730 milliseconds to about 700 milliseconds for
11+
a Release configuration build on a Google Pixel 3 XL device.

src/Mono.Android/Android.Runtime/AndroidRuntime.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@ namespace Android.Runtime {
1515

1616
class AndroidRuntime : JniRuntime {
1717

18-
internal AndroidRuntime (IntPtr jnienv, IntPtr vm, bool allocNewObjectSupported, IntPtr classLoader, IntPtr classLoader_loadClass)
19-
: base (new AndroidRuntimeOptions (jnienv, vm, allocNewObjectSupported, classLoader, classLoader_loadClass))
18+
internal AndroidRuntime (IntPtr jnienv,
19+
IntPtr vm,
20+
bool allocNewObjectSupported,
21+
IntPtr classLoader,
22+
IntPtr classLoader_loadClass,
23+
bool jniAddNativeMethodRegistrationAttributePresent)
24+
: base (new AndroidRuntimeOptions (jnienv,
25+
vm,
26+
allocNewObjectSupported,
27+
classLoader,
28+
classLoader_loadClass,
29+
jniAddNativeMethodRegistrationAttributePresent))
2030
{
2131
}
2232

@@ -67,18 +77,23 @@ public override void RaisePendingException (Exception pendingException)
6777
}
6878

6979
class AndroidRuntimeOptions : JniRuntime.CreationOptions {
70-
71-
public AndroidRuntimeOptions (IntPtr jnienv, IntPtr vm, bool allocNewObjectSupported, IntPtr classLoader, IntPtr classLoader_loadClass)
80+
public AndroidRuntimeOptions (IntPtr jnienv,
81+
IntPtr vm,
82+
bool allocNewObjectSupported,
83+
IntPtr classLoader,
84+
IntPtr classLoader_loadClass,
85+
bool jniAddNativeMethodRegistrationAttributePresent)
7286
{
7387
EnvironmentPointer = jnienv;
7488
ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global);
7589
ClassLoader_LoadClass_id= classLoader_loadClass;
7690
InvocationPointer = vm;
7791
NewObjectRequired = !allocNewObjectSupported;
7892
ObjectReferenceManager = new AndroidObjectReferenceManager ();
79-
TypeManager = new AndroidTypeManager ();
93+
TypeManager = new AndroidTypeManager (jniAddNativeMethodRegistrationAttributePresent);
8094
ValueManager = new AndroidValueManager ();
8195
UseMarshalMemberBuilder = false;
96+
JniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent;
8297
}
8398
}
8499

@@ -219,6 +234,12 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value)
219234
}
220235

221236
class AndroidTypeManager : JniRuntime.JniTypeManager {
237+
bool jniAddNativeMethodRegistrationAttributePresent;
238+
239+
public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent)
240+
{
241+
this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent;
242+
}
222243

223244
protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
224245
{
@@ -347,13 +368,15 @@ public override void RegisterNativeMembers (JniType nativeClass, Type type, stri
347368
return;
348369

349370
if (string.IsNullOrEmpty (methods)) {
350-
base.RegisterNativeMembers (nativeClass, type, methods);
371+
if (jniAddNativeMethodRegistrationAttributePresent)
372+
base.RegisterNativeMembers (nativeClass, type, methods);
351373
return;
352374
}
353375

354376
string[] members = methods.Split ('\n');
355377
if (members.Length < 2) {
356-
base.RegisterNativeMembers (nativeClass, type, methods);
378+
if (jniAddNativeMethodRegistrationAttributePresent)
379+
base.RegisterNativeMembers (nativeClass, type, methods);
357380
return;
358381
}
359382

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct JnienvInitializeArgs {
3333
public byte brokenExceptionTransitions;
3434
public int packageNamingPolicy;
3535
public byte ioExceptionType;
36+
public int jniAddNativeMethodRegistrationAttributePresent;
3637
}
3738
#pragma warning restore 0649
3839

@@ -54,7 +55,6 @@ public static partial class JNIEnv {
5455
internal static int gref_gc_threshold;
5556

5657
internal static bool PropagateExceptions;
57-
static UncaughtExceptionHandler defaultUncaughtExceptionHandler;
5858

5959
internal static bool IsRunningOnDesktop;
6060
internal static bool LogTypemapMissStackTrace;
@@ -173,7 +173,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
173173
Mono.SystemDependencyProvider.Initialize ();
174174

175175
BoundExceptionType = (BoundExceptionType)args->ioExceptionType;
176-
androidRuntime = new AndroidRuntime (args->env, args->javaVm, androidSdkVersion > 10, args->grefLoader, args->Loader_loadClass);
176+
androidRuntime = new AndroidRuntime (args->env, args->javaVm, androidSdkVersion > 10, args->grefLoader, args->Loader_loadClass, args->jniAddNativeMethodRegistrationAttributePresent != 0);
177177
AndroidValueManager = (AndroidValueManager) androidRuntime.ValueManager;
178178

179179
AllocObjectSupported = androidSdkVersion > 10;
@@ -183,12 +183,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
183183

184184
PropagateExceptions = args->brokenExceptionTransitions == 0;
185185

186-
if (PropagateExceptions) {
187-
defaultUncaughtExceptionHandler = new UncaughtExceptionHandler (Java.Lang.Thread.DefaultUncaughtExceptionHandler);
188-
if (!IsRunningOnDesktop)
189-
Java.Lang.Thread.DefaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
190-
}
191-
192186
JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args->packageNamingPolicy;
193187
if (IsRunningOnDesktop) {
194188
string packageNamingPolicy = Environment.GetEnvironmentVariable ("__XA_PACKAGE_NAMING_POLICY__");
@@ -204,13 +198,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
204198

205199
internal static void Exit ()
206200
{
207-
/* Reset uncaught exception handler so that we don't mistakenly reuse a
208-
* now-invalid handler the next time we reinitialize JNIEnv.
209-
*/
210-
var uncaughtExceptionHandler = Java.Lang.Thread.DefaultUncaughtExceptionHandler as UncaughtExceptionHandler;
211-
if (uncaughtExceptionHandler != null && uncaughtExceptionHandler == defaultUncaughtExceptionHandler)
212-
Java.Lang.Thread.DefaultUncaughtExceptionHandler = uncaughtExceptionHandler.DefaultHandler;
213-
214201
/* Manually dispose surfaced objects and close the current JniEnvironment to
215202
* avoid ObjectDisposedException thrown on finalizer threads after shutdown
216203
*/
@@ -251,13 +238,20 @@ static void ManualJavaObjectDispose (Java.Lang.Object obj)
251238

252239
internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
253240
{
254-
if (defaultUncaughtExceptionHandler == null)
241+
if (!PropagateExceptions)
255242
return;
256243

257-
var javaThread = JavaObject.GetObject<Java.Lang.Thread> (env, javaThreadPtr, JniHandleOwnership.DoNotTransfer);
258244
var javaException = JavaObject.GetObject<Java.Lang.Throwable> (env, javaExceptionPtr, JniHandleOwnership.DoNotTransfer);
259245

260-
defaultUncaughtExceptionHandler.UncaughtException (javaThread, javaException);
246+
System.Diagnostics.Debugger.Mono_UnhandledException (javaException);
247+
try {
248+
var jltp = javaException as JavaProxyThrowable;
249+
Exception innerException = jltp?.InnerException;
250+
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
251+
AppDomain.CurrentDomain.DoUnhandledException (args);
252+
} catch (Exception e) {
253+
Logger.Log (LogLevel.Error, "monodroid", "Exception thrown while raising AppDomain.UnhandledException event: " + e.ToString ());
254+
}
261255
}
262256

263257
[DllImport ("__Internal", CallingConvention = CallingConvention.Cdecl)]

src/Mono.Android/Android.Runtime/UncaughtExceptionHandler.cs

Lines changed: 0 additions & 69 deletions
This file was deleted.

src/Mono.Android/Android.Runtime/XAPeerMembers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Android.Runtime {
77

88
public class XAPeerMembers : JniPeerMembers {
99

10-
static Dictionary<string, JniPeerMembers> LegacyPeerMembers = new Dictionary<string, JniPeerMembers> ();
10+
static Dictionary<string, JniPeerMembers> LegacyPeerMembers = new Dictionary<string, JniPeerMembers> (StringComparer.Ordinal);
1111

1212
public XAPeerMembers (string jniPeerTypeName, Type managedPeerType)
1313
: base (jniPeerTypeName, managedPeerType)
@@ -73,4 +73,4 @@ static IntPtr GetThresholdClass (IJavaPeerable value)
7373
return IntPtr.Zero;
7474
}
7575
}
76-
}
76+
}

src/Mono.Android/Java.Interop/TypeManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ static class TypeManagerMapDictionaries
2323
public static Dictionary<string, Type> JniToManaged {
2424
get {
2525
if (_jniToManaged == null)
26-
_jniToManaged = new Dictionary<string, Type> ();
26+
_jniToManaged = new Dictionary<string, Type> (StringComparer.Ordinal);
2727
return _jniToManaged;
2828
}
2929
}

src/Mono.Android/Mono.Android.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@
234234
<Compile Include="Android.Runtime\StringDefAttribute.cs" />
235235
<Compile Include="Android.Runtime\TimingLogger.cs" />
236236
<Compile Include="Android.Runtime\TypeManager.cs" />
237-
<Compile Include="Android.Runtime\UncaughtExceptionHandler.cs" />
238237
<Compile Include="Android.Runtime\XAPeerMembers.cs" />
239238
<Compile Include="Android.Runtime\XmlPullParserReader.cs" />
240239
<Compile Include="Android.Runtime\XmlReaderPullParser.cs" />

src/Mono.Android/Test/Mono.Android-Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<AssemblyOriginatorKeyFile>..\..\..\product.snk</AssemblyOriginatorKeyFile>
2323
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
2424
<AndroidDexTool Condition=" '$(AndroidDexTool)' == '' ">d8</AndroidDexTool>
25+
<_SkipJniAddNativeMethodRegistrationAttributeScan>True</_SkipJniAddNativeMethodRegistrationAttributeScan>
2526
</PropertyGroup>
2627
<Import Project="Mono.Android-Test.Shared.projitems" Label="Shared" Condition="Exists('Mono.Android-Test.Shared.projitems')" />
2728
<Import Project="..\..\..\Configuration.props" />

src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public class GenerateJavaStubs : AndroidTask
7777

7878
public string ApplicationJavaClass { get; set; }
7979

80+
public bool SkipJniAddNativeMethodRegistrationAttributeScan { get; set; }
81+
8082
[Output]
8183
public string [] GeneratedBinaryTypeMaps { get; set; }
8284

@@ -400,9 +402,10 @@ void SaveResource (string resource, string filename, string destDir, Func<string
400402
void WriteTypeMappings (List<TypeDefinition> types)
401403
{
402404
var tmg = new TypeMapGenerator ((string message) => Log.LogDebugMessage (message), SupportedAbis);
403-
if (!tmg.Generate (types, TypemapOutputDirectory, GenerateNativeAssembly))
405+
if (!tmg.Generate (SkipJniAddNativeMethodRegistrationAttributeScan, types, TypemapOutputDirectory, GenerateNativeAssembly, out ApplicationConfigTaskState appConfState))
404406
throw new XamarinAndroidException (4308, Properties.Resources.XA4308);
405407
GeneratedBinaryTypeMaps = tmg.GeneratedBinaryTypeMaps.ToArray ();
408+
BuildEngine4.RegisterTaskObject (ApplicationConfigTaskState.RegisterTaskObjectKey, appConfState, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false);
406409
}
407410
}
408411
}

src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ void AddEnvironment ()
249249
throw new InvalidOperationException ($"Unsupported BoundExceptionType value '{BoundExceptionType}'");
250250
}
251251

252+
var appConfState = BuildEngine4.GetRegisteredTaskObject (ApplicationConfigTaskState.RegisterTaskObjectKey, RegisteredTaskObjectLifetime.Build) as ApplicationConfigTaskState;
252253
foreach (string abi in SupportedAbis) {
253254
NativeAssemblerTargetProvider asmTargetProvider;
254255
string baseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"environment.{abi.ToLowerInvariant ()}");
@@ -285,6 +286,7 @@ void AddEnvironment ()
285286
PackageNamingPolicy = pnp,
286287
BoundExceptionType = boundExceptionType,
287288
InstantRunEnabled = InstantRunEnabled,
289+
JniAddNativeMethodRegistrationAttributePresent = appConfState != null ? appConfState.JniAddNativeMethodRegistrationAttributePresent : false,
288290
};
289291

290292
using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {

0 commit comments

Comments
 (0)