Repository: open-telemetry/opentelemetry-dotnet
Pull request #6161: Revert TraceContextPropagator performance refactor from 5749
Author: login: stevejgordon
Labels: name: pkg:OpenTelemetry.Api
description: Issues related to OpenTelemetry.Api NuGet package
Created: 2025-03-03T13:48:21Z | Merged: 2025-03-03T19:55:52Z

Fixes #6158

## Changes

As discussed in #6158, this reverts to previous performance optimizations until we have added more tests and hardened the implementation.

## Merge requirement checklist

* [x] [CONTRIBUTING](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/CONTRIBUTING.md) guidelines followed (license requirements, nullable enabled, static analysis, etc.)
* [x] Unit tests added/updated
* [x] Appropriate `CHANGELOG.md` files updated for non-trivial changes
* [] ~Changes in public API reviewed (if applicable)~



Diff:
diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md
index a726bf04cc5..a5a7c084b04 100644
--- a/src/OpenTelemetry.Api/CHANGELOG.md
+++ b/src/OpenTelemetry.Api/CHANGELOG.md
@@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md).
 
 ## Unreleased
 
+* Revert optimize performance of `TraceContextPropagator.Extract` introduced
+  in #5749 to resolve #6158.
+  ([#6161](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6161))
+
 ## 1.11.1
 
 Released 2025-Jan-22
diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
index ee67aaaaafb..8a8408a8b47 100644
--- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
+++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
@@ -1,9 +1,9 @@
 // Copyright The OpenTelemetry Authors
 // SPDX-License-Identifier: Apache-2.0
 
-using System.Buffers;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
+using System.Text;
 using OpenTelemetry.Internal;
 
 namespace OpenTelemetry.Context.Propagation;
@@ -76,7 +76,7 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
             var tracestateCollection = getter(carrier, TraceState);
             if (tracestateCollection?.Any() ?? false)
             {
-                TryExtractTracestate(tracestateCollection, out tracestate);
+                TryExtractTracestate(tracestateCollection.ToArray(), out tracestate);
             }
 
             return new PropagationContext(
@@ -220,37 +220,31 @@ internal static bool TryExtractTraceparent(string traceparent, out ActivityTrace
         return true;
     }
 
-    internal static bool TryExtractTracestate(IEnumerable<string> tracestateCollection, out string tracestateResult)
+    internal static bool TryExtractTracestate(string[] tracestateCollection, out string tracestateResult)
     {
         tracestateResult = string.Empty;
 
-        char[]? rentedArray = null;
-        Span<char> traceStateBuffer = stackalloc char[128]; // 256B
-        Span<char> keyLookupBuffer = stackalloc char[96]; // 192B (3x32 keys)
-        int keys = 0;
-        int charsWritten = 0;
-
-        try
+        if (tracestateCollection != null)
         {
-            foreach (var tracestateItem in tracestateCollection)
+            var keySet = new HashSet<string>();
+            var result = new StringBuilder();
+            for (int i = 0; i < tracestateCollection.Length; ++i)
             {
-                var tracestate = tracestateItem.AsSpan();
-                int position = 0;
-
-                while (position < tracestate.Length)
+                var tracestate = tracestateCollection[i].AsSpan();
+                int begin = 0;
+                while (begin < tracestate.Length)
                 {
-                    int length = tracestate.Slice(position).IndexOf(',');
+                    int length = tracestate.Slice(begin).IndexOf(',');
                     ReadOnlySpan<char> listMember;
-
                     if (length != -1)
                     {
-                        listMember = tracestate.Slice(position, length).Trim();
-                        position += length + 1;
+                        listMember = tracestate.Slice(begin, length).Trim();
+                        begin += length + 1;
                     }
                     else
                     {
-                        listMember = tracestate.Slice(position).Trim();
-                        position = tracestate.Length;
+                        listMember = tracestate.Slice(begin).Trim();
+                        begin = tracestate.Length;
                     }
 
                     // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#tracestate-header-field-values
@@ -261,7 +255,7 @@ internal static bool TryExtractTracestate(IEnumerable<string> tracestateCollecti
                         continue;
                     }
 
-                    if (keys >= 32)
+                    if (keySet.Count >= 32)
                     {
                         // https://github.com/w3c/trace-context/blob/master/spec/20-http_request_header_format.md#list
                         // test_tracestate_member_count_limit
@@ -292,107 +286,25 @@ internal static bool TryExtractTracestate(IEnumerable<string> tracestateCollecti
                     }
 
                     // ValidateKey() call above has ensured the key does not contain upper case letters.
-
-                    var duplicationCheckLength = Math.Min(key.Length, 3);
-
-                    if (keys > 0)
-                    {
-                        // Fast path check of first three chars for potential duplicated keys
-                        var potentialMatchingKeyPosition = 1;
-                        var found = false;
-                        for (int i = 0; i < keys * 3; i += 3)
-                        {
-                            if (keyLookupBuffer.Slice(i, duplicationCheckLength).SequenceEqual(key.Slice(0, duplicationCheckLength)))
-                            {
-                                found = true;
-                                break;
-                            }
-
-                            potentialMatchingKeyPosition++;
-                        }
-
-                        // If the fast check has found a possible duplicate, we need to do a full check
-                        if (found)
-                        {
-                            var bufferToCompare = traceStateBuffer.Slice(0, charsWritten);
-
-                            // We know which key is the first possible duplicate, so skip to that key
-                            // by slicing to the position after the appropriate comma.
-                            for (int i = 1; i < potentialMatchingKeyPosition; i++)
-                            {
-                                var commaIndex = bufferToCompare.IndexOf(',');
-
-                                if (commaIndex > -1)
-                                {
-                                    bufferToCompare.Slice(commaIndex);
-                                }
-                            }
-
-                            int existingIndex = -1;
-                            while ((existingIndex = bufferToCompare.IndexOf(key)) > -1)
-                            {
-                                if ((existingIndex > 0 && bufferToCompare[existingIndex - 1] != ',') || bufferToCompare[existingIndex + key.Length] != '=')
-                                {
-                                    continue; // this is not a key
-                                }
-
-                                return false; // test_tracestate_duplicated_keys
-                            }
-                        }
-                    }
-
-                    // Store up to the first three characters of the key for use in the duplicate lookup fast path
-                    var startKeyLookupIndex = keys > 0 ? keys * 3 : 0;
-                    key.Slice(0, duplicationCheckLength).CopyTo(keyLookupBuffer.Slice(startKeyLookupIndex));
-
-                    // Check we have capacity to write the key and value
-                    var requiredCapacity = charsWritten > 0 ? listMember.Length + 1 : listMember.Length;
-
-                    while (charsWritten + requiredCapacity > traceStateBuffer.Length)
+                    if (!keySet.Add(key.ToString()))
                     {
-                        GrowBuffer(ref rentedArray, ref traceStateBuffer);
+                        // test_tracestate_duplicated_keys
+                        return false;
                     }
 
-                    if (charsWritten > 0)
+                    if (result.Length > 0)
                     {
-                        traceStateBuffer[charsWritten++] = ',';
+                        result.Append(',');
                     }
 
-                    listMember.CopyTo(traceStateBuffer.Slice(charsWritten));
-                    charsWritten += listMember.Length;
-
-                    keys++;
+                    result.Append(listMember.ToString());
                 }
             }
 
-            tracestateResult = traceStateBuffer.Slice(0, charsWritten).ToString();
-
-            return true;
+            tracestateResult = result.ToString();
         }
-        finally
-        {
-            if (rentedArray is not null)
-            {
-                ArrayPool<char>.Shared.Return(rentedArray);
-                rentedArray = null;
-            }
-        }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        static void GrowBuffer(ref char[]? array, ref Span<char> buffer)
-        {
-            var newBuffer = ArrayPool<char>.Shared.Rent(buffer.Length * 2);
 
-            buffer.CopyTo(newBuffer.AsSpan());
-
-            if (array is not null)
-            {
-                ArrayPool<char>.Shared.Return(array);
-            }
-
-            array = newBuffer;
-            buffer = array.AsSpan();
-        }
+        return true;
     }
 
     private static byte HexCharToByte(char c)


Reviews:
user: login: cijothomas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T16:00:34Z
user: login: cijothomas
body: LGTM, need to mention in changelog that it closes the reported perf issue.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T16:00:57Z
user: login: stevejgordon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:31:03Z
user: login: cijothomas
body: Changelog looks good as well! Thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T17:10:23Z
user: login: Kielek
body: @rajkumar-rangaraj, can we make a release 11.1.2 when this one is merged?

I think that we have 2 good reasons (fixes) for this.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T18:13:18Z
user: login: rajkumar-rangaraj
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T19:54:57Z

Review comments:
diff_hunk: @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md).
 
 ## Unreleased
 
+* Revert optimize performance of `TraceContextPropagator.Extract` from #5749.
path: src/OpenTelemetry.Api/CHANGELOG.md
user: login: cijothomas
body: We can resolve https://github.com/open-telemetry/opentelemetry-dotnet/issues/6158 with this change, but need a way to make it visible in changelog. 

 I suggest adding https://github.com/open-telemetry/opentelemetry-dotnet/issues/6158 in this changelog, and also in the instrumentation library changelog too when they bump api version

created_at: 2025-03-03T16:00:34Z
author_association: MEMBER
diff_hunk: @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md).
 
 ## Unreleased
 
+* Revert optimize performance of `TraceContextPropagator.Extract` from #5749.
path: src/OpenTelemetry.Api/CHANGELOG.md
user: login: stevejgordon
body: I've tweaked the changelog message.
created_at: 2025-03-03T16:31:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: rajkumar-rangaraj
created_at: 2025-03-03T18:21:45Z
body: > @rajkumar-rangaraj, can we make a release 11.1.2 when this one is merged?
> 
> I think that we have 2 good reasons (fixes) for this.

Yes, we should discuss in tomorrow's SIG and plan on immediate release. 

author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-03-03T19:56:00Z
body: ## [Codecov](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/pull/6161?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.30%. Comparing base [(`577337c`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/commit/577337c992b1af0ce99091f922366429af7b8c18?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) to head [(`066c2c6`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/commit/066c2c6bbf1532cca9578846bd60dd2aa41f1b78?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/pull/6161/graphs/tree.svg?width=650&height=150&src=pr&token=vscyfvPfy5&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/pull/6161?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@            Coverage Diff             @@
##             main    #6161      +/-   ##
==========================================
- Coverage   86.34%   86.30%   -0.05%     
==========================================
  Files         257      257              
  Lines       11557    11499      -58     
==========================================
- Hits         9979     9924      -55     
+ Misses       1578     1575       -3     
```

| [Files with missing lines](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/pull/6161?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [....Api/Context/Propagation/TraceContextPropagator.cs](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet/pull/6161?src=pr&el=tree&filepath=src%2FOpenTelemetry.Api%2FContext%2FPropagation%2FTraceContextPropagator.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-c3JjL09wZW5UZWxlbWV0cnkuQXBpL0NvbnRleHQvUHJvcGFnYXRpb24vVHJhY2VDb250ZXh0UHJvcGFnYXRvci5jcw==) | `89.37% <ø> (-1.46%)` | :arrow_down: |

</details>
author_association: NONE