Skip to content

Commit 5cb67de

Browse files
jamescrosswellvaind
authored andcommitted
Added memory optimisations for GetLastActiveSpan (#2642)
1 parent cecb99d commit 5cb67de

File tree

4 files changed

+114
-5
lines changed

4 files changed

+114
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ API Changes:
3636
- Added distributed tracing without performance for Azure Function Workers ([#2630](https://github.com/getsentry/sentry-dotnet/pull/2630))
3737
- The SDK now provides and overload of `ContinueTrace` that accepts headers as `string` ([#2601](https://github.com/getsentry/sentry-dotnet/pull/2601))
3838
- Sentry tracing middleware now gets configured automatically ([#2602](https://github.com/getsentry/sentry-dotnet/pull/2602))
39+
- Added memory optimisations for GetLastActiveSpan ([#2642](https://github.com/getsentry/sentry-dotnet/pull/2642))
3940

4041
### Fixes
4142

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
``` ini
2+
3+
BenchmarkDotNet=v0.13.5, OS=macOS Ventura 13.5.2 (22G91) [Darwin 22.6.0]
4+
Apple M1, 1 CPU, 8 logical and 8 physical cores
5+
.NET SDK=7.0.306
6+
[Host] : .NET 6.0.19 (6.0.1923.31806), Arm64 RyuJIT AdvSIMD
7+
ShortRun : .NET 6.0.19 (6.0.1923.31806), Arm64 RyuJIT AdvSIMD
8+
9+
Job=ShortRun IterationCount=3 LaunchCount=1
10+
WarmupCount=3
11+
12+
```
13+
| Method | SpanCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
14+
|-------------------------------- |---------- |----------:|------------:|----------:|--------:|--------:|----------:|
15+
| **'Create spans for scope access'** | **1** | **32.34 μs** | **26.58 μs** | **1.457 μs** | **4.7607** | **1.5259** | **16.68 KB** |
16+
| **'Create spans for scope access'** | **10** | **101.28 μs** | **239.09 μs** | **13.105 μs** | **9.1553** | **2.5635** | **39.21 KB** |
17+
| **'Create spans for scope access'** | **100** | **628.84 μs** | **1,267.56 μs** | **69.479 μs** | **59.5703** | **18.5547** | **266.14 KB** |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Perfolizer.Mathematics.Randomization;
3+
4+
namespace Sentry.Benchmarks;
5+
6+
public class LastActiveSpanBenchmarks
7+
{
8+
private const string Operation = "Operation";
9+
private const string Name = "Name";
10+
11+
[Params(1, 10, 100)]
12+
public int SpanCount;
13+
14+
private IDisposable _sdk;
15+
16+
[GlobalSetup(Target = nameof(CreateScopedSpans))]
17+
public void EnabledSdk() => _sdk = SentrySdk.Init(Constants.ValidDsn);
18+
19+
[GlobalCleanup(Target = nameof(CreateScopedSpans))]
20+
public void DisableDsk() => _sdk.Dispose();
21+
22+
[Benchmark(Description = "Create spans for scope access")]
23+
public void CreateScopedSpans()
24+
{
25+
var transaction = SentrySdk.StartTransaction(Name, Operation);
26+
SentrySdk.WithScope(scope =>
27+
{
28+
scope.Transaction = transaction;
29+
for (var i = 0; i < SpanCount; i++)
30+
{
31+
// Simulates a scenario where TransactionTracer.GetLastActiveSpan will be called frequently
32+
// See: https://github.com/getsentry/sentry-dotnet/blob/c2a31b4ead03da388c2db7fe07f290354aa51b9d/src/Sentry/Scope.cs#L567C1-L567C68
33+
CallOneFunction(i);
34+
}
35+
});
36+
transaction.Finish();
37+
}
38+
39+
private void CallOneFunction(int i)
40+
{
41+
var span = SentrySdk.GetSpan()!.StartChild($"One Function {i}");
42+
ThatCallsAnother(i);
43+
span.Finish();
44+
}
45+
46+
private void ThatCallsAnother(int i)
47+
{
48+
var span = SentrySdk.GetSpan()!.StartChild($"Another Function {i}");
49+
AndAnother($"Alternate Description {i}");
50+
span.Finish();
51+
}
52+
53+
private void AndAnother(string description)
54+
{
55+
SentrySdk.ConfigureScope(scope => scope.Span!.Description = description);
56+
}
57+
}

src/Sentry/TransactionTracer.cs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,48 @@ private void AddChildSpan(SpanTracer span)
313313
if (!isOutOfLimit)
314314
{
315315
_spans.Add(span);
316+
_activeSpanTracker.Push(span);
316317
}
317318
}
318319

320+
private class LastActiveSpanTracker
321+
{
322+
private readonly object _lock = new object();
323+
324+
private readonly Lazy<Stack<ISpan>> _trackedSpans = new();
325+
private Stack<ISpan> TrackedSpans => _trackedSpans.Value;
326+
327+
public void Push(ISpan span)
328+
{
329+
lock(_lock)
330+
{
331+
TrackedSpans.Push(span);
332+
}
333+
}
334+
335+
public ISpan? PeekActive()
336+
{
337+
lock(_lock)
338+
{
339+
while (TrackedSpans.Count > 0)
340+
{
341+
// Stop tracking inactive spans
342+
var span = TrackedSpans.Peek();
343+
if (!span.IsFinished)
344+
{
345+
return span;
346+
}
347+
TrackedSpans.Pop();
348+
}
349+
return null;
350+
}
351+
}
352+
}
353+
private readonly LastActiveSpanTracker _activeSpanTracker = new LastActiveSpanTracker();
354+
355+
/// <inheritdoc />
356+
public ISpan? GetLastActiveSpan() => _activeSpanTracker.PeekActive();
357+
319358
/// <inheritdoc />
320359
public void Finish()
321360
{
@@ -375,11 +414,6 @@ public void Finish(Exception exception, SpanStatus status)
375414
public void Finish(Exception exception) =>
376415
Finish(exception, SpanStatusConverter.FromException(exception));
377416

378-
/// <inheritdoc />
379-
public ISpan? GetLastActiveSpan() =>
380-
// We need to sort by timestamp because the order of ConcurrentBag<T> is not deterministic
381-
Spans.OrderByDescending(x => x.StartTimestamp).FirstOrDefault(s => !s.IsFinished);
382-
383417
/// <inheritdoc />
384418
public SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled);
385419
}

0 commit comments

Comments
 (0)