Skip to content

Commit aebd6a2

Browse files
UnsampledTransactions to reduce memory pressure (#4212)
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. Resolves #3636 - #3636
1 parent 24ab509 commit aebd6a2

21 files changed

+555
-309
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Reduced memory pressure when sampling less than 100% of traces/transactions ([#4212](https://github.com/getsentry/sentry-dotnet/pull/4212))
8+
59
### Fixes
610

711
- Support Linux arm64 on Native AOT ([#3700](https://github.com/getsentry/sentry-dotnet/pull/3700))

src/Sentry.AspNetCore/SentryTracingMiddleware.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ public async Task InvokeAsync(HttpContext context)
146146
}
147147
finally
148148
{
149-
if (transaction is not null)
149+
if (transaction is UnsampledTransaction)
150+
{
151+
transaction.Finish();
152+
}
153+
else if (transaction is TransactionTracer tracer)
150154
{
151155
// The Transaction name was altered during the pipeline execution,
152156
// That could be done by user interference or by some Event Capture
@@ -183,15 +187,15 @@ public async Task InvokeAsync(HttpContext context)
183187
if (!string.IsNullOrEmpty(customTransactionName))
184188
{
185189
transaction.Name = $"{method} {customTransactionName}";
186-
((TransactionTracer)transaction).NameSource = TransactionNameSource.Custom;
190+
tracer.NameSource = TransactionNameSource.Custom;
187191
}
188192
else
189193
{
190194
// Finally, fallback to using the URL path.
191195
// e.g. "GET /pets/1"
192196
var path = context.Request.Path;
193197
transaction.Name = $"{method} {path}";
194-
((TransactionTracer)transaction).NameSource = TransactionNameSource.Url;
198+
tracer.NameSource = TransactionNameSource.Url;
195199
}
196200
}
197201

@@ -200,7 +204,7 @@ public async Task InvokeAsync(HttpContext context)
200204
transaction.Finish(status);
201205
}
202206
// Status code not yet changed to 500 but an exception does exist
203-
// so lets avoid passing the misleading 200 down and close only with
207+
// so let's avoid passing the misleading 200 down and close only with
204208
// the exception instance that will be inferred as errored.
205209
else if (status == SpanStatus.Ok)
206210
{

src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private async Task<TransactionContext> StartOrContinueTraceAsync(FunctionContext
9292
if (requestData is null)
9393
{
9494
// not an HTTP trigger
95-
return SentrySdk.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation);
95+
return _hub.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation);
9696
}
9797

9898
var httpMethod = requestData.Method.ToUpperInvariant();

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 & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -129,61 +129,72 @@ 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+
bool? isSampled = null;
140+
double? sampleRate = null;
141+
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscsampleRand) ?? false
142+
? double.Parse(dscsampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
143+
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString());
165144

166-
// Random sampling runs only if the sampling decision hasn't been made already.
167-
if (transaction.IsSampled == null)
145+
// TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it.
146+
if (_options.TracesSampler is { } tracesSampler)
147+
{
148+
var samplingContext = new TransactionSamplingContext(
149+
context,
150+
customSamplingContext);
151+
152+
if (tracesSampler(samplingContext) is { } samplerSampleRate)
168153
{
169-
var sampleRate = _options.TracesSampleRate ?? 0.0;
170-
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
171-
transaction.SampleRate = sampleRate;
154+
// The TracesSampler trumps all other sampling decisions (even the trace header)
155+
sampleRate = samplerSampleRate;
156+
isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
172157
}
158+
}
173159

174-
if (transaction.IsSampled is true &&
175-
_options.TransactionProfilerFactory is { } profilerFactory &&
176-
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
160+
// If the sampling decision isn't made by a trace sampler we check the trace header first (from the context) or
161+
// finally fallback to Random sampling if the decision has been made by no other means
162+
sampleRate ??= _options.TracesSampleRate ?? 0.0;
163+
isSampled ??= context.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);
167+
168+
if (isSampled is false)
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, create one based on this transaction.
177+
// Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
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.
184-
// DSC creation must be done AFTER the sampling decision has been made.
185-
transaction.DynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession)
186-
?? transaction.CreateDynamicSamplingContext(_options, _replaySession);
182+
var transaction = new TransactionTracer(this, context)
183+
{
184+
SampleRate = sampleRate,
185+
SampleRand = sampleRand,
186+
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
187+
};
188+
// If no DSC was provided, create one based on this transaction.
189+
// Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
190+
transaction.DynamicSamplingContext ??= transaction.CreateDynamicSamplingContext(_options, _replaySession);
191+
192+
if (_options.TransactionProfilerFactory is { } profilerFactory &&
193+
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
194+
{
195+
// TODO cancellation token based on Hub being closed?
196+
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
197+
}
187198

188199
// A sampled out transaction still appears fully functional to the user
189200
// but will be dropped by the client and won't reach Sentry's servers.
@@ -220,7 +231,7 @@ public SentryTraceHeader GetTraceHeader()
220231
public BaggageHeader GetBaggage()
221232
{
222233
var span = GetSpan();
223-
if (span?.GetTransaction() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc })
234+
if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc)
224235
{
225236
return dsc.ToBaggageHeader();
226237
}
@@ -373,9 +384,9 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span)
373384
evt.Contexts.Trace.TraceId = span.TraceId;
374385
evt.Contexts.Trace.ParentSpanId = span.ParentSpanId;
375386

376-
if (span.GetTransaction() is TransactionTracer transactionTracer)
387+
if (span.GetTransaction().GetDynamicSamplingContext() is { } dsc)
377388
{
378-
evt.DynamicSamplingContext = transactionTracer.DynamicSamplingContext;
389+
evt.DynamicSamplingContext = dsc;
379390
}
380391
}
381392

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: 12 additions & 12 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;
24-
public DateTimeOffset? EndTimestamp => default;
25-
public bool IsFinished => default;
24+
public DateTimeOffset? EndTimestamp => null;
25+
public virtual bool IsFinished => false;
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

0 commit comments

Comments
 (0)