Repository navigation
fix: bound profiler memory growth by trimming interned call stacks #5503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
25f23d8
fad203f
9d90d21
05a1957
ef4fa83
ab58925
e3d582a
b7db5ae
a656b43
50c51ef
a9f09c4
129412c
6638df7
21be80c
cac6367
955f72e
137714e
1456cea
8da501d
908f26f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -457,6 +457,122 @@ public void ProfilerIntegration_WithProfilingEnabled_SetsFactory() | |
| Assert.NotNull(hub.Options.TransactionProfilerFactory); | ||
| } | ||
|
|
||
| [SkippableFact] | ||
| public void Profiler_WhenNoProfileRunning_TrimsInternedCallStacks() | ||
| { | ||
| Skip.If(TestEnvironment.IsGitHubActions, "Flaky in CI."); | ||
|
|
||
| // The interning tables grow whether or not a profile is running, so this deliberately never | ||
| // starts one - the trim has to happen off the back of ordinary samples. See #5469. | ||
| var originalMax = SamplingTransactionProfilerFactory.MaxCallStackCount; | ||
| SamplingTransactionProfilerFactory.MaxCallStackCount = 100; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 says
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch... fixed (partially) in 908f26f Also created: That might explain why we've had issues with flaky profiling tests (locally and in CI). |
||
| try | ||
| { | ||
| using var factory = new SamplingTransactionProfilerFactory(_testSentryOptions, TimeSpan.FromSeconds(30)); | ||
| SkipIfFailsInCI(() => factory._sessionTask.Wait(60_000)); | ||
| var traceLog = factory._sessionTask.Result.TraceLog; | ||
|
|
||
| // Produce a variety of stack shapes so the sampler has something to intern. | ||
| var stopwatch = Stopwatch.StartNew(); | ||
| var random = new Random(4242); | ||
| var highWaterMark = 0; | ||
| while (stopwatch.ElapsedMilliseconds < 3_000 && factory.TrimCount == 0) | ||
| { | ||
| RecursiveWork(random.Next(8, 24), random); | ||
| highWaterMark = Math.Max(highWaterMark, traceLog.CallStacks.Count); | ||
| } | ||
|
|
||
| SkipIfFailsInCI(() => | ||
| { | ||
| if (factory.TrimCount == 0) | ||
| { | ||
| throw new Exception($"No trim occurred; call stacks peaked at {highWaterMark}."); | ||
| } | ||
| }); | ||
|
|
||
| // Having trimmed at least once, the table must have been reset rather than left to grow. | ||
| Assert.True(factory.TrimCount > 0, $"Expected at least one trim, call stacks peaked at {highWaterMark}."); | ||
| Assert.True(traceLog.CallStacks.Count < highWaterMark, | ||
| $"Expected the interning table to shrink after trimming, but it is {traceLog.CallStacks.Count} against a peak of {highWaterMark}."); | ||
| } | ||
| finally | ||
| { | ||
| SamplingTransactionProfilerFactory.MaxCallStackCount = originalMax; | ||
| } | ||
| } | ||
|
|
||
| [SkippableFact] | ||
| public void Profiler_WhileProfileRunning_StillTrimsInternedCallStacks() | ||
| { | ||
| Skip.If(TestEnvironment.IsGitHubActions, "Flaky in CI."); | ||
|
|
||
| // A profile is held open for the whole test. Trimming must still happen: profiles can run | ||
| // back to back under load, so gating the trim on "no profile running" would starve it | ||
| // exactly when the tables grow fastest. The profile is then collected to show that trimming | ||
| // mid-profile does not corrupt its output. | ||
| var originalMax = SamplingTransactionProfilerFactory.MaxCallStackCount; | ||
| SamplingTransactionProfilerFactory.MaxCallStackCount = 100; | ||
| try | ||
| { | ||
| using var factory = new SamplingTransactionProfilerFactory(_testSentryOptions, TimeSpan.FromSeconds(30)); | ||
| SkipIfFailsInCI(() => factory._sessionTask.Wait(60_000)); | ||
|
|
||
| var clock = SentryStopwatch.StartNew(); | ||
| var transactionTracer = new TransactionTracer(Substitute.For<IHub>(), "test", ""); | ||
| var sut = factory.Start(transactionTracer, CancellationToken.None) as SamplingTransactionProfiler; | ||
| SkipIfFailsInCI(() => ArgumentNullException.ThrowIfNull(sut)); | ||
| transactionTracer.TransactionProfiler = sut; | ||
|
|
||
| // Discard trims from before the profile started - the tiny budget means the session | ||
| // startup alone can trigger one. From here every trim happens while _inProgress is true. | ||
| factory.TrimCount = 0; | ||
|
|
||
| var stopwatch = Stopwatch.StartNew(); | ||
| var random = new Random(4242); | ||
| while (stopwatch.ElapsedMilliseconds < 5_000 && factory.TrimCount == 0) | ||
| { | ||
| RecursiveWork(random.Next(8, 24), random); | ||
| } | ||
|
|
||
| SkipIfFailsInCI(() => | ||
| { | ||
| if (factory.TrimCount == 0) | ||
| { | ||
| throw new Exception("No trim occurred while a profile was running."); | ||
| } | ||
| }); | ||
| Assert.True(factory.TrimCount > 0, "Expected trimming to happen even while a profile is running."); | ||
|
|
||
| sut!.Finish(); | ||
| var elapsedNanoseconds = (ulong)((clock.CurrentDateTimeOffset - clock.StartDateTimeOffset).TotalMilliseconds * 1_000_000); | ||
| var collectTask = sut.CollectAsync(new SentryTransaction(transactionTracer)); | ||
| collectTask.Wait(); | ||
| ValidateProfile(collectTask.Result.Profile, elapsedNanoseconds); | ||
| } | ||
| finally | ||
| { | ||
| SamplingTransactionProfilerFactory.MaxCallStackCount = originalMax; | ||
| } | ||
| } | ||
|
|
||
| private static long RecursiveWork(int depth, Random random) | ||
| { | ||
| if (depth <= 0) | ||
| { | ||
| double sink = 0; | ||
| for (var i = 1; i < 5_000; i++) | ||
| { | ||
| sink += Math.Sqrt(i); | ||
| } | ||
| return (long)sink; | ||
| } | ||
|
|
||
| // Branch so that different calls produce different stack shapes. | ||
| return (random.Next(2) == 0) | ||
| ? RecursiveWork(depth - 1, random) + depth | ||
| : RecursiveWork(depth - 1, random) - depth; | ||
| } | ||
|
|
||
| [SkippableFact] | ||
| public void Downsampler_ShouldSample_Works() | ||
| { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.