Skip to content

Commit a542f0c

Browse files
perf: Avoid shallow copies of DSC (#4453)
Resolves #4381 - #4381 #skip-changelog
1 parent b7da47e commit a542f0c

File tree

6 files changed

+44
-104
lines changed

6 files changed

+44
-104
lines changed

src/Sentry.AspNet/HttpContextExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public static ITransactionTracer StartSentryTransaction(this HttpContext httpCon
113113
// See:
114114
// https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#freezing-dynamic-sampling-context
115115
// https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#unified-propagation-mechanism
116-
dynamicSamplingContext = DynamicSamplingContext.Empty;
116+
dynamicSamplingContext = DynamicSamplingContext.Empty();
117117
}
118118

119119
var transaction = SentrySdk.StartTransaction(transactionContext, customSamplingContext, dynamicSamplingContext);

src/Sentry.AspNetCore/SentryTracingMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public SentryTracingMiddleware(
7575
// See:
7676
// https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#freezing-dynamic-sampling-context
7777
// https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#unified-propagation-mechanism
78-
dynamicSamplingContext = DynamicSamplingContext.Empty;
78+
dynamicSamplingContext = DynamicSamplingContext.Empty();
7979
}
8080

8181
var transaction = _getHub().StartTransaction(transactionContext, customSamplingContext, dynamicSamplingContext);

src/Sentry/DynamicSamplingContext.cs

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ namespace Sentry;
99
/// <seealso href="https://develop.sentry.dev/sdk/performance/dynamic-sampling-context"/>
1010
internal class DynamicSamplingContext
1111
{
12-
public IReadOnlyDictionary<string, string> Items { get; }
12+
private readonly Dictionary<string, string> _items;
13+
14+
public IReadOnlyDictionary<string, string> Items => _items;
1315

1416
public bool IsEmpty => Items.Count == 0;
1517

16-
private DynamicSamplingContext(IReadOnlyDictionary<string, string> items) => Items = items;
18+
private DynamicSamplingContext(Dictionary<string, string> items) => _items = items;
1719

1820
/// <summary>
1921
/// Gets an empty <see cref="DynamicSamplingContext"/> that can be used to "freeze" the DSC on a transaction.
2022
/// </summary>
21-
public static readonly DynamicSamplingContext Empty = new(new Dictionary<string, string>().AsReadOnly());
23+
public static DynamicSamplingContext Empty() => new(new Dictionary<string, string>());
2224

2325
private DynamicSamplingContext(SentryId traceId,
2426
string publicKey,
@@ -93,39 +95,24 @@ private DynamicSamplingContext(SentryId traceId,
9395
items.Add("replay_id", replayId.ToString());
9496
}
9597

96-
Items = items;
98+
_items = items;
9799
}
98100

99101
public BaggageHeader ToBaggageHeader() => BaggageHeader.Create(Items, useSentryPrefix: true);
100102

101-
public DynamicSamplingContext WithSampleRate(double sampleRate)
103+
public void SetSampleRate(double sampleRate)
102104
{
103-
if (Items.TryGetValue("sample_rate", out var dscSampleRate))
104-
{
105-
if (double.TryParse(dscSampleRate, NumberStyles.Float, CultureInfo.InvariantCulture, out var rate))
106-
{
107-
if (Math.Abs(rate - sampleRate) > double.Epsilon)
108-
{
109-
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
110-
items["sample_rate"] = sampleRate.ToString(CultureInfo.InvariantCulture);
111-
return new DynamicSamplingContext(items);
112-
}
113-
}
114-
}
115-
116-
return this;
105+
_items["sample_rate"] = sampleRate.ToString(CultureInfo.InvariantCulture);
117106
}
118107

119-
public DynamicSamplingContext WithReplayId(IReplaySession? replaySession)
108+
internal DynamicSamplingContext Clone() => new(new Dictionary<string, string>(_items));
109+
110+
public void SetReplayId(IReplaySession? replaySession)
120111
{
121-
if (replaySession?.ActiveReplayId is not { } replayId || replayId == SentryId.Empty)
112+
if (replaySession?.ActiveReplayId is { } replayId && replayId != SentryId.Empty)
122113
{
123-
return this;
114+
_items["replay_id"] = replayId.ToString();
124115
}
125-
126-
var items = Items.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
127-
items["replay_id"] = replayId.ToString();
128-
return new DynamicSamplingContext(items);
129116
}
130117

131118
public static DynamicSamplingContext? CreateFromBaggageHeader(BaggageHeader baggage, IReplaySession? replaySession)

src/Sentry/Internal/Hub.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ internal ITransactionTracer StartTransaction(
193193
isSampled = SampleRandHelper.IsSampled(sampleRand, samplerSampleRate);
194194

195195
// Ensure the actual sampleRate is set on the provided DSC (if any) when the TracesSampler reached a sampling decision
196-
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(samplerSampleRate);
196+
dynamicSamplingContext?.SetSampleRate(samplerSampleRate);
197197
}
198198
}
199199

@@ -207,12 +207,12 @@ internal ITransactionTracer StartTransaction(
207207
if (context.IsSampled is null && _options.TracesSampleRate is not null)
208208
{
209209
// Ensure the actual sampleRate is set on the provided DSC (if any) when not IsSampled upstream but the TracesSampleRate reached a sampling decision
210-
dynamicSamplingContext = dynamicSamplingContext?.WithSampleRate(_options.TracesSampleRate.Value);
210+
dynamicSamplingContext?.SetSampleRate(_options.TracesSampleRate.Value);
211211
}
212212
}
213213

214214
// Make sure there is a replayId (if available) on the provided DSC (if any).
215-
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);
215+
dynamicSamplingContext?.SetReplayId(_replaySession);
216216

