Skip to content

Commit dc4dac2

Browse files
UnsampledTransactions to reduce memory pressure
Added a light weight `UnsampledTransaction` class to be used instead of a full blown `TransactionTracer` when transactions are not sampled. The aim is to reduce memory pressure when sampling less than 100% of transactions. Replaces #3972: - #3972
1 parent d622ff2 commit dc4dac2

File tree

6 files changed

+201
-57
lines changed

6 files changed

+201
-57
lines changed

src/Sentry/DynamicSamplingContext.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,31 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
198198
replaySession);
199199
}
200200

201+
public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession)
202+
{
203+
// These should already be set on the transaction.
204+
var publicKey = options.ParsedDsn.PublicKey;
205+
var traceId = transaction.TraceId;
206+
var sampled = transaction.IsSampled;
207+
var sampleRate = transaction.SampleRate!.Value;
208+
var sampleRand = transaction.SampleRand;
209+
var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null;
210+
211+
// These two may not have been set yet on the transaction, but we can get them directly.
212+
var release = options.SettingLocator.GetRelease();
213+
var environment = options.SettingLocator.GetEnvironment();
214+
215+
return new DynamicSamplingContext(traceId,
216+
publicKey,
217+
sampled,
218+
sampleRate,
219+
sampleRand,
220+
release,
221+
environment,
222+
transactionName,
223+
replaySession);
224+
}
225+
201226
public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
202227
{
203228
var traceId = propagationContext.TraceId;
@@ -224,6 +249,9 @@ internal static class DynamicSamplingContextExtensions
224249
public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession)
225250
=> DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession);
226251

252+
public static DynamicSamplingContext CreateDynamicSamplingContext(this UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession)
253+
=> DynamicSamplingContext.CreateFromUnsampledTransaction(transaction, options, replaySession);
254+
227255
public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
228256
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession);
229257
}

src/Sentry/Internal/Hub.cs

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -129,61 +129,73 @@ internal ITransactionTracer StartTransaction(
129129
IReadOnlyDictionary<string, object?> customSamplingContext,
130130
DynamicSamplingContext? dynamicSamplingContext)
131131
{
132-
var transaction = new TransactionTracer(this, context)
133-
{
134-
SampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var sampleRand) ?? false
135-
? double.Parse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
136-
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString())
137-
};
138-
139132
// If the hub is disabled, we will always sample out. In other words, starting a transaction
140133
// after disposing the hub will result in that transaction not being sent to Sentry.
141-
// Additionally, we will always sample out if tracing is explicitly disabled.
142-
// Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision
143-
// that may have been already set (i.e.: from a sentry-trace header).
144134
if (!IsEnabled)
145135
{
146-
transaction.IsSampled = false;
147-
transaction.SampleRate = 0.0;
136+
return NoOpTransaction.Instance;
148137
}
149-
else
150-
{
151-
// Except when tracing is disabled, TracesSampler runs regardless of whether a decision
152-
// has already been made, as it can be used to override it.
153-
if (_options.TracesSampler is { } tracesSampler)
154-
{
155-
var samplingContext = new TransactionSamplingContext(
156-
context,
157-
customSamplingContext);
158138

159-
if (tracesSampler(samplingContext) is { } sampleRate)
160-
{
161-
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
162-
transaction.SampleRate = sampleRate;
163-
}
164-
}
139+
double? sampleRate = null;
140+
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscsampleRand) ?? false
141+
? double.Parse(dscsampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
142+
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString());
165143

166-
// Random sampling runs only if the sampling decision hasn't been made already.
167-
if (transaction.IsSampled == null)
144+
// We will always sample out if tracing is explicitly disabled.
145+
// Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision
146+
// that may have been already set (i.e.: from a sentry-trace header).
147+
// Except when tracing is disabled, TracesSampler runs regardless of whether a decision
148+
// has already been made, as it can be used to override it.
149+
if (_options.TracesSampler is { } tracesSampler)
150+
{
151+
var samplingContext = new TransactionSamplingContext(
152+
context,
153+
customSamplingContext);
154+
155+
if (tracesSampler(samplingContext) is { } samplerSampleRate)
168156
{
169-
var sampleRate = _options.TracesSampleRate ?? 0.0;
170-
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
171-
transaction.SampleRate = sampleRate;
157+
sampleRate = samplerSampleRate;
172158
}
159+
}
160+
161+
// If the sampling decision isn't made by a trace sampler then fallback to Random sampling
162+
sampleRate ??= _options.TracesSampleRate ?? 0.0;
163+
var isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
164+
165+
// Make sure there is a replayId (if available) on the provided DSC (if any).
166+
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);
173167

