fix(profiling): release the EventPipe session when the SDK shuts down - #5470
Merged
Merged
Conversation
SamplingTransactionProfilerFactory.Dispose() disposed the antecedent Task<SampleProfilerSession> rather than the session it wraps. Fixing that alone changes nothing observable, because two further defects sit between "SDK shuts down" and "EventPipe session released": - SampleProfilerSession.Stop() built _processing as an OnlyOnFaulted continuation, which transitions to Canceled when Process() returns normally - so Wait() threw on every clean shutdown and the disposals after it were never reached. - ProfilingIntegration was not IDisposable, so Hub never registered it for cleanup and the factory's Dispose() was only ever called by tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5470 +/- ##
==========================================
- Coverage 74.87% 74.74% -0.13%
==========================================
Files 513 513
Lines 18659 18829 +170
Branches 3636 3682 +46
==========================================
+ Hits 13970 14074 +104
- Misses 3819 3875 +56
- Partials 870 880 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
jamescrosswell
marked this pull request as ready for review
August 19, 2026 09:24
Both review bots flagged the same window from different angles, and both were right: - The startup task read _shutdownCts.Token, which throws ObjectDisposedException once Dispose() has disposed the CTS. The token is now captured up front so the task never touches the source. - StartEventPipeSession() can block for up to 30s and isn't cancellable, so Dispose() could run before _session was ever assigned - disposing nothing and leaving the session that arrived later running forever. A lock now hands the session between the two sides so exactly one of them stops it. Adds test seams to SampleProfilerSession so the race can be exercised deterministically rather than by timing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e' into fix/5418-profiler-session-dispose # Conflicts: # src/Sentry.Profiling/SamplingTransactionProfilerFactory.cs
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
jamescrosswell
commented
Aug 19, 2026
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Documents the convention in AGENTS.md: this repo favours code that needs no comments, with the code and the PR description as the documentation. Minimal comments only where something genuinely isn't obvious. Applies it to this branch - 34 added comment lines down to 12. What survives is non-intuitive framework behaviour (a ContinueWith whose criteria aren't met is Canceled; reading CancellationTokenSource.Token after Dispose throws) and the justification for an empty catch. Test rationale moved into FluentAssertions "because" strings, where it shows up in failure output instead of sitting in a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e' into fix/5418-profiler-session-dispose
disposedDuringStartup read as "SDK startup". It means Dispose() ran while the EventPipe session was still being created, so disposedWhileSessionStarting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SamplingTransactionProfilerFactory.Dispose()disposed the antecedentTask<SampleProfilerSession>rather than the session it wraps, so theEventPipeSessionwas never released.That line turned out to be the last of three independent defects sitting between "SDK shuts down" and "EventPipe session released" — fixing it alone would not have changed anything observable.
What was broken
SampleProfilerSession.Stop()skipped its own disposals on the happy path._processingwas built withTaskContinuationOptions.OnlyOnFaulted. WheneventSource.Process()returns normally the continuation criteria aren't met, so the continuation transitions toCanceledand_processing.Wait()throwsAggregateException(TaskCanceledException).EventPipeSession.Dispose()andTraceLogEventSource.Dispose()sat after that call and were never reached — thecatchloggedError during sampler profiler session shutdown.and swallowed it. This fired on every clean shutdown, not on some edge case.Nothing ever called
Dispose()on the factory.ProfilingIntegrationwasn'tIDisposable, soHubnever added it to_integrationsToCleanup, and the factory's only other reference isoptions.TransactionProfilerFactory, which nothing disposes. Outside of tests,SamplingTransactionProfilerFactory.Dispose()was unreachable.The reported bug. The
ContinueWithparameter is the antecedent task, soDispose()calledTask.Dispose()— a near no-op on a completed task — instead of disposing the session.What this changes
ProfilingIntegrationnow owns the factory it creates. It implementsIDisposableso the Hub tears it down on shutdown, and disposal also clearsoptions.TransactionProfilerFactory, so aSentryOptionsreused for a later hub gets a working profiler rather than a stale reference. It only touches a factory it created itself — one supplied from elsewhere is left alone.SamplingTransactionProfilerFactory.Dispose()now actually stops the session.StartEventPipeSession()can block for up to 30 seconds and can't be cancelled, so disposal can land before there is any session to stop; a lock hands the session between the startup task andDispose()so exactly one of the two tears it down. The wait for the first event is cancelled on disposal rather than left to block indefinitely. Both waits are bounded at 2 seconds so a wedged session can't hang shutdown.SampleProfilerSession.Stop()moves the disposals into afinally, so the EventPipe connection is released even when stopping the session or draining its events fails.Impact
Low, and narrower than the issue suggests. The session is created once and lives for the SDK's lifetime —
SamplingTransactionProfiler.Stop()only clears_inProgress, it never touches the session — so the only disposal point is SDK shutdown, which is normally process shutdown, where the OS reclaims the handle and theTraceLogregardless. Nothing accumulates while an application runs.It matters where the process outlives the SDK: repeated
Init/Closecycles strand oneEventPipeSessionplus itsTraceLogper cycle, and hosts that close Sentry but keep running never release the pipe.The issue links this to the Windows Service memory growth in #3375. That link doesn't hold up — a service that initialises Sentry once would never reach any of these paths. The steady-state growth reported in #5469 is a separate mechanism and is not addressed here.
Notes
Error during sampler profiler session shutdown.on every clean shutdown with profiling enabled. Worth asking anyone reporting profiler-related resource growth whether they see it.Hub.Dispose()now does real work for profiling users where it previously did none.Sentry.Profiling.TestsisSkip.If(TestEnvironment.IsGitHubActions)because it opens real EventPipe sessions. TheProfilingIntegrationtests added here are plain[Fact]s that do run in CI; the two session-level tests follow the existing skip pattern.AGENTS.mdgains a short section recording the repo's preference for code over comments. Happy to split that into its own PR if it doesn't belong here.Fixes #5418