217217
if (isSampled is false)
218218
{

test/Sentry.Tests/DynamicSamplingContextTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public Fixture()
2525
[Fact]
2626
public void EmptyContext()
2727
{
28-
var dsc = DynamicSamplingContext.Empty;
28+
var dsc = DynamicSamplingContext.Empty();
2929

3030
Assert.True(dsc.IsEmpty);
3131
}

test/Sentry.Tests/HubTests.cs

Lines changed: 25 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
743743
{"sentry-sample_rate", "0.5"},
744744
{"sentry-sample_rand", "0.1234"},
745745
}).CreateDynamicSamplingContext();
746+
var originalDsc = dsc?.Clone();
746747

747748
_fixture.Options.TracesSampler = _ => tracesSampler;
748749
_fixture.Options.TracesSampleRate = tracesSampleRate;
@@ -759,12 +760,11 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
759760
transactionTracer.SampleRate.Should().Be(expectedSampleRate);
760761
if (expectedDscOverwritten)
761762
{
762-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
763-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
763+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(expectedSampleRate));
764764
}
765765
else
766766
{
767-
transactionTracer.DynamicSamplingContext.Should().BeSameAs(dsc);
767+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc);
768768
}
769769
}
770770
else
@@ -773,12 +773,11 @@ public void StartTransaction_DynamicSamplingContextWithSampleRate_OverwritesSamp
773773
unsampledTransaction.SampleRate.Should().Be(expectedSampleRate);
774774
if (expectedDscOverwritten)
775775
{
776-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
777-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(expectedSampleRate));
776+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(expectedSampleRate));
778777
}
779778
else
780779
{
781-
unsampledTransaction.DynamicSamplingContext.Should().BeSameAs(dsc);
780+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc);
782781
}
783782
}
784783
}
@@ -814,12 +813,12 @@ public void StartTransaction_DynamicSamplingContextWithReplayId_UsesActiveReplay
814813
transactionTracer.IsSampled.Should().BeTrue();
815814
transactionTracer.DynamicSamplingContext.Should().NotBeNull();
816815

817-
var expectedDsc = dsc.ReplaceSampleRate(_fixture.Options.TracesSampleRate.Value);
816+
var expectedDsc = dsc.CloneWithSampleRate(_fixture.Options.TracesSampleRate.Value);
818817
if (replaySessionIsActive)
819818
{
820819
// We overwrite the replay_id when we have an active replay session
821820
// Otherwise we propagate whatever was in the baggage header
822-
expectedDsc = expectedDsc.ReplaceReplayId(_fixture.ReplaySession);
821+
expectedDsc = expectedDsc.CloneWithReplayId(_fixture.ReplaySession);
823822
}
824823
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(expectedDsc);
825824
}
@@ -881,7 +880,7 @@ public void StartTransaction_DynamicSamplingContextWithoutSampleRand_SampleRandN
881880
var hub = _fixture.GetSut();
882881

