Skip to content

Commit 83ecef8

Browse files
stevejgordonvishweshbankwarCodeBlanch
authored
[api] Optimise TraceContextPropagator.Extract (#5749)
Co-authored-by: Vishwesh Bankwar <[email protected]> Co-authored-by: Mikel Blanchard <[email protected]>
1 parent 230adab commit 83ecef8

File tree

4 files changed

+202
-29
lines changed

4 files changed

+202
-29
lines changed

src/OpenTelemetry.Api/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Notes](../../RELEASENOTES.md).
1111
returned an empty set.
1212
([#5745](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5745))
1313

14+
* Optimize performance of `TraceContextPropagator.Extract`.
15+
([#5749](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5749))
16+
1417
## 1.9.0
1518

1619
Released 2024-Jun-14

src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Copyright The OpenTelemetry Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using System.Buffers;
45
using System.Diagnostics;
56
using System.Runtime.CompilerServices;
6-
using System.Text;
77
using OpenTelemetry.Internal;
88

99
namespace OpenTelemetry.Context.Propagation;
@@ -76,7 +76,7 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
7676
var tracestateCollection = getter(carrier, TraceState);
7777
if (tracestateCollection?.Any() ?? false)
7878
{
79-
TryExtractTracestate(tracestateCollection.ToArray(), out tracestate);
79+
TryExtractTracestate(tracestateCollection, out tracestate);
8080
}
8181

8282
return new PropagationContext(
@@ -220,31 +220,37 @@ internal static bool TryExtractTraceparent(string traceparent, out ActivityTrace
220220
return true;
221221
}
222222

223-
internal static bool TryExtractTracestate(string[] tracestateCollection, out string tracestateResult)
223+
internal static bool TryExtractTracestate(IEnumerable<string> tracestateCollection, out string tracestateResult)
224224
{
225225
tracestateResult = string.Empty;
226226

227-
if (tracestateCollection != null)
227+
char[] rentedArray = null;
228+
Span<char> traceStateBuffer = stackalloc char[128]; // 256B
229+
Span<char> keyLookupBuffer = stackalloc char[96]; // 192B (3x32 keys)
230+
int keys = 0;
231+
int charsWritten = 0;
232+
233+
try
228234
{
229-
var keySet = new HashSet<string>();
230-
var result = new StringBuilder();
231-
for (int i = 0; i < tracestateCollection.Length; ++i)
235+
foreach (var tracestateItem in tracestateCollection)
232236
{
233-
var tracestate = tracestateCollection[i].AsSpan();
234-
int begin = 0;
235-
while (begin < tracestate.Length)
237+
var tracestate = tracestateItem.AsSpan();
238+
int position = 0;
239+
240+
while (position < tracestate.Length)
236241
{
237-
int length = tracestate.Slice(begin).IndexOf(',');
242+
int length = tracestate.Slice(position).IndexOf(',');
238243
ReadOnlySpan<char> listMember;
244+
239245
if (length != -1)
240246
{
241-
listMember = tracestate.Slice(begin, length).Trim();
242-
begin += length + 1;
247+
listMember = tracestate.Slice(position, length).Trim();
248+
position += length + 1;
243249
}
244250
else
245251
{
246-
listMember = tracestate.Slice(begin).Trim();
247-
begin = tracestate.Length;
252+
listMember = tracestate.Slice(position).Trim();
253+
position = tracestate.Length;
248254
}
249255

250256
// https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values
@@ -255,7 +261,7 @@ internal static bool TryExtractTracestate(string[] tracestateCollection, out str
255261
continue;
256262
}
257263

258-
if (keySet.Count >= 32)
264+
if (keys >= 32)
259265
{
260266
// https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list
261267
// test_tracestate_member_count_limit
@@ -286,25 +292,107 @@ internal static bool TryExtractTracestate(string[] tracestateCollection, out str
286292
}
287293

288294
// ValidateKey() call above has ensured the key does not contain upper case letters.
289-
if (!keySet.Add(key.ToString()))
295+
296+
var duplicationCheckLength = Math.Min(key.Length, 3);
297+
298+
if (keys > 0)
290299
{
291-
// test_tracestate_duplicated_keys
292-
return false;
300+
// Fast path check of first three chars for potential duplicated keys
301+
var potentialMatchingKeyPosition = 1;
302+
var found = false;
303+
for (int i = 0; i < keys * 3; i += 3)
304+
{
305+
if (keyLookupBuffer.Slice(i, duplicationCheckLength).SequenceEqual(key.Slice(0, duplicationCheckLength)))
306+
{
307+
found = true;
308+
break;
309+
}
310+
311+
potentialMatchingKeyPosition++;
312+
}
313+
314+
// If the fast check has found a possible duplicate, we need to do a full check
315+
if (found)
316+
{
317+
var bufferToCompare = traceStateBuffer.Slice(0, charsWritten);
318+
319+
// We know which key is the first possible duplicate, so skip to that key
320+
// by slicing to the position after the appropriate comma.
321+
for (int i = 1; i < potentialMatchingKeyPosition; i++)
322+
{
323+
var commaIndex = bufferToCompare.IndexOf(',');
324+
325+
if (commaIndex > -1)
326+
{
327+
bufferToCompare.Slice(commaIndex);
328+
}
329+
}
330+
331+
int existingIndex = -1;
332+
while ((existingIndex = bufferToCompare.IndexOf(key)) > -1)
333+
{
334+
if ((existingIndex > 0 && bufferToCompare[existingIndex - 1] != ',') || bufferToCompare[existingIndex + key.Length] != '=')
335+
{
336+
continue; // this is not a key
337+
}
338+
339+
return false; // test_tracestate_duplicated_keys
340+
}
341+
}
293342
}
294343

295-
if (result.Length > 0)
344+
// Store up to the first three characters of the key for use in the duplicate lookup fast path
345+
var startKeyLookupIndex = keys > 0 ? keys * 3 : 0;
346+
key.Slice(0, duplicationCheckLength).CopyTo(keyLookupBuffer.Slice(startKeyLookupIndex));
347+
348+
// Check we have capacity to write the key and value
349+
var requiredCapacity = charsWritten > 0 ? listMember.Length + 1 : listMember.Length;
350+
351+
while (charsWritten + requiredCapacity > traceStateBuffer.Length)
296352
{
297-
result.Append(',');
353+
GrowBuffer(ref rentedArray, ref traceStateBuffer);
298354
}
299355

300-
result.Append(listMember.ToString());
356+
if (charsWritten > 0)
357+
{
358+
traceStateBuffer[charsWritten++] = ',';
359+
}
360+
361+
listMember.CopyTo(traceStateBuffer.Slice(charsWritten));
362+
charsWritten += listMember.Length;
363+
364+
keys++;
301365
}
302366
}
303367

304-
tracestateResult = result.ToString();
368+
tracestateResult = traceStateBuffer.Slice(0, charsWritten).ToString();
369+
370+
return true;
371+
}
372+
finally
373+
{
374+
if (rentedArray is not null)
375+
{
376+
ArrayPool<char>.Shared.Return(rentedArray);
377+
rentedArray = null;
378+
}
305379
}
306380

307-
return true;
381+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
382+
static void GrowBuffer(ref char[] array, ref Span<char> buffer)
383+
{
384+
var newBuffer = ArrayPool<char>.Shared.Rent(buffer.Length * 2);
385+
386+
buffer.CopyTo(newBuffer.AsSpan());
387+
388+
if (array is not null)
389+
{
390+
ArrayPool<char>.Shared.Return(array);
391+
}
392+
393+
array = newBuffer;
394+
buffer = array.AsSpan();
395+
}
308396
}
309397

310398
private static byte HexCharToByte(char c)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using BenchmarkDotNet.Attributes;
5+
using OpenTelemetry.Context.Propagation;
6+
7+
namespace Benchmarks.Context.Propagation;
8+
9+
public class TraceContextPropagatorBenchmarks
10+
{
11+
private const string TraceParent = "traceparent";
12+
private const string TraceState = "tracestate";
13+
private const string TraceId = "0af7651916cd43dd8448eb211c80319c";
14+
private const string SpanId = "b9c7c989f97918e1";
15+
16+
private static readonly Random Random = new(455946);
17+
private static readonly TraceContextPropagator TraceContextPropagator = new();
18+
19+
private static readonly Func<IReadOnlyDictionary<string, string>, string, IEnumerable<string>> Getter = (headers, name) =>
20+
{
21+
if (headers.TryGetValue(name, out var value))
22+
{
23+
return [value];
24+
}
25+
26+
return [];
27+
};
28+
29+
private Dictionary<string, string> headers;
30+
31+
[Params(true, false)]
32+
public bool LongListMember { get; set; }
33+
34+
[Params(0, 4, 32)]
35+
public int MembersCount { get; set; }
36+
37+
public Dictionary<string, string> Headers => this.headers;
38+
39+
[GlobalSetup]
40+
public void Setup()
41+
{
42+
var length = this.LongListMember ? 256 : 20;
43+
44+
var value = new string('a', length);
45+
46+
Span<char> keyBuffer = stackalloc char[length - 2];
47+
48+
string traceState = string.Empty;
49+
for (var i = 0; i < this.MembersCount; i++)
50+
{
51+
// We want a unique key for each member
52+
for (var j = 0; j < length - 2; j++)
53+
{
54+
keyBuffer[j] = (char)('a' + Random.Next(0, 26));
55+
}
56+
57+
var key = keyBuffer.ToString();
58+
59+
var listMember = $"{key}{i:00}={value}";
60+
traceState += i < this.MembersCount - 1 ? $"{listMember}," : listMember;
61+
}
62+
63+
this.headers = new Dictionary<string, string>
64+
{
65+
{ TraceParent, $"00-{TraceId}-{SpanId}-01" },
66+
{ TraceState, traceState },
67+
};
68+
}
69+
70+
[Benchmark(Baseline = true)]
71+
public void Extract() => _ = TraceContextPropagator!.Extract(default, this.headers, Getter);
72+
}

test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTest.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ public class TraceContextPropagatorTest
1313
private const string TraceId = "0af7651916cd43dd8448eb211c80319c";
1414
private const string SpanId = "b9c7c989f97918e1";
1515

16-
private static readonly string[] Empty = Array.Empty<string>();
16+
private static readonly string[] Empty = [];
1717
private static readonly Func<IDictionary<string, string>, string, IEnumerable<string>> Getter = (headers, name) =>
1818
{
1919
if (headers.TryGetValue(name, out var value))
2020
{
21-
return new[] { value };
21+
return [value];
2222
}
2323

2424
return Empty;
@@ -31,7 +31,7 @@ public class TraceContextPropagatorTest
3131
return value;
3232
}
3333

34-
return Array.Empty<string>();
34+
return [];
3535
};
3636

3737
private static readonly Action<IDictionary<string, string>, string, string> Setter = (carrier, name, value) =>
@@ -183,8 +183,18 @@ public void DuplicateKeys()
183183
// test_tracestate_duplicated_keys
184184
Assert.Empty(CallTraceContextPropagator("foo=1,foo=1"));
185185
Assert.Empty(CallTraceContextPropagator("foo=1,foo=2"));
186-
Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=1" }));
187-
Assert.Empty(CallTraceContextPropagator(new[] { "foo=1", "foo=2" }));
186+
Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=1"]));
187+
Assert.Empty(CallTraceContextPropagator(["foo=1", "foo=2"]));
188+
Assert.Empty(CallTraceContextPropagator("foo=1,bar=2,baz=3,foo=4"));
189+
}
190+
191+
[Fact]
192+
public void NoDuplicateKeys()
193+
{
194+
Assert.Equal("foo=1,bar=foo,baz=2", CallTraceContextPropagator("foo=1,bar=foo,baz=2"));
195+
Assert.Equal("foo=1,bar=2,baz=foo", CallTraceContextPropagator("foo=1,bar=2,baz=foo"));
196+
Assert.Equal("foo=1,foo@tenant=2", CallTraceContextPropagator("foo=1,foo@tenant=2"));
197+
Assert.Equal("foo=1,tenant@foo=2", CallTraceContextPropagator("foo=1,tenant@foo=2"));
188198
}
189199

190200
[Fact]

0 commit comments

Comments
 (0)