11// Copyright The OpenTelemetry Authors
22// SPDX-License-Identifier: Apache-2.0
33
4+ using System . Buffers ;
45using System . Diagnostics ;
56using System . Runtime . CompilerServices ;
6- using System . Text ;
77using OpenTelemetry . Internal ;
88
99namespace 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 )
0 commit comments