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