diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs index 35799ce1958..1f8fee3aedb 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Java; +using System.Threading; using Android.Runtime; using Java.Interop; @@ -31,8 +32,9 @@ namespace Microsoft.Android.Runtime; /// static class JavaMarshalRegisteredPeers { - static readonly Dictionary> RegisteredInstances = new (); + static readonly Dictionary RegisteredInstances = new (); static readonly ConcurrentQueue CollectedContexts = new (); + static readonly Lock s_instancesLock = new (); static readonly object initializeLock = new (); static bool initialized; @@ -65,7 +67,7 @@ public static void CollectPeers () Debug.Assert (contextPtr != IntPtr.Zero, "CollectedContexts should not contain null pointers."); HandleContext* context = (HandleContext*)contextPtr; - lock (RegisteredInstances) { + lock (s_instancesLock) { Remove (context); } @@ -75,18 +77,19 @@ public static void CollectPeers () void Remove (HandleContext* context) { int key = context->PeerIdentityHashCode; - if (!RegisteredInstances.TryGetValue (key, out List? peers)) + if (!RegisteredInstances.TryGetValue (key, out RegisteredPeerBucket peers)) return; for (int i = peers.Count - 1; i >= 0; i--) { - var peer = peers [i]; - if (peer.BelongsToContext (context)) { + if (peers [i].BelongsToContext (context)) { peers.RemoveAt (i); } } if (peers.Count == 0) { RegisteredInstances.Remove (key); + } else { + RegisteredInstances [key] = peers; } } } @@ -106,45 +109,71 @@ public static void AddPeer (IJavaPeerable value) JniObjectReference.Dispose (ref r, JniObjectReferenceOptions.CopyAndDispose); } int key = value.JniIdentityHashCode; - lock (RegisteredInstances) { - List? peers; - if (!RegisteredInstances.TryGetValue (key, out peers)) { - peers = [new ReferenceTrackingHandle (value)]; - RegisteredInstances.Add (key, peers); + lock (s_instancesLock) { + if (!RegisteredInstances.TryGetValue (key, out RegisteredPeerBucket peers)) { + RegisteredInstances.Add (key, new RegisteredPeerBucket (new ReferenceTrackingHandle (value))); return; } for (int i = peers.Count - 1; i >= 0; i--) { - ReferenceTrackingHandle peer = peers [i]; - if (peer.Target is not IJavaPeerable target) - continue; - if (!JniEnvironment.Types.IsSameObject (target.PeerReference, value.PeerReference)) - continue; - // JNIEnv.NewObject/JNIEnv.CreateInstance() compatibility. - // When two MCW's are created for one Java instance [0], - // we want the 2nd MCW to replace the 1st, as the 2nd is - // the one the dev created; the 1st is an implicit intermediary. - // - // Meanwhile, a new "replaceable" instance should *not* replace an - // existing "replaceable" instance; see dotnet/android#9862. - // - // [0]: If Java ctor invokes overridden virtual method, we'll - // transition into managed code w/o a registered instance, and - // thus will create an "intermediary" via - // (IntPtr, JniHandleOwnership) .ctor. - if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) && - !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { - peer.Dispose (); - peers [i] = new ReferenceTrackingHandle (value); - } else if (JniEnvironment.Runtime.ObjectReferenceManager.LogGlobalReferenceMessages) { - WarnNotReplacing (key, value, target); + var result = ReconcilePeer (peers [i], value, key, out ReferenceTrackingHandle replacement); + if (result == PeerReconciliationResult.Replace) { + peers [i] = replacement; + RegisteredInstances [key] = peers; + return; } - GC.KeepAlive (target); - return; + if (result == PeerReconciliationResult.Keep) + return; } peers.Add (new ReferenceTrackingHandle (value)); + RegisteredInstances [key] = peers; + } + } + + enum PeerReconciliationResult + { + NoMatch, + Keep, + Replace, + } + + static PeerReconciliationResult ReconcilePeer ( + ReferenceTrackingHandle registered, + IJavaPeerable value, + int key, + out ReferenceTrackingHandle replacement) + { + replacement = default; + if (registered.Target is not IJavaPeerable target) + return PeerReconciliationResult.NoMatch; + if (!JniEnvironment.Types.IsSameObject (target.PeerReference, value.PeerReference)) + return PeerReconciliationResult.NoMatch; + + // JNIEnv.NewObject/JNIEnv.CreateInstance() compatibility. + // When two MCW's are created for one Java instance [0], + // we want the 2nd MCW to replace the 1st, as the 2nd is + // the one the dev created; the 1st is an implicit intermediary. + // + // Meanwhile, a new "replaceable" instance should *not* replace an + // existing "replaceable" instance; see dotnet/android#9862. + // + // [0]: If Java ctor invokes overridden virtual method, we'll + // transition into managed code w/o a registered instance, and + // thus will create an "intermediary" via + // (IntPtr, JniHandleOwnership) .ctor. + if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) && + !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { + registered.Dispose (); + replacement = new ReferenceTrackingHandle (value); + GC.KeepAlive (target); + return PeerReconciliationResult.Replace; } + + if (JniEnvironment.Runtime.ObjectReferenceManager.LogGlobalReferenceMessages) + WarnNotReplacing (key, value, target); + GC.KeepAlive (target); + return PeerReconciliationResult.Keep; } static void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) @@ -170,8 +199,8 @@ static void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable int key = JniEnvironment.References.GetIdentityHashCode (reference); - lock (RegisteredInstances) { - if (!RegisteredInstances.TryGetValue (key, out List? peers)) + lock (s_instancesLock) { + if (!RegisteredInstances.TryGetValue (key, out RegisteredPeerBucket peers)) return null; for (int i = peers.Count - 1; i >= 0; i--) { @@ -196,9 +225,9 @@ public static void RemovePeer (IJavaPeerable value) if (value == null) throw new ArgumentNullException (nameof (value)); - lock (RegisteredInstances) { + lock (s_instancesLock) { int key = value.JniIdentityHashCode; - if (!RegisteredInstances.TryGetValue (key, out List? peers)) + if (!RegisteredInstances.TryGetValue (key, out RegisteredPeerBucket peers)) return; for (int i = peers.Count - 1; i >= 0; i--) { @@ -212,6 +241,8 @@ public static void RemovePeer (IJavaPeerable value) } if (peers.Count == 0) RegisteredInstances.Remove (key); + else + RegisteredInstances [key] = peers; } } @@ -255,10 +286,11 @@ public static List GetSurfacedPeers () // Remove any collected contexts before iterating over all the registered instances CollectPeers (); - lock (RegisteredInstances) { + lock (s_instancesLock) { var peers = new List (RegisteredInstances.Count); - foreach (var (identityHashCode, referenceTrackingHandles) in RegisteredInstances) { - foreach (var peer in referenceTrackingHandles) { + foreach (var (identityHashCode, registered) in RegisteredInstances) { + for (int i = 0; i < registered.Count; i++) { + var peer = registered [i]; if (peer.Target is IJavaPeerable target) { peers.Add (new JniSurfacedPeerInfo (identityHashCode, new WeakReference (target))); } @@ -268,6 +300,70 @@ public static List GetSurfacedPeers () } } + struct RegisteredPeerBucket + { + ReferenceTrackingHandle _first; + List? _rest; + + public RegisteredPeerBucket (ReferenceTrackingHandle first) + { + _first = first; + _rest = null; + } + + public int Count => (_first.IsValid ? 1 : 0) + (_rest?.Count ?? 0); + + public ReferenceTrackingHandle this [int index] { + get { + if (index == 0 && _first.IsValid) + return _first; + + var rest = _rest; + if (rest == null) + throw new ArgumentOutOfRangeException (nameof (index)); + return rest [index - 1]; + } + set { + if (index == 0 && _first.IsValid) { + _first = value; + return; + } + + var rest = _rest; + if (rest == null) + throw new ArgumentOutOfRangeException (nameof (index)); + rest [index - 1] = value; + } + } + + public void Add (ReferenceTrackingHandle peer) + { + if (!_first.IsValid) { + _first = peer; + return; + } + + _rest ??= []; + _rest.Add (peer); + } + + public void RemoveAt (int index) + { + if (index == 0) { + _first = default; + if (_rest?.Count > 0) { + _first = _rest [0]; + _rest.RemoveAt (0); + } + } else { + _rest?.RemoveAt (index - 1); + } + + if (_rest?.Count == 0) + _rest = null; + } + } + unsafe struct ReferenceTrackingHandle : IDisposable { WeakReference _weakReference; @@ -285,6 +381,8 @@ public ReferenceTrackingHandle (IJavaPeerable peer) public IJavaPeerable? Target => _weakReference.TryGetTarget (out var target) ? target : null; + public bool IsValid => _context != null; + public void Dispose () { if (_context == null) diff --git a/tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs b/tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs new file mode 100644 index 00000000000..4224e1651cc --- /dev/null +++ b/tests/Xamarin.Android.Tools.Benchmarks/RegisteredPeersBenchmarks.cs @@ -0,0 +1,594 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace Xamarin.Android.Tools.Benchmarks; + +[MemoryDiagnoser] +[InProcess] +[WarmupCount (3)] +[IterationCount (3)] +public class RegisteredPeersAddBenchmarks +{ + Peer[] peers = []; + + [Params (1_000)] + public int PeerCount { get; set; } + + [GlobalSetup] + public void Setup () + { + peers = Peer.CreateUnique (PeerCount); + } + + [Benchmark (Baseline = true)] + public int AlwaysList () + { + var registered = new AlwaysListRegisteredPeers (PeerCount); + for (int i = 0; i < peers.Length; i++) { + registered.Add (peers [i]); + } + return registered.Count; + } + + [Benchmark] + public int FirstRest () + { + var registered = new FirstRestRegisteredPeers (PeerCount); + for (int i = 0; i < peers.Length; i++) { + registered.Add (peers [i]); + } + return registered.Count; + } + + [Benchmark] + public int ObjectOrList () + { + var registered = new ObjectOrListRegisteredPeers (PeerCount); + for (int i = 0; i < peers.Length; i++) { + registered.Add (peers [i]); + } + return registered.Count; + } + + [Benchmark] + public int InlineFirstRest () + { + var registered = new InlineFirstRestRegisteredPeers (PeerCount); + for (int i = 0; i < peers.Length; i++) { + registered.Add (peers [i]); + } + return registered.Count; + } +} + +[MemoryDiagnoser] +[InProcess] +[WarmupCount (3)] +[IterationCount (3)] +public class RegisteredPeersPeekBenchmarks +{ + const int OperationsPerInvoke = 1_000; + + AlwaysListRegisteredPeers alwaysList = new (0); + FirstRestRegisteredPeers firstRest = new (0); + ObjectOrListRegisteredPeers objectOrList = new (0); + InlineFirstRestRegisteredPeers inlineFirstRest = new (0); + Peer[] expected = []; + + [Params (1, 4)] + public int PeersPerHash { get; set; } + + [GlobalSetup] + public void Setup () + { + alwaysList = new AlwaysListRegisteredPeers (OperationsPerInvoke); + firstRest = new FirstRestRegisteredPeers (OperationsPerInvoke); + objectOrList = new ObjectOrListRegisteredPeers (OperationsPerInvoke); + inlineFirstRest = new InlineFirstRestRegisteredPeers (OperationsPerInvoke); + expected = new Peer [OperationsPerInvoke]; + + for (int hash = 0; hash < OperationsPerInvoke; hash++) { + for (int id = 0; id < PeersPerHash; id++) { + var peer = new Peer (hash, id); + alwaysList.Add (peer); + firstRest.Add (peer); + objectOrList.Add (peer); + inlineFirstRest.Add (peer); + expected [hash] = peer; + } + } + } + + [Benchmark (Baseline = true, OperationsPerInvoke = OperationsPerInvoke)] + public int AlwaysList () + { + int checksum = 0; + for (int i = 0; i < expected.Length; i++) { + checksum += alwaysList.Peek (expected [i]).Id; + } + return checksum; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int FirstRest () + { + int checksum = 0; + for (int i = 0; i < expected.Length; i++) { + checksum += firstRest.Peek (expected [i]).Id; + } + return checksum; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int ObjectOrList () + { + int checksum = 0; + for (int i = 0; i < expected.Length; i++) { + checksum += objectOrList.Peek (expected [i]).Id; + } + return checksum; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int InlineFirstRest () + { + int checksum = 0; + for (int i = 0; i < expected.Length; i++) { + checksum += inlineFirstRest.Peek (expected [i]).Id; + } + return checksum; + } +} + +[MemoryDiagnoser] +[InProcess] +[WarmupCount (3)] +[IterationCount (3)] +public class RegisteredPeersChurnBenchmarks +{ + const int HashCount = 1_000; + const int PeersPerHash = 4; + const int OperationsPerInvoke = HashCount; + + readonly Peer[] peers = Peer.CreateCollisions (HashCount, PeersPerHash); + readonly Peer[] removePeers = new Peer [HashCount]; + AlwaysListRegisteredPeers alwaysList = new (0); + FirstRestRegisteredPeers firstRest = new (0); + ObjectOrListRegisteredPeers objectOrList = new (0); + InlineFirstRestRegisteredPeers inlineFirstRest = new (0); + + [GlobalSetup] + public void Setup () + { + alwaysList = new AlwaysListRegisteredPeers (HashCount); + firstRest = new FirstRestRegisteredPeers (HashCount); + objectOrList = new ObjectOrListRegisteredPeers (HashCount); + inlineFirstRest = new InlineFirstRestRegisteredPeers (HashCount); + AddPeers (alwaysList); + AddPeers (firstRest); + AddPeers (objectOrList); + AddPeers (inlineFirstRest); + for (int hash = 0; hash < removePeers.Length; hash++) { + removePeers [hash] = peers [(hash * PeersPerHash) + PeersPerHash - 1]; + } + } + + [Benchmark (Baseline = true, OperationsPerInvoke = OperationsPerInvoke)] + public int AlwaysList () + { + for (int i = 0; i < removePeers.Length; i++) { + alwaysList.Remove (removePeers [i]); + alwaysList.Add (removePeers [i]); + } + return alwaysList.Count; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int FirstRest () + { + for (int i = 0; i < removePeers.Length; i++) { + firstRest.Remove (removePeers [i]); + firstRest.Add (removePeers [i]); + } + return firstRest.Count; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int ObjectOrList () + { + for (int i = 0; i < removePeers.Length; i++) { + objectOrList.Remove (removePeers [i]); + objectOrList.Add (removePeers [i]); + } + return objectOrList.Count; + } + + [Benchmark (OperationsPerInvoke = OperationsPerInvoke)] + public int InlineFirstRest () + { + for (int i = 0; i < removePeers.Length; i++) { + inlineFirstRest.Remove (removePeers [i]); + inlineFirstRest.Add (removePeers [i]); + } + return inlineFirstRest.Count; + } + + void AddPeers (IRegisteredPeers registered) + { + for (int i = 0; i < peers.Length; i++) { + registered.Add (peers [i]); + } + } +} + +interface IRegisteredPeers +{ + int Count { get; } + + void Add (Peer peer); + + Peer Peek (Peer expected); + + void Remove (Peer peer); +} + +sealed class AlwaysListRegisteredPeers : IRegisteredPeers +{ + readonly Dictionary> peers; + + public AlwaysListRegisteredPeers (int capacity) + { + peers = new Dictionary> (capacity); + } + + public int Count => peers.Count; + + public void Add (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out List values)) { + values = [new ReferenceTrackingHandle (peer)]; + peers.Add (peer.Hash, values); + return; + } + + values.Add (new ReferenceTrackingHandle (peer)); + } + + public Peer Peek (Peer expected) + { + if (!peers.TryGetValue (expected.Hash, out List values)) + return null; + + for (int i = values.Count - 1; i >= 0; i--) { + Peer candidate = values [i].Target; + if (candidate.Id == expected.Id) + return candidate; + } + return null; + } + + public void Remove (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out List values)) + return; + + for (int i = values.Count - 1; i >= 0; i--) { + if (ReferenceEquals (values [i].Target, peer)) { + values.RemoveAt (i); + } + } + if (values.Count == 0) + peers.Remove (peer.Hash); + } +} + +sealed class FirstRestRegisteredPeers : IRegisteredPeers +{ + readonly Dictionary peers; + + public FirstRestRegisteredPeers (int capacity) + { + peers = new Dictionary (capacity); + } + + public int Count => peers.Count; + + public void Add (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out FirstRestBucket values)) { + peers.Add (peer.Hash, new FirstRestBucket (peer)); + return; + } + + values.Add (peer); + } + + public Peer Peek (Peer expected) + { + if (!peers.TryGetValue (expected.Hash, out FirstRestBucket values)) + return null; + + for (int i = values.Count - 1; i >= 0; i--) { + Peer candidate = values [i].Target; + if (candidate.Id == expected.Id) + return candidate; + } + return null; + } + + public void Remove (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out FirstRestBucket values)) + return; + + for (int i = values.Count - 1; i >= 0; i--) { + if (ReferenceEquals (values [i].Target, peer)) { + values.RemoveAt (i); + } + } + if (values.Count == 0) + peers.Remove (peer.Hash); + } +} + +sealed class FirstRestBucket +{ + ReferenceTrackingHandle first; + List rest; + bool hasFirst; + + public FirstRestBucket (Peer peer) + { + first = new ReferenceTrackingHandle (peer); + hasFirst = true; + } + + public int Count => (hasFirst ? 1 : 0) + (rest?.Count ?? 0); + + public ReferenceTrackingHandle this [int index] { + get => index == 0 ? first : rest [index - 1]; + } + + public void Add (Peer peer) + { + if (!hasFirst) { + first = new ReferenceTrackingHandle (peer); + hasFirst = true; + return; + } + + rest ??= []; + rest.Add (new ReferenceTrackingHandle (peer)); + } + + public void RemoveAt (int index) + { + if (index == 0) { + hasFirst = false; + if (rest?.Count > 0) { + first = rest [0]; + hasFirst = true; + rest.RemoveAt (0); + } + } else { + rest?.RemoveAt (index - 1); + } + + if (rest?.Count == 0) + rest = null; + } +} + +sealed class ObjectOrListRegisteredPeers : IRegisteredPeers +{ + readonly Dictionary peers; + + public ObjectOrListRegisteredPeers (int capacity) + { + peers = new Dictionary (capacity); + } + + public int Count => peers.Count; + + public void Add (Peer peer) + { + var handle = new ReferenceTrackingHandle (peer); + if (!peers.TryGetValue (peer.Hash, out object values)) { + peers.Add (peer.Hash, handle); + return; + } + + if (values is ReferenceTrackingHandle first) { + peers [peer.Hash] = new List { first, handle }; + return; + } + + ((List) values).Add (handle); + } + + public Peer Peek (Peer expected) + { + if (!peers.TryGetValue (expected.Hash, out object values)) + return null; + + if (values is ReferenceTrackingHandle single) + return single.Target.Id == expected.Id ? single.Target : null; + + var list = (List) values; + for (int i = list.Count - 1; i >= 0; i--) { + Peer candidate = list [i].Target; + if (candidate.Id == expected.Id) + return candidate; + } + return null; + } + + public void Remove (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out object values)) + return; + + if (values is ReferenceTrackingHandle single) { + if (ReferenceEquals (single.Target, peer)) + peers.Remove (peer.Hash); + return; + } + + var list = (List) values; + for (int i = list.Count - 1; i >= 0; i--) { + if (ReferenceEquals (list [i].Target, peer)) { + list.RemoveAt (i); + } + } + if (list.Count == 0) { + peers.Remove (peer.Hash); + } + } +} + +sealed class InlineFirstRestRegisteredPeers : IRegisteredPeers +{ + readonly Dictionary peers; + + public InlineFirstRestRegisteredPeers (int capacity) + { + peers = new Dictionary (capacity); + } + + public int Count => peers.Count; + + public void Add (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out InlineFirstRestBucket values)) { + peers.Add (peer.Hash, new InlineFirstRestBucket (peer)); + return; + } + + values.Add (peer); + peers [peer.Hash] = values; + } + + public Peer Peek (Peer expected) + { + if (!peers.TryGetValue (expected.Hash, out InlineFirstRestBucket values)) + return null; + + for (int i = values.Count - 1; i >= 0; i--) { + Peer candidate = values [i].Target; + if (candidate.Id == expected.Id) + return candidate; + } + return null; + } + + public void Remove (Peer peer) + { + if (!peers.TryGetValue (peer.Hash, out InlineFirstRestBucket values)) + return; + + for (int i = values.Count - 1; i >= 0; i--) { + if (ReferenceEquals (values [i].Target, peer)) { + values.RemoveAt (i); + } + } + if (values.Count == 0) + peers.Remove (peer.Hash); + else + peers [peer.Hash] = values; + } +} + +struct InlineFirstRestBucket +{ + ReferenceTrackingHandle first; + List rest; + + public InlineFirstRestBucket (Peer peer) + { + first = new ReferenceTrackingHandle (peer); + rest = null; + } + + public int Count => (first.IsValid ? 1 : 0) + (rest?.Count ?? 0); + + public ReferenceTrackingHandle this [int index] { + get => index == 0 ? first : rest [index - 1]; + } + + public void Add (Peer peer) + { + if (!first.IsValid) { + first = new ReferenceTrackingHandle (peer); + return; + } + + rest ??= []; + rest.Add (new ReferenceTrackingHandle (peer)); + } + + public void RemoveAt (int index) + { + if (index == 0) { + first = default; + if (rest?.Count > 0) { + first = rest [0]; + rest.RemoveAt (0); + } + } else { + rest?.RemoveAt (index - 1); + } + + if (rest?.Count == 0) + rest = null; + } +} + +// Matches the two managed pointer-sized fields in the runtime's ReferenceTrackingHandle. +readonly struct ReferenceTrackingHandle +{ + public ReferenceTrackingHandle (Peer target) + { + Target = target; + Context = (nint) target.Id; + } + + public Peer Target { get; } + + public nint Context { get; } + + public bool IsValid => Target != null; +} + +sealed class Peer +{ + public Peer (int hash, int id) + { + Hash = hash; + Id = id; + } + + public int Hash { get; } + + public int Id { get; } + + public static Peer[] CreateUnique (int count) + { + var peers = new Peer [count]; + for (int i = 0; i < peers.Length; i++) { + peers [i] = new Peer (i, i); + } + return peers; + } + + public static Peer[] CreateCollisions (int hashCount, int peersPerHash) + { + var peers = new Peer [hashCount * peersPerHash]; + for (int hash = 0; hash < hashCount; hash++) { + for (int id = 0; id < peersPerHash; id++) { + peers [(hash * peersPerHash) + id] = new Peer (hash, id); + } + } + return peers; + } +}