Add EventPipe interop instrumentation for wrapper lifecycle and GC-bridge reachability - #12258
Add EventPipe interop instrumentation for wrapper lifecycle and GC-bridge reachability#12258jkoritzinsky with Copilot wants to merge 2 commits into
Conversation
Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces EventPipe instrumentation across the .NET↔Java interop layer to make wrapper lifecycle and GC-bridge reachability transitions observable in production traces via a new Java.Interop EventSource provider.
Changes:
- Added a new
Java.InteropEventSource (InteropEventSource) with stable event IDs/keywords and a documented payload schema. - Instrumented wrapper creation/release in Java.Interop value managers + Mono.Android runtime, and added GC-bridge reachability transition events with correlation fields.
- Added unit tests (Java.Interop) and runtime-facing tests (Mono.Android) plus documentation for capturing with
dotnet-trace.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InteropEventSourceRuntimeTests.cs | Runtime tests validating lifecycle events are emitted on-device/in-runtime. |
| src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs | Emits wrapper creation events for trimmable typemap value manager paths. |
| src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs | Emits wrapper release + GC-bridge reachability events during bridge processing. |
| src/Mono.Android/Android.Runtime/AndroidRuntime.cs | Emits wrapper creation events for AndroidValueManager.CreatePeer. |
| external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InteropEventSourceTests.cs | Unit tests validating EventSource emission and payload shape. |
| external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt | Declares new public API surface for InteropEventSource. |
| external/Java.Interop/src/Java.Interop/Java.Interop/ManagedPeer.cs | Emits JavaWrapperCreated during managed-peer construction from Java. |
| external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.ReflectionJniValueManager.cs | Emits wrapper creation events in reflection-based value manager. |
| external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs | Emits wrapper release events during peer disposal. |
| external/Java.Interop/src/Java.Interop/Java.Interop/InteropEventSource.cs | New provider implementation + event definitions/keywords. |
| external/Java.Interop/Documentation/EventPipeInteropEvents.md | Event catalog, payload schema, and dotnet-trace collection instructions. |
|
|
||
| peer.SetPeerReference (newRef); | ||
| peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); | ||
| if (!peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable) && InteropEventSource.IsEnabled ()) { |
There was a problem hiding this comment.
Can InteropEventSource.IsEnabled () be backed by a trimmer feature switch that is off by default? We have a RuntimeFeature.cs where we enable these.
| if (!peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable) && InteropEventSource.IsEnabled ()) { | ||
| EmitJavaWrapperCreatedEvent (peer, newRef); | ||
| } |
| if (InteropEventSource.IsEnabled ()) { | ||
| EmitJavaWrapperReleasedDotNetReference (value); | ||
| } | ||
| peers.RemoveAt (i); | ||
| peer.Dispose (); | ||
| } |
| public static bool IsEnabled () | ||
| { | ||
| return source.IsEnabled (); | ||
| } |
| string GetRuntimeMode () | ||
| { | ||
| return Runtime.GetType ().FullName ?? "Unknown"; | ||
| } |
| if (InteropEventSource.IsEnabled ()) { | ||
| var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (h); | ||
| InteropEventSource.DotNetWrapperReleasedJavaReference ( | ||
| value.GetType ().FullName, | ||
| javaType, | ||
| value.JniIdentityHashCode, | ||
| RuntimeHelpers.GetHashCode (value), | ||
| Runtime.GetType ().FullName); | ||
| } |
| if (InteropEventSource.IsEnabled ()) { | ||
| var managedObjectHashCode = self != null ? RuntimeHelpers.GetHashCode (self) : 0; | ||
| InteropEventSource.JavaWrapperCreated ( | ||
| type.FullName, | ||
| typeSig.SimpleReference, | ||
| runtime.ValueManager.GetJniIdentityHashCode (r_self), | ||
| managedObjectHashCode, | ||
| runtime.GetType ().FullName); | ||
| } |
|
|
||
| namespace Java.Interop | ||
| { | ||
| public static class InteropEventSource |
There was a problem hiding this comment.
Does this need to be public? I'd rather users opt into this logging with settings (either MSBuild property/trimmer switch or Android system property)
And then we'd need no public APIs at all?
| public static void DotNetWrapperCreated ( | ||
| string? managedType, | ||
| string? javaType, | ||
| int jniIdentityHashCode, | ||
| int managedObjectHashCode, | ||
| string? runtimeMode) | ||
| { |
There was a problem hiding this comment.
The naming choices here seem odd, e.g. DotNetWrapper. Can we use pre-existing terminology? You might look in external/java.interop/docs/ for examples.
|
|
||
| peer.SetPeerReference (newRef); | ||
| peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); | ||
| if (!peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable) && InteropEventSource.IsEnabled ()) { |
There was a problem hiding this comment.
Can InteropEventSource.IsEnabled () be backed by a trimmer feature switch that is off by default? We have a RuntimeFeature.cs where we enable these.
| The .NET ↔ Java interop layer emits EventPipe events through a single provider: | ||
|
|
||
| - **Provider name:** `Java.Interop` | ||
|
|
||
| ## Event catalog | ||
|
|
||
| | Event ID | Event name | Meaning | | ||
| |---|---|---| | ||
| | 1 | `DotNetWrapperCreated` | A managed wrapper for a Java object was created. | | ||
| | 2 | `JavaWrapperCreated` | A Java wrapper for a managed object was created. | | ||
| | 3 | `DotNetWrapperReleasedJavaReference` | A managed wrapper released its Java reference. | | ||
| | 4 | `JavaWrapperReleasedDotNetReference` | A Java wrapper released its managed reference. | | ||
| | 5 | `DotNetObjectOnlyReachableFromJava` | A managed object is only reachable from Java during bridge processing. | | ||
| | 6 | `JavaObjectOnlyReachableFromDotNet` | A Java object is only reachable from .NET during bridge processing. | |
There was a problem hiding this comment.
There is some existing code in here for writing gref/lref logs. It is old and text-based. Does this replace or supplement this?
This adds first-class EventPipe visibility into the .NET↔Java interop layer so wrapper lifecycle and cross-runtime reachability transitions can be observed in production traces. The instrumentation covers wrapper creation/release on both sides and GC-bridge reachability state transitions with correlation data.
New interop EventSource provider
Java.InteropDotNetWrapperCreatedJavaWrapperCreatedDotNetWrapperReleasedJavaReferenceJavaWrapperReleasedDotNetReferenceDotNetObjectOnlyReachableFromJavaJavaObjectOnlyReachableFromDotNetWrapper creation instrumentation
.NET wrapper for Java objectevents from:ReflectionJniValueManager.CreatePeerTrimmableTypeMapValueManager.CreatePeerAndroidRuntime.AndroidValueManager.CreatePeerJava wrapper for .NET objectevents from:ManagedPeer.ConstructConstructPeerCorepaths where Java peer references are establishedWrapper release instrumentation
.NET wrapper releases Java referencefrom disposal/finalization paths:JniValueManager.DisposePeerJavaMarshalRegisteredPeers.FinalizePeerJava wrapper releases .NET referencefrom registered-peer removal:JavaMarshalRegisteredPeers.RemovePeerGC-bridge reachability instrumentation
JavaMarshalRegisteredPeersbridge processing:.NET object only reachable from JavaJava object only reachable from .NETcomponentIndex,contextIndex,contextPointer) for trace-side reconstruction.Low-overhead event gating
InteropEventSource.IsEnabled()before payload-heavy work (e.g., Java type resolution) to avoid unnecessary overhead when no listener is attached.API, tests, and docs
InteropEventSource.dotnet-traceusage documentation.