diff --git a/external/Java.Interop/Documentation/EventPipeInteropEvents.md b/external/Java.Interop/Documentation/EventPipeInteropEvents.md new file mode 100644 index 00000000000..712af9ed70c --- /dev/null +++ b/external/Java.Interop/Documentation/EventPipeInteropEvents.md @@ -0,0 +1,55 @@ +# Java.Interop EventPipe interop events + +The .NET ↔ Java interop layer emits EventPipe events through two providers: + +- `Java.Interop` emits events from the shared Java interop layer. +- `Microsoft.Android.Runtime` emits events from the Android runtime layer. + +## Event catalog + +| Event ID | Event name | Meaning | +|---|---|---| +| 1 | `ManagedPeerCreated` | A managed peer for a Java peer was created. | +| 2 | `JavaPeerCreated` | A Java peer for a managed peer was created. | +| 3 | `ManagedPeerReleasedJavaPeer` | A managed peer released its Java peer. | +| 4 | `JavaPeerReleasedManagedPeer` | A Java peer released its managed peer. | +| 5 | `ManagedPeerOnlyReachableFromJavaPeer` | A managed peer is only reachable from its Java peer during bridge processing. | +| 6 | `JavaPeerOnlyReachableFromManagedPeer` | A Java peer is only reachable from its managed peer during bridge processing. | + +## Payload schema + +All events contain: + +- `managedType` (`string`) +- `javaType` (`string`) +- `jniIdentityHashCode` (`int`) +- `managedObjectHashCode` (`int`) +- `runtimeFlavor` (`string`): `MonoVM`, `CoreCLR`, `NativeAOT`, or `Unknown` + +Reachability events (`5`, `6`) additionally contain: + +- `componentIndex` (`int`) +- `contextIndex` (`int`) +- `contextPointer` (`long`) + +## Enabling events + +Interop events are disabled by default so that unused instrumentation can be removed by trimming. Enable them in the application project: + +```xml + + <_AndroidEnableInteropEventSource>true + +``` + +## Collecting events + +Use `dotnet-trace` to capture both providers: + +```bash +dotnet-trace collect --process-id --providers Java.Interop:0x3:4,Microsoft.Android.Runtime:0x3:4 +``` + +`0x3` enables both peer lifecycle and reachability keywords, and `4` enables informational-level events. + +These structured events supplement the existing text-based global and local JNI reference logs; they do not replace them. diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/InteropEventSource.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/InteropEventSource.cs new file mode 100644 index 00000000000..425745d47c7 --- /dev/null +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/InteropEventSource.cs @@ -0,0 +1,198 @@ +#nullable enable + +using System; +using System.Diagnostics.Tracing; + +#if INSIDE_MONO_ANDROID_RUNTIME +namespace Microsoft.Android.Runtime +#else +namespace Java.Interop +#endif +{ + internal static class InteropEventSource + { +#if INSIDE_MONO_ANDROID_RUNTIME + internal const string ProviderName = "Microsoft.Android.Runtime"; +#else + internal const string ProviderName = "Java.Interop"; +#endif + const string UnknownValue = "Unknown"; + const string RuntimeFeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; + + internal const EventKeywords PeerLifecycleKeyword = (EventKeywords) 0x1; + internal const EventKeywords ReachabilityKeyword = (EventKeywords) 0x2; + + static readonly InteropEventSourceImplementation source = new InteropEventSourceImplementation (); + + internal static bool IsEnabled (EventKeywords keywords) + { + return source.IsEnabled (EventLevel.Informational, keywords); + } + + internal static void ManagedPeerCreated ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode) + { + source.ManagedPeerCreated ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor ()); + } + + internal static void JavaPeerCreated ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode) + { + source.JavaPeerCreated ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor ()); + } + + internal static void ManagedPeerReleasedJavaPeer ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode) + { + source.ManagedPeerReleasedJavaPeer ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor ()); + } + + internal static void JavaPeerReleasedManagedPeer ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode) + { + source.JavaPeerReleasedManagedPeer ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor ()); + } + + internal static void ManagedPeerOnlyReachableFromJavaPeer ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode, + int componentIndex, + int contextIndex, + long contextPointer) + { + source.ManagedPeerOnlyReachableFromJavaPeer ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor (), + componentIndex, + contextIndex, + contextPointer); + } + + internal static void JavaPeerOnlyReachableFromManagedPeer ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode, + int componentIndex, + int contextIndex, + long contextPointer) + { + source.JavaPeerOnlyReachableFromManagedPeer ( + GetPayloadValue (managedType), + GetPayloadValue (javaType), + jniIdentityHashCode, + managedObjectHashCode, + GetRuntimeFlavor (), + componentIndex, + contextIndex, + contextPointer); + } + + static string GetPayloadValue (string? value) + { + return value ?? UnknownValue; + } + + static string GetRuntimeFlavor () + { + if (IsRuntimeFeatureEnabled ("IsNativeAotRuntime")) { + return "NativeAOT"; + } + if (IsRuntimeFeatureEnabled ("IsCoreClrRuntime")) { + return "CoreCLR"; + } + if (IsRuntimeFeatureEnabled ("IsMonoRuntime")) { + return "MonoVM"; + } + return UnknownValue; + } + + static bool IsRuntimeFeatureEnabled (string feature) + { + return AppContext.TryGetSwitch ($"{RuntimeFeatureSwitchPrefix}{feature}", out bool isEnabled) && isEnabled; + } + + [EventSource (Name = ProviderName)] + sealed class InteropEventSourceImplementation : EventSource + { + public static class Keywords + { + public const EventKeywords PeerLifecycle = PeerLifecycleKeyword; + public const EventKeywords Reachability = ReachabilityKeyword; + } + + [Event (1, Level = EventLevel.Informational, Keywords = Keywords.PeerLifecycle)] + public void ManagedPeerCreated (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor) + { + WriteEvent (1, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor); + } + + [Event (2, Level = EventLevel.Informational, Keywords = Keywords.PeerLifecycle)] + public void JavaPeerCreated (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor) + { + WriteEvent (2, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor); + } + + [Event (3, Level = EventLevel.Informational, Keywords = Keywords.PeerLifecycle)] + public void ManagedPeerReleasedJavaPeer (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor) + { + WriteEvent (3, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor); + } + + [Event (4, Level = EventLevel.Informational, Keywords = Keywords.PeerLifecycle)] + public void JavaPeerReleasedManagedPeer (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor) + { + WriteEvent (4, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor); + } + + [Event (5, Level = EventLevel.Informational, Keywords = Keywords.Reachability)] + public void ManagedPeerOnlyReachableFromJavaPeer (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor, int componentIndex, int contextIndex, long contextPointer) + { + WriteEvent (5, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor, componentIndex, contextIndex, contextPointer); + } + + [Event (6, Level = EventLevel.Informational, Keywords = Keywords.Reachability)] + public void JavaPeerOnlyReachableFromManagedPeer (string managedType, string javaType, int jniIdentityHashCode, int managedObjectHashCode, string runtimeFlavor, int componentIndex, int contextIndex, long contextPointer) + { + WriteEvent (6, managedType, javaType, jniIdentityHashCode, managedObjectHashCode, runtimeFlavor, componentIndex, contextIndex, contextPointer); + } + } + } +} diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs index a5383701a1e..a43b170135b 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs @@ -116,6 +116,14 @@ public virtual void DisposePeer (IJavaPeerable value) var h = value.PeerReference; if (!h.IsValid) return; + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (h); + InteropEventSource.ManagedPeerReleasedJavaPeer ( + value.GetType ().FullName, + javaType, + value.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (value)); + } DisposePeer (h, value); } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs index 56f836bd2e3..6ca77b04681 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs @@ -78,6 +78,9 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe peer.SetPeerReference (newRef); peer.SetJniIdentityHashCode (JniSystem.IdentityHashCode (newRef)); + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + EmitJavaPeerCreatedEvent (peer, newRef); + } var o = Runtime.ObjectReferenceManager; if (o.LogGlobalReferenceMessages) { @@ -94,6 +97,27 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe } } + void EmitManagedPeerCreatedEvent (IJavaPeerable peer) + { + JniObjectReference reference = peer.PeerReference; + var javaType = reference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (reference) : null; + InteropEventSource.ManagedPeerCreated ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } + + void EmitJavaPeerCreatedEvent (IJavaPeerable peer, JniObjectReference reference) + { + var javaType = reference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (reference) : null; + InteropEventSource.JavaPeerCreated ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } + // This base method implementation is NOT reachable in trimmable typemap - it is featureswitch guarded public override IJavaPeerable? CreatePeer ( ref JniObjectReference reference, @@ -143,6 +167,9 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); } peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + EmitManagedPeerCreatedEvent (peer); + } return peer; } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs index d026789a180..e874dd7a4ec 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; @@ -105,6 +106,14 @@ static void Construct ( var typeSig = new JniTypeSignature (JniEnvironment.Types.GetJniTypeNameFromInstance (r_self)); var type = GetTypeFromSignature (runtime.TypeManager, typeSig); + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + var managedObjectHashCode = self != null ? RuntimeHelpers.GetHashCode (self) : 0; + InteropEventSource.JavaPeerCreated ( + type.FullName, + typeSig.SimpleReference, + runtime.ValueManager.GetJniIdentityHashCode (r_self), + managedObjectHashCode); + } if (type.IsGenericTypeDefinition) { throw new NotSupportedException ( diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/RuntimeFeature.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/RuntimeFeature.cs index afc27a26d32..3a096f64c3a 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/RuntimeFeature.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/RuntimeFeature.cs @@ -1,11 +1,13 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; namespace Java.Interop { static class RuntimeFeature { const bool ManagedPeerNativeRegistrationEnabledByDefault = true; + const bool InteropEventSourceEnabledByDefault = false; const string FeatureSwitchPrefix = "Java.Interop.RuntimeFeature."; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedPeerNativeRegistration)}")] @@ -14,5 +16,16 @@ static class RuntimeFeature AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedPeerNativeRegistration)}", out bool isEnabled) ? isEnabled : ManagedPeerNativeRegistrationEnabledByDefault; + + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (InteropEventSource)}")] + internal static bool InteropEventSource { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (InteropEventSource)}", out bool isEnabled) + ? isEnabled + : InteropEventSourceEnabledByDefault; + + internal static bool IsInteropEventSourceEnabled (EventKeywords keywords) + { + return InteropEventSource && global::Java.Interop.InteropEventSource.IsEnabled (keywords); + } } } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InteropEventSourceTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InteropEventSourceTests.cs new file mode 100644 index 00000000000..7946340e6cc --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InteropEventSourceTests.cs @@ -0,0 +1,121 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + public class InteropEventSourceTests + { + [Test] + public void PeerLifecycleEvents_HaveExpectedPayload () + { + using (var listener = new CapturingEventListener ()) { + InteropEventSource.ManagedPeerCreated ("Managed.Type", "java/type", 1, 2); + InteropEventSource.JavaPeerCreated ("Managed.Type", "java/type", 3, 4); + InteropEventSource.ManagedPeerReleasedJavaPeer ("Managed.Type", "java/type", 5, 6); + InteropEventSource.JavaPeerReleasedManagedPeer ("Managed.Type", "java/type", 7, 8); + + var lifecycleEvents = listener.Events.Where (e => e.EventId is >= 1 and <= 4).ToArray (); + Assert.AreEqual (4, lifecycleEvents.Length, "Expected all lifecycle events to be emitted."); + + AssertEventPayload (lifecycleEvents [0], "ManagedPeerCreated", "Managed.Type", "java/type", 1, 2, "Unknown"); + AssertEventPayload (lifecycleEvents [1], "JavaPeerCreated", "Managed.Type", "java/type", 3, 4, "Unknown"); + AssertEventPayload (lifecycleEvents [2], "ManagedPeerReleasedJavaPeer", "Managed.Type", "java/type", 5, 6, "Unknown"); + AssertEventPayload (lifecycleEvents [3], "JavaPeerReleasedManagedPeer", "Managed.Type", "java/type", 7, 8, "Unknown"); + } + } + + [Test] + public void ReachabilityEvents_HaveExpectedPayload () + { + using (var listener = new CapturingEventListener ()) { + InteropEventSource.ManagedPeerOnlyReachableFromJavaPeer ("Managed.Type", "java/type", 11, 12, 2, 3, 16); + InteropEventSource.JavaPeerOnlyReachableFromManagedPeer ("Managed.Type", "java/type", 21, 22, 4, 5, 32); + + var reachabilityEvents = listener.Events.Where (e => e.EventId is >= 5 and <= 6).ToArray (); + Assert.AreEqual (2, reachabilityEvents.Length, "Expected both reachability events to be emitted."); + + AssertReachabilityPayload (reachabilityEvents [0], "ManagedPeerOnlyReachableFromJavaPeer", "Managed.Type", "java/type", 11, 12, "Unknown", 2, 3, 16L); + AssertReachabilityPayload (reachabilityEvents [1], "JavaPeerOnlyReachableFromManagedPeer", "Managed.Type", "java/type", 21, 22, "Unknown", 4, 5, 32L); + } + } + + [Test] + public void CallsWithoutListener_DoNotThrow () + { + Assert.DoesNotThrow (() => InteropEventSource.ManagedPeerCreated ("Managed.Type", "java/type", 1, 2)); + Assert.DoesNotThrow (() => InteropEventSource.JavaPeerCreated ("Managed.Type", "java/type", 1, 2)); + Assert.DoesNotThrow (() => InteropEventSource.ManagedPeerReleasedJavaPeer ("Managed.Type", "java/type", 1, 2)); + Assert.DoesNotThrow (() => InteropEventSource.JavaPeerReleasedManagedPeer ("Managed.Type", "java/type", 1, 2)); + Assert.DoesNotThrow (() => InteropEventSource.ManagedPeerOnlyReachableFromJavaPeer ("Managed.Type", "java/type", 1, 2, 1, 1, 1)); + Assert.DoesNotThrow (() => InteropEventSource.JavaPeerOnlyReachableFromManagedPeer ("Managed.Type", "java/type", 1, 2, 1, 1, 1)); + } + + static void AssertEventPayload (CapturedEvent captured, string eventName, string managedType, string javaType, int jniHash, int managedHash, string runtimeFlavor) + { + Assert.AreEqual (eventName, captured.EventName); + Assert.AreEqual (managedType, captured.Payload [0]); + Assert.AreEqual (javaType, captured.Payload [1]); + Assert.AreEqual (jniHash, captured.Payload [2]); + Assert.AreEqual (managedHash, captured.Payload [3]); + Assert.AreEqual (runtimeFlavor, captured.Payload [4]); + } + + static void AssertReachabilityPayload (CapturedEvent captured, string eventName, string managedType, string javaType, int jniHash, int managedHash, string runtimeFlavor, int componentIndex, int contextIndex, long contextPointer) + { + Assert.AreEqual (eventName, captured.EventName); + Assert.AreEqual (managedType, captured.Payload [0]); + Assert.AreEqual (javaType, captured.Payload [1]); + Assert.AreEqual (jniHash, captured.Payload [2]); + Assert.AreEqual (managedHash, captured.Payload [3]); + Assert.AreEqual (runtimeFlavor, captured.Payload [4]); + Assert.AreEqual (componentIndex, captured.Payload [5]); + Assert.AreEqual (contextIndex, captured.Payload [6]); + Assert.AreEqual (contextPointer, captured.Payload [7]); + } + + readonly struct CapturedEvent + { + public string EventName { get; } + public int EventId { get; } + public object?[] Payload { get; } + + public CapturedEvent (string eventName, int eventId, object?[] payload) + { + EventName = eventName; + EventId = eventId; + Payload = payload; + } + } + + sealed class CapturingEventListener : EventListener + { + public List Events { get; } = new List (); + + protected override void OnEventSourceCreated (EventSource eventSource) + { + if (eventSource.Name == InteropEventSource.ProviderName) { + EnableEvents (eventSource, EventLevel.Verbose, EventKeywords.All); + } + } + + protected override void OnEventWritten (EventWrittenEventArgs eventData) + { + if (eventData.EventName == null) { + return; + } + + var payload = eventData.Payload?.ToArray () ?? Array.Empty (); + Events.Add (new CapturedEvent (eventData.EventName, eventData.EventId, payload)); + } + } + } +} diff --git a/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj b/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj index a73aaea64d9..0f7bff622f9 100644 --- a/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj +++ b/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj @@ -70,6 +70,7 @@ + diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 34bafabbe58..32d5a370209 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -626,6 +626,15 @@ public override void WaitForGCBridgeProcessing () return null; var peer = Java.Interop.TypeManager.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType) as IJavaPeerable; + if (peer != null && RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + var peerReference = peer.PeerReference; + var javaType = peerReference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (peerReference) : null; + InteropEventSource.ManagedPeerCreated ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } JniObjectReference.Dispose (ref reference, options); return peer; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs index cf6f550b5fd..9929d963329 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs @@ -183,6 +183,7 @@ public static void RemovePeer (IJavaPeerable value) if (value == null) throw new ArgumentNullException (nameof (value)); + bool removed = false; lock (RegisteredInstances) { int key = value.JniIdentityHashCode; if (!RegisteredInstances.TryGetValue (key, out List? peers)) @@ -194,12 +195,17 @@ public static void RemovePeer (IJavaPeerable value) if (ReferenceEquals (value, target)) { peers.RemoveAt (i); peer.Dispose (); + removed = true; } GC.KeepAlive (target); } if (peers.Count == 0) RegisteredInstances.Remove (key); } + + if (removed && RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + EmitJavaPeerReleasedManagedPeer (value); + } } public static void FinalizePeer (IJavaPeerable value) @@ -232,6 +238,9 @@ public static void FinalizePeer (IJavaPeerable value) RuntimeHelpers.GetHashCode (value).ToString ("x", CultureInfo.InvariantCulture), value.GetType ().ToString ()); } + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + EmitManagedPeerReleasedJavaPeer (value, h); + } value.SetPeerReference (new JniObjectReference ()); JniObjectReference.Dispose (ref h); value.Finalized (); @@ -255,6 +264,105 @@ public static List GetSurfacedPeers () } } + static void EmitJavaPeerReleasedManagedPeer (IJavaPeerable peer) + { + JniObjectReference reference = peer.PeerReference; + var javaType = reference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (reference) : null; + InteropEventSource.JavaPeerReleasedManagedPeer ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } + + static void EmitManagedPeerReleasedJavaPeer (IJavaPeerable peer, JniObjectReference reference) + { + var javaType = reference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (reference) : null; + InteropEventSource.ManagedPeerReleasedJavaPeer ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } + + unsafe static ReachabilityEventData CaptureReachabilityEvent (HandleContext* context, GCHandle handle, int componentIndex, int contextIndex, bool isCollected) + { + IJavaPeerable? peer = handle.Target as IJavaPeerable; + string? managedType = peer?.GetType ().FullName; + int managedObjectHashCode = peer != null ? RuntimeHelpers.GetHashCode (peer) : 0; + string? javaType = null; + JniObjectReference javaPeerReference = context->JavaPeerReference; + if (javaPeerReference.IsValid) { + javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (javaPeerReference); + } + + return new ReachabilityEventData ( + managedType, + javaType, + context->PeerIdentityHashCode, + managedObjectHashCode, + componentIndex, + contextIndex, + (long) (nint) context, + isCollected); + } + + static void EmitReachabilityEvent (ReachabilityEventData eventData) + { + if (eventData.IsCollected) { + InteropEventSource.JavaPeerOnlyReachableFromManagedPeer ( + eventData.ManagedType, + eventData.JavaType, + eventData.JniIdentityHashCode, + eventData.ManagedObjectHashCode, + eventData.ComponentIndex, + eventData.ContextIndex, + eventData.ContextPointer); + return; + } + + InteropEventSource.ManagedPeerOnlyReachableFromJavaPeer ( + eventData.ManagedType, + eventData.JavaType, + eventData.JniIdentityHashCode, + eventData.ManagedObjectHashCode, + eventData.ComponentIndex, + eventData.ContextIndex, + eventData.ContextPointer); + } + + readonly struct ReachabilityEventData + { + public string? ManagedType { get; } + public string? JavaType { get; } + public int JniIdentityHashCode { get; } + public int ManagedObjectHashCode { get; } + public int ComponentIndex { get; } + public int ContextIndex { get; } + public long ContextPointer { get; } + public bool IsCollected { get; } + + public ReachabilityEventData ( + string? managedType, + string? javaType, + int jniIdentityHashCode, + int managedObjectHashCode, + int componentIndex, + int contextIndex, + long contextPointer, + bool isCollected) + { + ManagedType = managedType; + JavaType = javaType; + JniIdentityHashCode = jniIdentityHashCode; + ManagedObjectHashCode = managedObjectHashCode; + ComponentIndex = componentIndex; + ContextIndex = contextIndex; + ContextPointer = contextPointer; + IsCollected = isCollected; + } + } + unsafe struct ReferenceTrackingHandle : IDisposable { WeakReference _weakReference; @@ -301,6 +409,19 @@ unsafe struct HandleContext IntPtr controlBlock; public int PeerIdentityHashCode => identityHashCode; + public JniObjectReference JavaPeerReference + { + get { + if (controlBlock == IntPtr.Zero) { + return new JniObjectReference (); + } + + IntPtr javaHandle = ((JniObjectReferenceControlBlock*) controlBlock)->handle; + return javaHandle == IntPtr.Zero + ? new JniObjectReference () + : new JniObjectReference (javaHandle, JniObjectReferenceType.Global); + } + } public bool IsCollected { get @@ -414,15 +535,23 @@ static unsafe void BridgeProcessingFinished (MarkCrossReferencesArgs* mcr) throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); } - ReadOnlySpan handlesToFree = ProcessCollectedContexts (mcr); + List? reachabilityEvents = + RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.ReachabilityKeyword) ? [] : null; + ReadOnlySpan handlesToFree = ProcessCollectedContexts (mcr, reachabilityEvents); // This call site is reachable on all platforms. 'JavaMarshal.FinishCrossReferenceProcessing(MarkCrossReferencesArgs*, ReadOnlySpan)' is only supported on: 'android'. #pragma warning disable CA1416 JavaMarshal.FinishCrossReferenceProcessing (mcr, handlesToFree); #pragma warning restore CA1416 + + if (reachabilityEvents != null) { + foreach (ReachabilityEventData eventData in reachabilityEvents) { + EmitReachabilityEvent (eventData); + } + } } - static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) + static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr, List? reachabilityEvents) { List handlesToFree = []; @@ -435,24 +564,32 @@ static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferenc for (int i = 0; (nuint)i < mcr->ComponentCount; i++) { StronglyConnectedComponent component = mcr->Components [i]; for (int j = 0; (nuint)j < component.Count; j++) { - ProcessContext ((HandleContext*)component.Contexts [j]); + ProcessContext ((HandleContext*)component.Contexts [j], i, j); } } #pragma warning restore CA1416 - void ProcessContext (HandleContext* context) + void ProcessContext (HandleContext* context, int componentIndex, int contextIndex) { if (context == null) { throw new ArgumentNullException (nameof (context), "HandleContext should never be null."); } - // Ignore contexts which were not collected - if (!context->IsCollected) { + bool isCollected = context->IsCollected; + if (!isCollected && reachabilityEvents == null) { return; } GCHandle handle = HandleContext.GetAssociatedGCHandle (context); + if (reachabilityEvents != null) { + reachabilityEvents.Add (CaptureReachabilityEvent (context, handle, componentIndex, contextIndex, isCollected)); + } + + // Ignore contexts which were not collected + if (!isCollected) { + return; + } // Note: modifying the RegisteredInstances dictionary while processing the collected contexts // is tricky and can lead to deadlocks, so we remember which contexts were collected and we will free diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 718f0b6f656..79e44f6233e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; namespace Microsoft.Android.Runtime; @@ -12,6 +13,7 @@ static class RuntimeFeature const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; + const bool InteropEventSourceEnabledByDefault = false; const bool ManagedToJavaUsesAssemblyFullNameEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; @@ -46,6 +48,15 @@ static class RuntimeFeature internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (InteropEventSource)}")] + internal static bool InteropEventSource { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (InteropEventSource)}", out bool isEnabled) ? isEnabled : InteropEventSourceEnabledByDefault; + + internal static bool IsInteropEventSourceEnabled (EventKeywords keywords) + { + return InteropEventSource && global::Microsoft.Android.Runtime.InteropEventSource.IsEnabled (keywords); + } + // Enabled for Debug builds, whose string-based typemaps support Fast Deployment without embedding assembly MVIDs. [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedToJavaUsesAssemblyFullName)}")] internal static bool ManagedToJavaUsesAssemblyFullName { get; } = diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index b2344f8fd2a..5bf91ff24f1 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -100,6 +100,14 @@ protected override void ConstructPeerCore ( peer.SetPeerReference (newRef); peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); + if (RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + var javaType = newRef.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (newRef) : null; + InteropEventSource.JavaPeerCreated ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } var o = Runtime.ObjectReferenceManager; if (o.LogGlobalReferenceMessages) { @@ -130,8 +138,18 @@ protected override void ConstructPeerCore ( try { var resolvedTargetType = ResolvePeerType (targetType); - return TrimmableTypeMap.Instance.CreateInstance (reference.Handle, resolvedTargetType) + var peer = TrimmableTypeMap.Instance.CreateInstance (reference.Handle, resolvedTargetType) ?? NotFoundFallback (ref reference, targetType, resolvedTargetType); + if (peer != null && RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.PeerLifecycleKeyword)) { + var peerReference = peer.PeerReference; + var javaType = peerReference.IsValid ? JniEnvironment.Types.GetJniTypeNameFromInstance (peerReference) : null; + InteropEventSource.ManagedPeerCreated ( + peer.GetType ().FullName, + javaType, + peer.JniIdentityHashCode, + RuntimeHelpers.GetHashCode (peer)); + } + return peer; } finally { JniObjectReference.Dispose (ref reference, transfer); } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets index fe4db586982..09ec0762df4 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets @@ -16,6 +16,7 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f On non-trimmed (Debug) builds, default to true so that debug.mono.log=gref continues to work as expected. --> <_AndroidEnableObjectReferenceLogging Condition=" '$(_AndroidEnableObjectReferenceLogging)' == '' And '$(PublishTrimmed)' == 'true' ">false <_AndroidEnableObjectReferenceLogging Condition=" '$(_AndroidEnableObjectReferenceLogging)' == '' And '$(PublishTrimmed)' != 'true' ">true + <_AndroidEnableInteropEventSource Condition=" '$(_AndroidEnableInteropEventSource)' == '' ">false <_AndroidEnableDiagnosticCrashReporting Condition=" '$(_AndroidEnableDiagnosticCrashReporting)' == '' ">true @@ -63,6 +64,14 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f Value="$(_AndroidEnableObjectReferenceLogging)" Trim="true" /> + + ", "()V"); + Assert.AreNotEqual (IntPtr.Zero, ctor, "Failed to resolve java/lang/Object constructor."); + + IntPtr handle = JNIEnv.NewObject (klass, ctor); + Assert.AreNotEqual (IntPtr.Zero, handle, "Failed to create java/lang/Object instance."); + + var wrapper = Java.Lang.Object.GetObject (handle, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (wrapper); + wrapper.Dispose (); + } finally { + JNIEnv.DeleteLocalRef (klass); + } + + Assert.IsTrue (listener.EventNames.Contains ("ManagedPeerCreated"), "Expected ManagedPeerCreated event."); + } + } + + sealed class CapturingEventListener : EventListener + { + public HashSet EventNames { get; } = new HashSet (StringComparer.Ordinal); + + protected override void OnEventSourceCreated (EventSource eventSource) + { + if (eventSource.Name == "Java.Interop" || eventSource.Name == "Microsoft.Android.Runtime") { + EnableEvents (eventSource, EventLevel.Verbose, EventKeywords.All); + } + } + + protected override void OnEventWritten (EventWrittenEventArgs eventData) + { + if (eventData.EventName != null) { + EventNames.Add (eventData.EventName); + } + } + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index ee23c0b6f13..7fe97ef3a23 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -21,6 +21,7 @@ IL2037 true + <_AndroidEnableInteropEventSource>true