Skip to content

Commit 2aff4e7

Browse files
authored
[Mono.Android] Reduce Reflection & reduce startup overhead (#4302)
Reduce or remove reflection use around JNI method registration, and rework how uncaught exceptions are handled. Java.Interop provides an alternate mechanism to allow registering Java native methods: [the `[JniAddNativeMethodRegistrationAttribute]` custom attribute][0] which can be placed on methods of a type to allow "manual" insertion into the [`JNIEnv::RegisterNatives()`][2] process. The `Java.Interop-Tests.dll` unit test assembly (130905e) uses this alternate native method registration mechanism.. Unfortunately, `[JniAddNativeMethodRegistration]` is looked up via Reflection, which isn't exactly "fast". Given that `[JniAddNativeMethodRegistration]` is only used by one assembly *in the world*, let's optimize the common case: [dotnet/java-interop@b33d183d][3] made the `[JniAddNativeMethodRegistration]` lookup *optional*. Disable the custom attribute lookup *by default*, and allow it to be enabled by setting the `$(_SkipJniAddNativeMethodRegistrationAttributeScan)` MSBuild property to True. Yes, this property is "private". We'll investigate more automatic and optimized ideas in the future. Rework how Java-side unhandled exceptions are processed. Previously, this was handled by the `Android.Runtime.UncaughtExceptionHandler` class, which was constructed during process startup. A side effect of this is that it required going through the (*normal* reflection- based) JNI registration logic of `mono.android.Runtime.register()`, contributing to startup for something that *ideally* would Never Happen™. (Nobody likes unhandled exceptions.) Changes this so that instead of a C#-side `UncaughtExceptionHandler` type we instead have a Java-side `mono.android.XamarinUncaughtExceptionHandler` type which is created and provided to `java.lang.Thread.setDefaultUncaughtExceptionHandler()` during startup. `XamarinUncaughtExceptionHandler` doesn't do anything until Java calls `XamarinUncaughtExceptionHandler.uncaughtException()`, which in turn invokes `Runtime.propagateUncaughtException()`, which in turn does what `UncaughtExceptionHandler` previously did: lookup `AppDomain.DoUnhandledException()` via Reflection and invoke it, thus raising the `AppDomain.UnhandledException` event. In this manner all overheads associated with unhandled exceptions are delayed until the exception happens, with minimal impact on startup. 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 | | [0]: dotnet/java-interop@7d51163 [2]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives [3]: dotnet/java-interop@b33d183
1 parent ad5af12 commit 2aff4e7

File tree

23 files changed

+179
-110
lines changed

23 files changed

+179
-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: 49 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
*/
@@ -249,15 +236,59 @@ static void ManualJavaObjectDispose (Java.Lang.Object obj)
249236
GC.SuppressFinalize (obj);
250237
}
251238

239+
static Action<Exception> mono_unhandled_exception;
240+
static Action<AppDomain, UnhandledExceptionEventArgs> AppDomain_DoUnhandledException;
241+
242+
static void Initialize ()
243+
{
244+
if (mono_unhandled_exception == null) {
245+
var mono_UnhandledException = typeof (System.Diagnostics.Debugger)
246+
.GetMethod ("Mono_UnhandledException", BindingFlags.NonPublic | BindingFlags.Static);
247+
mono_unhandled_exception = (Action<Exception>) Delegate.CreateDelegate (typeof(Action<Exception>), mono_UnhandledException);
248+
}
249+
250+
if (AppDomain_DoUnhandledException == null) {
251+
var ad_due = typeof (AppDomain)
252+
.GetMethod ("DoUnhandledException",
253+
bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance,
254+
binder: null,
255+
types: new []{typeof (UnhandledExceptionEventArgs)},
256+
modifiers: null);
257+
if (ad_due != null) {
258+
AppDomain_DoUnhandledException = (Action<AppDomain, UnhandledExceptionEventArgs>) Delegate.CreateDelegate (
259+
typeof (Action<AppDomain, UnhandledExceptionEventArgs>), ad_due);
260+
}
261+
}
262+
}
263+
252264
internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
253265
{
254-
if (defaultUncaughtExceptionHandler == null)
266+
if (!PropagateExceptions)
255267
return;
256268

257-
var javaThread = JavaObject.GetObject<Java.Lang.Thread> (env, javaThreadPtr, JniHandleOwnership.DoNotTransfer);
269+
try {
270+
Initialize ();
271+
} catch (Exception e) {
272+
Android.Runtime.AndroidEnvironment.FailFast ($"Unable to initialize UncaughtExceptionHandler. Nested exception caught: {e}");
273+
}
274+
258275
var javaException = JavaObject.GetObject<Java.Lang.Throwable> (env, javaExceptionPtr, JniHandleOwnership.DoNotTransfer);
259276

260-
defaultUncaughtExceptionHandler.UncaughtException (javaThread, javaException);
277+
// Disabled until Linker error surfaced in https://github.com/xamarin/xamarin-android/pull/4302#issuecomment-596400025 is resolved
278+
//System.Diagnostics.Debugger.Mono_UnhandledException (javaException);
279+
mono_unhandled_exception (javaException);
280+
281+
try {
282+
var jltp = javaException as JavaProxyThrowable;
283+
Exception innerException = jltp?.InnerException;
284+
var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
285+
286+
// Disabled until Linker error surfaced in https://github.com/xamarin/xamarin-android/pull/4302#issuecomment-596400025 is resolved
287+
//AppDomain.CurrentDomain.DoUnhandledException (args);
288+
AppDomain_DoUnhandledException (AppDomain.CurrentDomain, args);
289+
} catch (Exception e) {
290+
Logger.Log (LogLevel.Error, "monodroid", "Exception thrown while raising AppDomain.UnhandledException event: " + e.ToString ());
291+
}
261292
}
262293

263294
[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)