diff --git a/external/Java.Interop/Documentation/EventPipeInteropEvents.md b/external/Java.Interop/Documentation/EventPipeInteropEvents.md new file mode 100644 index 00000000000..6daf8cc0122 --- /dev/null +++ b/external/Java.Interop/Documentation/EventPipeInteropEvents.md @@ -0,0 +1,69 @@ +# Java.Interop EventPipe interop events + +The .NET ↔ Java interop layer emits diagnostics through three providers: + +- `Java.Interop` emits events from the shared Java interop layer. +- `Microsoft.Android.Runtime` emits events from the Android runtime layer. +- `Microsoft.Android.Runtime.InteropMetrics` emits aggregate GC-bridge EventCounters 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. + +## Collecting aggregate bridge metrics + +Use `dotnet-counters` to enable the aggregate GC-bridge metrics without turning on the fine-grained lifecycle/reachability events: + +```bash +dotnet-counters monitor --process-id --counters Microsoft.Android.Runtime.InteropMetrics +``` + +The aggregate counter provider emits: + +- `managed-objects-only-reachable-from-java` +- `java-objects-only-reachable-from-managed` + +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/InteropCounterEventSource.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/InteropCounterEventSource.cs new file mode 100644 index 00000000000..276fd9ca6c5 --- /dev/null +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/InteropCounterEventSource.cs @@ -0,0 +1,140 @@ +#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 InteropCounterEventSource + { +#if INSIDE_MONO_ANDROID_RUNTIME + internal const string ProviderName = "Microsoft.Android.Runtime.InteropMetrics"; +#else + internal const string ProviderName = "Java.Interop.InteropMetrics"; +#endif + + internal const string ManagedObjectsOnlyReachableFromJavaCounterName = "managed-objects-only-reachable-from-java"; + internal const string JavaObjectsOnlyReachableFromManagedCounterName = "java-objects-only-reachable-from-managed"; + + static readonly InteropCounterEventSourceImplementation source = new (); + + internal static bool AreBridgeProcessingCountersEnabled () + { + return source.BridgeProcessingCountersEnabled; + } + + internal static void ReportBridgeProcessingMetrics ( + int managedObjectsOnlyReachableFromJavaCount, + int javaObjectsOnlyReachableFromManagedCount) + { + source.ReportBridgeProcessingMetrics ( + managedObjectsOnlyReachableFromJavaCount, + javaObjectsOnlyReachableFromManagedCount); + } + + [EventSource (Name = ProviderName)] + sealed class InteropCounterEventSourceImplementation : EventSource + { + const string EventCounterIntervalSecArgumentName = "EventCounterIntervalSec"; + + EventCounter? managedObjectsOnlyReachableFromJavaCounter; + EventCounter? javaObjectsOnlyReachableFromManagedCounter; + readonly object countersLock = new (); + + volatile bool bridgeProcessingCountersEnabled; + + internal bool BridgeProcessingCountersEnabled => bridgeProcessingCountersEnabled; + + [NonEvent] + protected override void OnEventCommand (EventCommandEventArgs command) + { + base.OnEventCommand (command); + + if (command.Command == EventCommand.Disable) { + lock (countersLock) { + bridgeProcessingCountersEnabled = false; + DisposeBridgeProcessingCounters (); + } + return; + } + + if (command.Command != EventCommand.Enable || command.Arguments == null || !command.Arguments.ContainsKey (EventCounterIntervalSecArgumentName)) { + return; + } + + lock (countersLock) { + EnsureBridgeProcessingCountersInitialized (); + bridgeProcessingCountersEnabled = true; + } + } + + [NonEvent] + internal void ReportBridgeProcessingMetrics ( + int managedObjectsOnlyReachableFromJavaCount, + int javaObjectsOnlyReachableFromManagedCount) + { + lock (countersLock) { + if (!bridgeProcessingCountersEnabled) { + return; + } + + GetManagedObjectsOnlyReachableFromJavaCounter ().WriteMetric (managedObjectsOnlyReachableFromJavaCount); + GetJavaObjectsOnlyReachableFromManagedCounter ().WriteMetric (javaObjectsOnlyReachableFromManagedCount); + } + } + + [NonEvent] + void EnsureBridgeProcessingCountersInitialized () + { + managedObjectsOnlyReachableFromJavaCounter ??= CreateCounter ( + ManagedObjectsOnlyReachableFromJavaCounterName, + ".NET objects only reachable from Java"); + javaObjectsOnlyReachableFromManagedCounter ??= CreateCounter ( + JavaObjectsOnlyReachableFromManagedCounterName, + "Java objects only reachable from .NET"); + } + + [NonEvent] + EventCounter CreateCounter (string name, string displayName) + { + return new EventCounter (name, this) { + DisplayName = displayName, + DisplayUnits = "count", + }; + } + + [NonEvent] + EventCounter GetManagedObjectsOnlyReachableFromJavaCounter () + { + if (managedObjectsOnlyReachableFromJavaCounter == null) { + throw new InvalidOperationException ($"{ManagedObjectsOnlyReachableFromJavaCounterName} counter was not initialized."); + } + + return managedObjectsOnlyReachableFromJavaCounter; + } + + [NonEvent] + EventCounter GetJavaObjectsOnlyReachableFromManagedCounter () + { + if (javaObjectsOnlyReachableFromManagedCounter == null) { + throw new InvalidOperationException ($"{JavaObjectsOnlyReachableFromManagedCounterName} counter was not initialized."); + } + + return javaObjectsOnlyReachableFromManagedCounter; + } + + [NonEvent] + void DisposeBridgeProcessingCounters () + { + managedObjectsOnlyReachableFromJavaCounter?.Dispose (); + managedObjectsOnlyReachableFromJavaCounter = null; + javaObjectsOnlyReachableFromManagedCounter?.Dispose (); + javaObjectsOnlyReachableFromManagedCounter = null; + } + } + } +} 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..fc90c8e215b --- /dev/null +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/InteropEventSource.cs @@ -0,0 +1,197 @@ +#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..a3a6bf05424 --- /dev/null +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InteropEventSourceTests.cs @@ -0,0 +1,190 @@ +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Threading; + +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)); + Assert.DoesNotThrow (() => InteropCounterEventSource.ReportBridgeProcessingMetrics (1, 2)); + } + + [Test] + public void BridgeProcessingCounters_CanBeEnabledSeparately () + { + using (var listener = new CounterCapturingEventListener ()) { + bool countersEnabled = SpinWait.SpinUntil (() => InteropCounterEventSource.AreBridgeProcessingCountersEnabled (), TimeSpan.FromSeconds (5)); + Assert.IsTrue (countersEnabled, "Expected bridge processing counters to be enabled."); + Assert.IsFalse (InteropEventSource.IsEnabled (InteropEventSource.PeerLifecycleKeyword), "Did not expect the fine-grained interop event source to be enabled."); + + InteropCounterEventSource.ReportBridgeProcessingMetrics (11, 12); + + bool receivedCounters = SpinWait.SpinUntil (() => listener.CounterValues.Count == 2, TimeSpan.FromSeconds (5)); + Assert.IsTrue (receivedCounters, "Expected all bridge processing counters to be emitted."); + AssertCounterValue (listener, InteropCounterEventSource.ManagedObjectsOnlyReachableFromJavaCounterName, 11); + AssertCounterValue (listener, InteropCounterEventSource.JavaObjectsOnlyReachableFromManagedCounterName, 12); + } + } + + 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]); + } + + static void AssertCounterValue (CounterCapturingEventListener listener, string counterName, double expectedValue) + { + Assert.IsTrue (listener.CounterValues.TryGetValue (counterName, out double actualValue), $"Expected counter '{counterName}' to be present."); + Assert.AreEqual (expectedValue, actualValue, 0.001d); + } + + 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)); + } + } + + sealed class CounterCapturingEventListener : EventListener + { + public ConcurrentDictionary CounterValues { get; } = new ConcurrentDictionary (StringComparer.Ordinal); + + protected override void OnEventSourceCreated (EventSource eventSource) + { + if (eventSource.Name == InteropCounterEventSource.ProviderName) { + EnableEvents ( + eventSource, + EventLevel.Verbose, + EventKeywords.None, + new Dictionary { + ["EventCounterIntervalSec"] = "1", + }); + } + } + + protected override void OnEventWritten (EventWrittenEventArgs eventData) + { + if (eventData.EventName != "EventCounters") { + return; + } + + if (eventData.Payload == null || eventData.Payload.Count == 0 || eventData.Payload [0] is not IEnumerable> payload) { + return; + } + + string? counterName = null; + double? mean = null; + foreach (var entry in payload) { + if (entry.Key == "Name") { + counterName = entry.Value as string; + } else if (entry.Key == "Mean" && entry.Value is double value) { + mean = value; + } + } + + if (counterName != null && mean.HasValue) { + CounterValues [counterName] = mean.Value; + } + } + } + } +} diff --git a/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj b/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj index a73aaea64d9..6ec280643e1 100644 --- a/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj +++ b/src/Mono.Android.Runtime/Mono.Android.Runtime.csproj @@ -70,6 +70,8 @@ + + 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..05e16f79dbf 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,17 +535,33 @@ static unsafe void BridgeProcessingFinished (MarkCrossReferencesArgs* mcr) throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); } - ReadOnlySpan handlesToFree = ProcessCollectedContexts (mcr); + bool captureBridgeProcessingMetrics = InteropCounterEventSource.AreBridgeProcessingCountersEnabled (); + List? reachabilityEvents = + RuntimeFeature.IsInteropEventSourceEnabled (InteropEventSource.ReachabilityKeyword) ? [] : null; + ReadOnlySpan handlesToFree = ProcessCollectedContexts (mcr, reachabilityEvents, captureBridgeProcessingMetrics, out BridgeProcessingMetrics metrics); // 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 (captureBridgeProcessingMetrics) { + InteropCounterEventSource.ReportBridgeProcessingMetrics ( + metrics.ManagedObjectsOnlyReachableFromJavaCount, + metrics.JavaObjectsOnlyReachableFromManagedCount); + } + + if (reachabilityEvents != null) { + foreach (ReachabilityEventData eventData in reachabilityEvents) { + EmitReachabilityEvent (eventData); + } + } } - static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) + static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr, List? reachabilityEvents, bool captureBridgeProcessingMetrics, out BridgeProcessingMetrics metrics) { List handlesToFree = []; + BridgeProcessingMetrics currentMetrics = default; // This call site is reachable on all platforms. 'MarkCrossReferencesArgs.ComponentCount' is only supported on: 'android'. // This call site is reachable on all platforms. 'MarkCrossReferencesArgs.Components' is only supported on: 'android'. @@ -435,24 +572,36 @@ 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 (captureBridgeProcessingMetrics) { + currentMetrics = currentMetrics.WithContext (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 @@ -463,7 +612,33 @@ void ProcessContext (HandleContext* context) handlesToFree.Add (handle); } + metrics = currentMetrics; return CollectionsMarshal.AsSpan (handlesToFree); } + readonly struct BridgeProcessingMetrics + { + public int ManagedObjectsOnlyReachableFromJavaCount { get; } + public int JavaObjectsOnlyReachableFromManagedCount { get; } + + public BridgeProcessingMetrics ( + int managedObjectsOnlyReachableFromJavaCount, + int javaObjectsOnlyReachableFromManagedCount) + { + ManagedObjectsOnlyReachableFromJavaCount = managedObjectsOnlyReachableFromJavaCount; + JavaObjectsOnlyReachableFromManagedCount = javaObjectsOnlyReachableFromManagedCount; + } + + public BridgeProcessingMetrics WithContext (bool isCollected) + { + return isCollected + ? new BridgeProcessingMetrics ( + ManagedObjectsOnlyReachableFromJavaCount, + JavaObjectsOnlyReachableFromManagedCount + 1) + : new BridgeProcessingMetrics ( + ManagedObjectsOnlyReachableFromJavaCount + 1, + JavaObjectsOnlyReachableFromManagedCount); + } + } + } 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