174-
if (transaction.IsSampled is true &&
175-
_options.TransactionProfilerFactory is { } profilerFactory &&
176-
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
168+
if (!isSampled)
169+
{
170+
var unsampledTransaction = new UnsampledTransaction(this, context)
177171
{
178-
// TODO cancellation token based on Hub being closed?
179-
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
180-
}
172+
SampleRate = sampleRate,
173+
SampleRand = sampleRand,
174+
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
175+
};
176+
// If no DSC was provided DSC, create one based on this transaction.
177+
// DSC creation must be done AFTER the sampling decision has been made.
178+
unsampledTransaction.DynamicSamplingContext ??= unsampledTransaction.CreateDynamicSamplingContext(_options, _replaySession);
179+
return unsampledTransaction;
181180
}
182181

183-
// Use the provided DSC (adding the active replayId if necessary), or create one based on this transaction.
182+
var transaction = new TransactionTracer(this, context)
183+
{
184+
IsSampled = true,
185+
SampleRate = sampleRate,
186+
SampleRand = sampleRand,
187+
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
188+
};
189+
// If no DSC was provided DSC, create one based on this transaction.
184190
// DSC creation must be done AFTER the sampling decision has been made.
185-
transaction.DynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession)
186-
?? transaction.CreateDynamicSamplingContext(_options, _replaySession);
191+
transaction.DynamicSamplingContext ??= transaction.CreateDynamicSamplingContext(_options, _replaySession);
192+
193+
if (_options.TransactionProfilerFactory is { } profilerFactory &&
194+
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
195+
{
196+
// TODO cancellation token based on Hub being closed?
197+
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
198+
}
187199

188200
// A sampled out transaction still appears fully functional to the user
189201
// but will be dropped by the client and won't reach Sentry's servers.
@@ -220,7 +232,7 @@ public SentryTraceHeader GetTraceHeader()
220232
public BaggageHeader GetBaggage()
221233
{
222234
var span = GetSpan();
223-
if (span?.GetTransaction() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc })
235+
if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc)
224236
{
225237
return dsc.ToBaggageHeader();
226238
}
@@ -373,9 +385,9 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span)
373385
evt.Contexts.Trace.TraceId = span.TraceId;
374386
evt.Contexts.Trace.ParentSpanId = span.ParentSpanId;
375387

376-
if (span.GetTransaction() is TransactionTracer transactionTracer)
388+
if (span.GetTransaction().GetDynamicSamplingContext() is {} dsc)
377389
{
378-
evt.DynamicSamplingContext = transactionTracer.DynamicSamplingContext;
390+
evt.DynamicSamplingContext = dsc;
379391
}
380392
}
381393

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Sentry.Internal;
2+
3+
internal static class TransactionExtensions
4+
{
5+
public static DynamicSamplingContext? GetDynamicSamplingContext(this ITransactionTracer transaction)
6+
{
7+
if (transaction is UnsampledTransaction unsampledTransaction)
8+
{
9+
return unsampledTransaction.DynamicSamplingContext;
10+
}
11+
if (transaction is TransactionTracer transactionTracer)
12+
{
13+
return transactionTracer.DynamicSamplingContext;
14+
}
15+
return null;
16+
}
17+
}

src/Sentry/Internal/NoOpSpan.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ protected NoOpSpan()
1313
{
1414
}
1515

16-
public SpanId SpanId => SpanId.Empty;
16+
public virtual SpanId SpanId => SpanId.Empty;
1717
public SpanId? ParentSpanId => SpanId.Empty;
18-
public SentryId TraceId => SentryId.Empty;
19-
public bool? IsSampled => default;
18+
public virtual SentryId TraceId => SentryId.Empty;
19+
public virtual bool? IsSampled => default;
2020
public IReadOnlyDictionary<string, string> Tags => ImmutableDictionary<string, string>.Empty;
2121
public IReadOnlyDictionary<string, object?> Extra => ImmutableDictionary<string, object?>.Empty;
2222
public IReadOnlyDictionary<string, object?> Data => ImmutableDictionary<string, object?>.Empty;
2323
public DateTimeOffset StartTimestamp => default;
2424
public DateTimeOffset? EndTimestamp => default;
2525
public bool IsFinished => default;
2626

27-
public string Operation
27+
public virtual string Operation
2828
{
2929
get => string.Empty;
3030
set { }
@@ -42,21 +42,21 @@ public SpanStatus? Status
4242
set { }
4343
}
4444

45-
public ISpan StartChild(string operation) => this;
45+
public virtual ISpan StartChild(string operation) => this;
4646

