diff --git a/Xamarin.Android-Tests.slnx b/Xamarin.Android-Tests.slnx index 18741c6dc9b..4bc3d8fd1e6 100644 --- a/Xamarin.Android-Tests.slnx +++ b/Xamarin.Android-Tests.slnx @@ -27,6 +27,7 @@ + diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs index e0a5ae40d87..43c33bdf95d 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceFields.cs @@ -1,7 +1,7 @@ #nullable enable using System; -using System.Collections.Generic; +using System.Collections.Concurrent; namespace Java.Interop { @@ -15,7 +15,7 @@ internal JniInstanceFields (JniPeerMembers members) readonly JniPeerMembers Members; - Dictionary InstanceFields = new Dictionary(StringComparer.Ordinal); + readonly ConcurrentDictionary InstanceFields = new ConcurrentDictionary (1, 3, StringComparer.Ordinal); internal void Dispose () { @@ -24,16 +24,11 @@ internal void Dispose () public JniFieldInfo GetFieldInfo (string encodedMember) { - lock (InstanceFields) { - if (!InstanceFields.TryGetValue (encodedMember, out var f)) { - string field, signature; - JniPeerMembers.GetNameAndSignature (encodedMember, out field, out signature); - f = Members.JniPeerType.GetInstanceField (field, signature); - InstanceFields.Add (encodedMember, f); - } - return f; - } + return InstanceFields.GetOrAdd (encodedMember, static (member, fields) => { + string field, signature; + JniPeerMembers.GetNameAndSignature (member, out field, out signature); + return fields.Members.JniPeerType.GetInstanceField (field, signature); + }, this); } }} } - diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index 6d8d14602ca..90ababdb74b 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -1,7 +1,7 @@ #nullable enable using System; -using System.Collections.Generic; +using System.Collections.Concurrent; namespace Java.Interop { @@ -39,8 +39,8 @@ internal JniType JniPeerType { readonly Type DeclaringType; - Dictionary InstanceMethods = new Dictionary(StringComparer.Ordinal); - Dictionary SubclassConstructors = new Dictionary (); + readonly ConcurrentDictionary InstanceMethods = new ConcurrentDictionary (1, 3, StringComparer.Ordinal); + readonly ConcurrentDictionary SubclassConstructors = new ConcurrentDictionary (1, 1); internal void Dispose () { @@ -58,13 +58,8 @@ public JniMethodInfo GetConstructor (string signature) { if (signature == null) throw new ArgumentNullException (nameof (signature)); - lock (InstanceMethods) { - if (!InstanceMethods.TryGetValue (signature, out var m)) { - m = JniPeerType.GetConstructor (signature); - InstanceMethods.Add (signature, m); - } - return m; - } + return InstanceMethods.GetOrAdd (signature, static (member, methods) => + methods.JniPeerType.GetConstructor (member), this); } internal JniInstanceMethods GetConstructorsForType (Type declaringType) @@ -72,13 +67,7 @@ internal JniInstanceMethods GetConstructorsForType (Type declaringType) if (declaringType == DeclaringType) return this; - JniInstanceMethods? methods; - - lock (SubclassConstructors) { - if (SubclassConstructors.TryGetValue (declaringType, out methods)) - return methods; - } - // Init outside of `lock` in case we have recursive access: + // Initialize before publication in case construction recursively accesses this cache: // System.ArgumentException: An item with the same key has already been added. Key: Java.Interop.JavaProxyThrowable // at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) // at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) @@ -100,32 +89,16 @@ internal JniInstanceMethods GetConstructorsForType (Type declaringType) // at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 27 // at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 77 // at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 146 - methods = new JniInstanceMethods (declaringType); - lock (SubclassConstructors) { - if (SubclassConstructors.TryGetValue (declaringType, out var m)) - return m; - SubclassConstructors.Add (declaringType, methods); - return methods; - } + return SubclassConstructors.GetOrAdd (declaringType, static type => new JniInstanceMethods (type)); } public JniMethodInfo GetMethodInfo (string encodedMember) { - lock (InstanceMethods) { - if (InstanceMethods.TryGetValue (encodedMember, out var m)) { - return m; - } - } - string method, signature; - JniPeerMembers.GetNameAndSignature (encodedMember, out method, out signature); - var info = GetMethodInfo (method, signature); - lock (InstanceMethods) { - if (InstanceMethods.TryGetValue (encodedMember, out var m)) { - return m; - } - InstanceMethods.Add (encodedMember, info); - } - return info; + return InstanceMethods.GetOrAdd (encodedMember, static (member, methods) => { + string method, signature; + JniPeerMembers.GetNameAndSignature (member, out method, out signature); + return methods.GetMethodInfo (method, signature); + }, this); } JniMethodInfo GetMethodInfo (string method, string signature) diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs index 7fe131b6206..f0c490460ab 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticFields.cs @@ -1,7 +1,7 @@ #nullable enable using System; -using System.Collections.Generic; +using System.Collections.Concurrent; namespace Java.Interop { @@ -15,19 +15,15 @@ internal JniStaticFields (JniPeerMembers members) readonly JniPeerMembers Members; - Dictionary StaticFields = new Dictionary(StringComparer.Ordinal); + readonly ConcurrentDictionary StaticFields = new ConcurrentDictionary (1, 3, StringComparer.Ordinal); public JniFieldInfo GetFieldInfo (string encodedMember) { - lock (StaticFields) { - if (!StaticFields.TryGetValue (encodedMember, out var f)) { - string field, signature; - JniPeerMembers.GetNameAndSignature (encodedMember, out field, out signature); - f = Members.JniPeerType.GetStaticField (field, signature); - StaticFields.Add (encodedMember, f); - } - return f; - } + return StaticFields.GetOrAdd (encodedMember, static (member, fields) => { + string field, signature; + JniPeerMembers.GetNameAndSignature (member, out field, out signature); + return fields.Members.JniPeerType.GetStaticField (field, signature); + }, this); } internal void Dispose () @@ -36,4 +32,3 @@ internal void Dispose () } }} } - diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs index 00b391bc112..379d6f21c52 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs @@ -1,7 +1,7 @@ #nullable enable using System; -using System.Collections.Generic; +using System.Collections.Concurrent; namespace Java.Interop { @@ -15,7 +15,7 @@ internal JniStaticMethods (JniPeerMembers members) internal readonly JniPeerMembers Members; - Dictionary StaticMethods = new Dictionary(StringComparer.Ordinal); + readonly ConcurrentDictionary StaticMethods = new ConcurrentDictionary (1, 3, StringComparer.Ordinal); internal void Dispose () { @@ -24,21 +24,11 @@ internal void Dispose () public JniMethodInfo GetMethodInfo (string encodedMember) { - lock (StaticMethods) { - if (StaticMethods.TryGetValue (encodedMember, out var m)) { - return m; - } - } - string method, signature; - JniPeerMembers.GetNameAndSignature (encodedMember, out method, out signature); - var info = GetMethodInfo (method, signature); - lock (StaticMethods) { - if (StaticMethods.TryGetValue (encodedMember, out var m)) { - return m; - } - StaticMethods.Add (encodedMember, info); - } - return info; + return StaticMethods.GetOrAdd (encodedMember, static (member, methods) => { + string method, signature; + JniPeerMembers.GetNameAndSignature (member, out method, out signature); + return methods.GetMethodInfo (method, signature); + }, this); } JniMethodInfo GetMethodInfo (string method, string signature) @@ -160,4 +150,3 @@ public unsafe JniObjectReference InvokeObjectMethod (string encodedMember, JniAr } }} } - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index b065dd8c14c..45b2a3cb6e1 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Reflection; using Java.Interop; @@ -30,10 +30,10 @@ public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () } } - static Dictionary GetInstanceMethods (JniPeerMembers.JniInstanceMethods methods) + static ConcurrentDictionary GetInstanceMethods (JniPeerMembers.JniInstanceMethods methods) { var f = typeof (JniPeerMembers.JniInstanceMethods).GetField ("InstanceMethods", BindingFlags.NonPublic | BindingFlags.Instance); - return (Dictionary) f.GetValue (methods); + return (ConcurrentDictionary) f.GetValue (methods); } [Test] diff --git a/tests/Android.Benchmarks/Android.Benchmarks.csproj b/tests/Android.Benchmarks/Android.Benchmarks.csproj new file mode 100644 index 00000000000..514ce0b7051 --- /dev/null +++ b/tests/Android.Benchmarks/Android.Benchmarks.csproj @@ -0,0 +1,22 @@ + + + + $(DotNetAndroidTargetFramework) + $(AndroidMinimumDotNetApiLevel) + Exe + enable + enable + true + net.dot.android.benchmarks + 1 + 1.0 + Xamarin.Android.Benchmarks + apk + false + + + + + + + diff --git a/tests/Android.Benchmarks/AndroidManifest.xml b/tests/Android.Benchmarks/AndroidManifest.xml new file mode 100644 index 00000000000..4f6311983a5 --- /dev/null +++ b/tests/Android.Benchmarks/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/Android.Benchmarks/BenchmarkInstrumentation.cs b/tests/Android.Benchmarks/BenchmarkInstrumentation.cs new file mode 100644 index 00000000000..2247219cb7c --- /dev/null +++ b/tests/Android.Benchmarks/BenchmarkInstrumentation.cs @@ -0,0 +1,83 @@ +using Android.Runtime; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; + +namespace Xamarin.Android.Benchmarks; + +[Instrumentation (Name = "net.dot.android.benchmarks.BenchmarkInstrumentation")] +public class BenchmarkInstrumentation : Instrumentation +{ + protected BenchmarkInstrumentation (IntPtr handle, JniHandleOwnership ownership) + : base (handle, ownership) + { + } + + public override void OnCreate (Bundle? arguments) + { + base.OnCreate (arguments); + Filter = GetFilter (arguments); + Start (); + } + + public override void OnStart () + { + base.OnStart (); + + var results = new Bundle (); + try { + var externalFiles = Application.Context.GetExternalFilesDir (null)?.AbsolutePath; + var artifactsPath = Path.Combine (externalFiles ?? Path.GetTempPath (), "BenchmarkDotNet.Artifacts"); + var config = ManualConfig.CreateEmpty () + .AddJob (Job.ShortRun + .WithToolchain (InProcessNoEmitToolchain.Instance) + .WithId ("Android")) + .AddLogger (ConsoleLogger.Default) + .AddColumnProvider (DefaultColumnProviders.Instance) + .AddExporter (CsvExporter.Default, MarkdownExporter.GitHub) + .WithArtifactsPath (artifactsPath) + .WithOptions (ConfigOptions.DisableOptimizationsValidator); + if (Filter != null) + config.AddFilter (new GlobFilter ([Filter])); + + var summaries = BenchmarkRunner.Run (GetType ().Assembly, config); + var reportCount = summaries.Sum (summary => summary.Reports.Length); + var hasErrors = summaries.Any (summary => summary.HasCriticalValidationErrors); + + results.PutInt ("reports", reportCount); + results.PutString ("artifactsPath", artifactsPath); + Console.WriteLine ($"BENCHMARKS_COMPLETE reports={reportCount} artifacts={artifactsPath}"); + Finish (hasErrors ? Result.Canceled : Result.Ok, results); + } catch (Exception ex) { + results.PutString ("error", ex.ToString ()); + Console.WriteLine ($"BENCHMARKS_FAILED {ex}"); + Finish (Result.Canceled, results); + } + } + + string? Filter { get; set; } + + static string? GetFilter (Bundle? arguments) + { + var filter = arguments?.GetString ("filter"); + if (!string.IsNullOrWhiteSpace (filter)) + return filter; + + var value = arguments?.GetString ("args"); + if (string.IsNullOrWhiteSpace (value)) + return null; + + var values = value.Split (' ', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < values.Length - 1; i++) { + if (values [i] == "--filter") + return values [i + 1]; + } + return null; + } +} diff --git a/tests/Android.Benchmarks/JniMethodInfoBenchmarks.cs b/tests/Android.Benchmarks/JniMethodInfoBenchmarks.cs new file mode 100644 index 00000000000..1b34093d821 --- /dev/null +++ b/tests/Android.Benchmarks/JniMethodInfoBenchmarks.cs @@ -0,0 +1,42 @@ +using BenchmarkDotNet.Attributes; + +namespace Xamarin.Android.Benchmarks; + +public unsafe class JniMethodInfoBenchmarks +{ + const string EncodedMember = "hashCode.()I"; + + readonly Java.Lang.String peer; + readonly Java.Interop.JniPeerMembers.JniInstanceMethods methods; + + public JniMethodInfoBenchmarks () + { + peer = new Java.Lang.String ("benchmark"); + methods = peer.JniPeerMembers.InstanceMethods; + } + + [GlobalSetup] + public void Setup () + { + _ = InvokeWithStringCache (); + _ = InvokeGeneratedBinding (); + } + + [GlobalCleanup] + public void Cleanup () + { + peer.Dispose (); + } + + [Benchmark (Baseline = true)] + public int InvokeWithStringCache () + { + return methods.InvokeVirtualInt32Method (EncodedMember, peer, null); + } + + [Benchmark] + public int InvokeGeneratedBinding () + { + return peer.GetHashCode (); + } +} diff --git a/tests/Android.Benchmarks/README.md b/tests/Android.Benchmarks/README.md new file mode 100644 index 00000000000..f540d1cde96 --- /dev/null +++ b/tests/Android.Benchmarks/README.md @@ -0,0 +1,19 @@ +# Android benchmarks + +This instrumentation-only app runs BenchmarkDotNet in-process on an Android +device. It has no activity. + +Run every benchmark: + +```sh +./dotnet-local.sh run --project tests/Android.Benchmarks/Android.Benchmarks.csproj -c Release +``` + +Filter benchmarks by passing BenchmarkDotNet's `--filter` argument: + +```sh +./dotnet-local.sh run --project tests/Android.Benchmarks/Android.Benchmarks.csproj -c Release -- --filter '*JniMethodInfoBenchmarks*' +``` + +The instrumentation result reports the on-device artifacts directory. Results +are also streamed through logcat by `dotnet run`.