From 6fb9a220155e04b99f23c20fa9a9de3167796fe0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 17 Aug 2026 13:28:33 -0500 Subject: [PATCH 1/2] [Mono.Android] Avoid lock for activation constructor cache Use a ConcurrentDictionary stateful factory so cached proxy activation lookups no longer enter a monitor while preserving constructor trim annotations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bafa7c78-cb0c-45a5-9ec0-65cecea94620 --- src/Mono.Android/Java.Interop/TypeManager.cs | 46 +++++++++------ .../ActivationConstructorCacheBenchmarks.cs | 58 +++++++++++++++++++ 2 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 632a81d5798..7f131d55ffd 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -407,8 +408,7 @@ static Type monovm_typemap_java_to_managed (string java_type_name) static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; static readonly Type[] JIConstructorSignature = new Type [] { typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions) }; - static readonly Dictionary ActivationConstructorCache = new Dictionary (); - static readonly Lock ActivationConstructorCacheLock = new Lock (); + static readonly ConcurrentDictionary ActivationConstructorCache = new ConcurrentDictionary (1, 3); enum ActivationConstructorKind { @@ -419,6 +419,23 @@ enum ActivationConstructorKind readonly record struct ActivationConstructor (ConstructorInfo? Constructor, ActivationConstructorKind Kind); + /// + /// Preserves constructor annotations through the stateful GetOrAdd factory, + /// whose Func key parameter cannot carry . + /// + readonly struct AnnotatedType + { + public AnnotatedType ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) + { + Type = type; + } + + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + public Type Type { get; } + } + internal static object CreateProxy ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, @@ -452,23 +469,18 @@ static ActivationConstructor GetActivationConstructor ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) { - lock (ActivationConstructorCacheLock) { - if (ActivationConstructorCache.TryGetValue (type, out var activation)) - return activation; - - const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - var constructor = type.GetConstructor (flags, null, XAConstructorSignature, null); - if (constructor != null) { - activation = new ActivationConstructor (constructor, ActivationConstructorKind.XA); - } else { - constructor = type.GetConstructor (flags, null, JIConstructorSignature, null); - activation = new ActivationConstructor ( + return ActivationConstructorCache.GetOrAdd (type, + static (_, state) => { + const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var constructor = state.Type.GetConstructor (flags, null, XAConstructorSignature, null); + if (constructor != null) + return new ActivationConstructor (constructor, ActivationConstructorKind.XA); + + constructor = state.Type.GetConstructor (flags, null, JIConstructorSignature, null); + return new ActivationConstructor ( constructor, constructor == null ? ActivationConstructorKind.Missing : ActivationConstructorKind.JI); - } - ActivationConstructorCache.Add (type, activation); - return activation; - } + }, new AnnotatedType (type)); } static IJavaPeerable GetUninitializedObject ( diff --git a/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs b/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs new file mode 100644 index 00000000000..10d50b2c90c --- /dev/null +++ b/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Android.Runtime; +using BenchmarkDotNet.Attributes; + +namespace Xamarin.Android.Benchmarks; + +public class ActivationConstructorCacheBenchmarks +{ + delegate object CreateProxyDelegate (Type type, IntPtr handle, JniHandleOwnership transfer); + + readonly CreateProxyDelegate createProxy; + readonly Java.Lang.String peer; + + public ActivationConstructorCacheBenchmarks () + { + const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Static; + var method = typeof (Java.Interop.TypeManager).GetMethod ( + "CreateProxy", + flags, + null, + [typeof (Type), typeof (IntPtr), typeof (JniHandleOwnership)], + null); + if (method == null) + throw new InvalidOperationException ("Could not find TypeManager.CreateProxy."); + + createProxy = method.CreateDelegate (); + peer = new Java.Lang.String ("benchmark"); + } + + [GlobalSetup] + public void Setup () + { + _ = CreateProxy (); + } + + [GlobalCleanup] + public void Cleanup () + { + peer.Dispose (); + } + + [Benchmark] + public int CreateProxy () + { + IntPtr reference = JNIEnv.NewLocalRef (peer.Handle); + Java.Lang.String? proxy = null; + try { + proxy = (Java.Lang.String) createProxy (typeof (Java.Lang.String), reference, JniHandleOwnership.TransferLocalRef); + reference = IntPtr.Zero; + return RuntimeHelpers.GetHashCode (proxy); + } finally { + if (reference != IntPtr.Zero) + JNIEnv.DeleteLocalRef (reference); + proxy?.Dispose (); + } + } +} From cb000a165b2001b2a99dcefdbac429a44c51e0e2 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 17 Aug 2026 13:53:56 -0500 Subject: [PATCH 2/2] [tests] Benchmark unregistered activation handles Use an unmanaged jstring global reference so the CreateProxy benchmark exercises normal managed activation instead of duplicate-peer detection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bafa7c78-cb0c-45a5-9ec0-65cecea94620 --- .../ActivationConstructorCacheBenchmarks.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs b/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs index 10d50b2c90c..e4acc594416 100644 --- a/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs +++ b/tests/Android.Benchmarks/ActivationConstructorCacheBenchmarks.cs @@ -10,7 +10,7 @@ public class ActivationConstructorCacheBenchmarks delegate object CreateProxyDelegate (Type type, IntPtr handle, JniHandleOwnership transfer); readonly CreateProxyDelegate createProxy; - readonly Java.Lang.String peer; + IntPtr globalReference; public ActivationConstructorCacheBenchmarks () { @@ -25,25 +25,30 @@ public ActivationConstructorCacheBenchmarks () throw new InvalidOperationException ("Could not find TypeManager.CreateProxy."); createProxy = method.CreateDelegate (); - peer = new Java.Lang.String ("benchmark"); } [GlobalSetup] public void Setup () { + IntPtr reference = JNIEnv.NewString ("benchmark"); + try { + globalReference = JNIEnv.NewGlobalRef (reference); + } finally { + JNIEnv.DeleteLocalRef (reference); + } _ = CreateProxy (); } [GlobalCleanup] public void Cleanup () { - peer.Dispose (); + JNIEnv.DeleteGlobalRef (globalReference); } [Benchmark] public int CreateProxy () { - IntPtr reference = JNIEnv.NewLocalRef (peer.Handle); + IntPtr reference = JNIEnv.NewLocalRef (globalReference); Java.Lang.String? proxy = null; try { proxy = (Java.Lang.String) createProxy (typeof (Java.Lang.String), reference, JniHandleOwnership.TransferLocalRef);