883882
// Act
884-
var transaction = hub.StartTransaction(transactionContext, new Dictionary<string, object>(), DynamicSamplingContext.Empty);
883+
var transaction = hub.StartTransaction(transactionContext, new Dictionary<string, object>(), DynamicSamplingContext.Empty());
885884

886885
// Assert
887886
var transactionTracer = transaction.Should().BeOfType<TransactionTracer>().Subject;
@@ -905,6 +904,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample
905904
{"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic
906905
{"sentry-sample_rand", "0.1234"}
907906
}).CreateDynamicSamplingContext(dummyReplaySession);
907+
var originalDsc = dsc.Clone();
908908

909909
_fixture.Options.TracesSampleRate = 0.4;
910910
var hub = _fixture.GetSut();
@@ -917,8 +917,7 @@ public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSample
917917
transactionTracer.IsSampled.Should().BeTrue();
918918
transactionTracer.SampleRate.Should().Be(0.4);
919919
transactionTracer.SampleRand.Should().Be(0.1234);
920-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
921-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(0.4));
920+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(0.4));
922921
}
923922

924923
[Theory]
@@ -937,6 +936,7 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool
937936
{"sentry-sample_rate", "0.5"},
938937
{"sentry-sample_rand", "0.1234"}
939938
}).CreateDynamicSamplingContext(_fixture.ReplaySession);
939+
var originalDsc = dsc.Clone();
940940

941941
_fixture.Options.TracesSampler = _ => sampleRate;
942942
var hub = _fixture.GetSut();
@@ -951,17 +951,15 @@ public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool
951951
transactionTracer.IsSampled.Should().BeTrue();
952952
transactionTracer.SampleRate.Should().Be(sampleRate);
953953
transactionTracer.SampleRand.Should().Be(0.1234);
954-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
955-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
954+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
956955
}
957956
else
958957
{
959958
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
960959
unsampledTransaction.IsSampled.Should().BeFalse();
961960
unsampledTransaction.SampleRate.Should().Be(sampleRate);
962961
unsampledTransaction.SampleRand.Should().Be(0.1234);
963-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
964-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
962+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
965963
}
966964
}
967965

@@ -981,6 +979,7 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo
981979
{"sentry-sample_rate", "0.5"}, // Static sampling ignores this and uses options.TracesSampleRate instead
982980
{"sentry-sample_rand", "0.1234"}
983981
}).CreateDynamicSamplingContext(dummyReplaySession);
982+
var originalDsc = dsc.Clone();
984983

985984
_fixture.Options.TracesSampleRate = sampleRate;
986985
var hub = _fixture.GetSut();
@@ -995,17 +994,15 @@ public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, boo
995994
transactionTracer.IsSampled.Should().BeTrue();
996995
transactionTracer.SampleRate.Should().Be(sampleRate);
997996
transactionTracer.SampleRand.Should().Be(0.1234);
998-
transactionTracer.DynamicSamplingContext.Should().NotBeSameAs(dsc);
999-
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
997+
transactionTracer.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
1000998
}
1001999
else
10021000
{
10031001
var unsampledTransaction = transaction.Should().BeOfType<UnsampledTransaction>().Subject;
10041002
unsampledTransaction.IsSampled.Should().BeFalse();
10051003
unsampledTransaction.SampleRate.Should().Be(sampleRate);
10061004
unsampledTransaction.SampleRand.Should().Be(0.1234);
1007-
unsampledTransaction.DynamicSamplingContext.Should().NotBeSameAs(dsc);
1008-
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(dsc.ReplaceSampleRate(sampleRate));
1005+
unsampledTransaction.DynamicSamplingContext.Should().BeEquivalentTo(originalDsc.CloneWithSampleRate(sampleRate));
10091006
}
10101007
}
10111008