47-
public void Finish()
47+
public virtual void Finish()
4848
{
4949
}
5050

51-
public void Finish(SpanStatus status)
51+
public virtual void Finish(SpanStatus status)
5252
{
5353
}
5454

55-
public void Finish(Exception exception, SpanStatus status)
55+
public virtual void Finish(Exception exception, SpanStatus status)
5656
{
5757
}
5858

59-
public void Finish(Exception exception)
59+
public virtual void Finish(Exception exception)
6060
{
6161
}
6262

@@ -76,7 +76,7 @@ public void SetData(string key, object? value)
7676
{
7777
}
7878

79-
public SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;
79+
public virtual SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;
8080

8181
public IReadOnlyDictionary<string, Measurement> Measurements => ImmutableDictionary<string, Measurement>.Empty;
8282

src/Sentry/Internal/NoOpTransaction.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ internal class NoOpTransaction : NoOpSpan, ITransactionTracer
77
{
88
public new static ITransactionTracer Instance { get; } = new NoOpTransaction();
99

10-
private NoOpTransaction()
10+
protected NoOpTransaction()
1111
{
1212
}
1313

1414
public SdkVersion Sdk => SdkVersion.Instance;
1515

16-
public string Name
16+
public virtual string Name
1717
{
1818
get => string.Empty;
1919
set { }
@@ -87,7 +87,7 @@ public IReadOnlyList<string> Fingerprint
8787
set { }
8888
}
8989

90-
public IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;
90+
public virtual IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;
9191

9292
public IReadOnlyCollection<Breadcrumb> Breadcrumbs => ImmutableList<Breadcrumb>.Empty;
9393

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Sentry.Extensibility;
2+
3+
namespace Sentry.Internal;
4+
5+
/// <summary>
6+
/// We know already, when starting a transaction, whether it's going to be sampled or not. When it's not sampled, we can
7+
/// avoid lots of unecessary processing. The only thing we need to track is the number of spans that would have been
8+
/// created (the client reports detailing discarded events includes this detail).
9+
/// </summary>
10+
internal sealed class UnsampledTransaction : NoOpTransaction
11+
{
12+
// Although it's a little bit wasteful to create separate individual class instances here when all we're going to
13+
// report to sentry is the span count (in the client report), SDK users may refer to things like
14+
// `ITransaction.Spans.Count`, so we create an actual collection
15+
private readonly ConcurrentBag<ISpan> _spans = [];
16+
private readonly IHub _hub;
17+
private readonly ITransactionContext _context;
18+
private readonly SentryOptions? _options;
19+
20+
public UnsampledTransaction(IHub hub, ITransactionContext context)
21+
{
22+
_hub = hub;
23+
_options = _hub.GetSentryOptions();
24+
_options?.LogDebug("Starting unsampled transaction");
25+
_context = context;
26+
}
27+
28+
internal DynamicSamplingContext? DynamicSamplingContext { get; set; }
29+
30+
public override IReadOnlyCollection<ISpan> Spans => _spans;
31+
32+
public override SpanId SpanId => _context.SpanId;
33+
public override SentryId TraceId => _context.TraceId;
34+
public override bool? IsSampled => false;
35+
36+
public double? SampleRate { get; set; }
37+
38+
public double? SampleRand { get; set; }
39+
40+
public override string Name
41+
{
42+
get => _context.Name;
43+
set { }
44+
}
45+
46+
public override string Operation
47+
{
48+
get => _context.Operation;
49+
set { }
50+
}
51+
52+
public override void Finish()
53+
{
54+
_options?.LogDebug("Finishing unsampled transaction");
55+
56+
// Clear the transaction from the scope
57+
_hub.ConfigureScope(scope => scope.ResetTransaction(this));
58+
59+
// Record the discarded events
60+
var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself
61+
_options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Transaction);
62+
_options?.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.SampleRate, DataCategory.Span, spanCount);
63+
64+
_options?.LogDebug("Finished unsampled transaction");
65+
}
66+
67+
public override void Finish(SpanStatus status) => Finish();
68+
69+
public override void Finish(Exception exception, SpanStatus status) => Finish();
70+
71+
public override void Finish(Exception exception) => Finish();
72+
73+
/// <inheritdoc />
74+
public override SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled);
75+
76+
public override ISpan StartChild(string operation)
77+
{
78+
var span = new UnsampledSpan(this);
79+
_spans.Add(span);
80+
return span;
81+
}
82+
83+
private class UnsampledSpan(UnsampledTransaction transaction) : NoOpSpan
84+
{
85+
public override ISpan StartChild(string operation) => transaction.StartChild(operation);
86+
}
87+
}

0 commit comments

Comments
 (0)