You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[EventPipe] Remove thread lock and cleanup ThreadSessionState lifecycle (#118415)
Fixes#111368
This PR removes EventPipe's complex dual-lock synchronization scheme and
integrates ThreadSessionState cleanup as part of event reading logic.
### Thread Lock Removal
- Eliminated the EventPipe thread lock, replacing the complex dual-lock
scheme (thread lock + buffer_manager lock) with a single buffer_manager
lock
- Unified synchronization: Thread list and session_state slot updates
now occur atomically under one lock, eliminating race conditions between
buffer operations and session state management
- Simplified lock-free reads: Removed complex cross-lock coordination
requirements
### ThreadSessionState Lifecycle Cleanup
- Fixed memory leaks: Proper cleanup of ThreadSessionState objects
prevents unbounded growth in long-running sessions
- Unified resource management: ThreadSessionState and buffer_list now
have matching lifetimes, eliminating buffer_list leaks from the previous
cleanup logic
- Eliminated TSS leak scenarios when threads exit before session disable
- Atomic cleanup operations: Single-lock model enables proper cleanup
without cross-lock race conditions
- SequencePoints are updated when a ThreadSessionState is being removed
### Slight Event Reading Optimization
- Added snapshotting of known EventPipeThreadSessionStates to prevent
iterating over Threads whose buffers are guaranteed to not have the
earliest events
## Testing
Using the original repro
```
Throwing and catching 1000 exceptions (warm up JIT)
Throwing and catching 5000000 exceptions
RSS: 51.7MiB, user CPU: 100.4%
20000000 EventPipe events received, 5000000 exceptions thrown
Spawning 5000 short-lived threads
RSS: 60.3MiB, user CPU: 119.0%
20000 EventPipe events received, 5000 exceptions thrown
Throwing and catching 5000000 exceptions
RSS: 54.3MiB, user CPU: 101.5%
20000000 EventPipe events received, 5000000 exceptions thrown
```
## Performance
Adapted
https://github.com/dotnet/diagnostics/tree/main/src/tests/EventPipeStress
to also stress the native in-proc event listener EventPipeSession.
Testing a matrix of Session Type X Thread Count X Exceptions/Thread
Session Type: IPC Streaming, Native In-Proc
Thread Count: 1, 8, 16, 50, 100, 1000
Exceptions/Thread: 1, 2, 100, 1000
Note: The local stress test that I put together primarily tests an
immediate burst in events being written to EventPipe, which doesn't
reflect the behavior of any steady state behavior.
### Native InProc
Baseline vs PR
<img width="2021" height="531" alt="Screenshot 2026-01-14 221654"
src="https://github.com/user-attachments/assets/3bc35c38-fe60-4db0-8210-8d2bdb599dfa"
/>
This PR overall performs better in fewer events dropped, especially
considering the high thread x high exceptions/thread scenarios.
### IPC Streaming
Baseline VS PR
<img width="2010" height="533" alt="Screenshot 2026-01-14 222304"
src="https://github.com/user-attachments/assets/3dc347d8-ab8d-434a-9c42-7ad842f162dc"
/>
This PR performs similarly, with slight drop (0.8%) in events read for
high threads x high exceptions/thread.
---------
Co-authored-by: Noah Falk <[email protected]>
Copy file name to clipboardExpand all lines: src/native/eventpipe/ep-buffer.h
+4-5Lines changed: 4 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -24,9 +24,9 @@
24
24
// isn't writable and read-related methods will assert if it isn't readable. Methods that have no asserts should have immutable results that
25
25
// can be used at any point during the buffer's lifetime. The buffer has no internal locks so it is the caller's responsibility to synchronize
26
26
// their usage.
27
-
// Writing into the buffer and calling convert_to_read_only() is always done with EventPipeThread rt_lock held. The eventual reader thread can do
27
+
// Calling convert_to_read_only() is always done with EventPipeBufferManager lock held, and it yields to concurrent writes. The eventual reader thread can do
28
28
// a few different things to ensure it sees a consistent state:
29
-
// 1) Take the writer's EventPipeThread rt_lock at least once after the last time the writer writes events
29
+
// 1) Take the buffer manager's lock at least once after the last time the writer writes events
30
30
// 2) Use a memory barrier that prevents reader loads from being re-ordered earlier, such as the one that will occur implicitly by evaluating
0 commit comments