@@ -1255,7 +1252,7 @@ public void GetBaggage_SpanActive_ReturnsBaggageFromSpan(bool isSampled)
12551252
baggage.Members.Should().Equal([
12561253
new KeyValuePair<string, string>("sentry-trace_id", "43365712692146d08ee11a729dfbcaca"),
12571254
new KeyValuePair<string, string>("sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"),
1258-
new KeyValuePair<string, string>("sentry-sample_rate", isSampled ? "1" : "0.0"),
1255+
new KeyValuePair<string, string>("sentry-sample_rate", isSampled ? "1" : "0"),
12591256
new KeyValuePair<string, string>("sentry-sample_rand", sampleRand!.Value.ToString(CultureInfo.InvariantCulture)),
12601257
]);
12611258
}
@@ -2409,63 +2406,19 @@ internal partial class HubTestsJsonContext : JsonSerializerContext
24092406
#nullable enable
24102407
file static class DynamicSamplingContextExtensions
24112408
{
2412-
public static DynamicSamplingContext ReplaceSampleRate(this DynamicSamplingContext? dsc, double sampleRate)
2409+
public static DynamicSamplingContext CloneWithSampleRate(this DynamicSamplingContext? dsc, double sampleRate)
24132410
{
24142411
Assert.NotNull(dsc);
2415-
2416-
var value = sampleRate.ToString(CultureInfo.InvariantCulture);
2417-
return dsc.Replace("sentry-sample_rate", value);
2412+
var newDsc = dsc.Clone();
2413+
newDsc.SetSampleRate(sampleRate);
2414+
return newDsc;
24182415
}
24192416

2420-
public static DynamicSamplingContext ReplaceReplayId(this DynamicSamplingContext? dsc, IReplaySession replaySession)
2417+
public static DynamicSamplingContext CloneWithReplayId(this DynamicSamplingContext? dsc, IReplaySession replaySession)
24212418
{
24222419
Assert.NotNull(dsc);
2423-
2424-
if (!replaySession.ActiveReplayId.HasValue)
2425-
{
2426-
throw new InvalidOperationException($"No {nameof(IReplaySession.ActiveReplayId)}.");
2427-
}
2428-
2429-
var value = replaySession.ActiveReplayId.Value.ToString();
2430-
return dsc.Replace("sentry-replay_id", value);
2431-
}
2432-
2433-
private static DynamicSamplingContext Replace(this DynamicSamplingContext dsc, string key, string newValue)
2434-
{
2435-
var items = dsc.ToBaggageHeader().Members.ToList();
2436-
var index = items.FindSingleIndex(key);
2437-
2438-
var oldValue = items[index].Value;
2439-
if (oldValue == newValue)
2440-
{
2441-
throw new InvalidOperationException($"{key} already is {oldValue}.");
2442-
}
2443-
2444-
items[index] = new KeyValuePair<string, string>(key, newValue);
2445-
2446-
var baggage = BaggageHeader.Create(items);
2447-
var dynamicSamplingContext = DynamicSamplingContext.CreateFromBaggageHeader(baggage, null);
2448-
if (dynamicSamplingContext is null)
2449-
{
2450-
throw new InvalidOperationException($"Invalid {nameof(BaggageHeader)}: {baggage}");
2451-
}
2452-
2453-
return dynamicSamplingContext;
2454-
}
2455-
2456-
private static int FindSingleIndex(this List<KeyValuePair<string, string>> items, string key)
2457-
{
2458-
var index = items.FindIndex(item => item.Key == key);
2459-
if (index == -1)
2460-
{
2461-
throw new InvalidOperationException($"{key} not found.");
2462-
}
2463-
2464-
if (items.FindLastIndex(item => item.Key == key) != index)
2465-
{
2466-
throw new InvalidOperationException($"Duplicate {key} found.");
2467-
}
2468-
2469-
return index;
2420+
var newDsc = dsc.Clone();
2421+
newDsc.SetReplayId(replaySession);
2422+
return newDsc;
24702423
}
24712424
}

0 commit comments

Comments
 (0)