Skip to content

[Api] Optimize TraceContextPropagator#7060

Open
martincostello wants to merge 7 commits intoopen-telemetry:mainfrom
martincostello:optimize-TraceContextPropagator
Open

[Api] Optimize TraceContextPropagator#7060
martincostello wants to merge 7 commits intoopen-telemetry:mainfrom
martincostello:optimize-TraceContextPropagator

Conversation

@martincostello
Copy link
Copy Markdown
Member

@martincostello martincostello commented Apr 10, 2026

Changes

While looking at some profiles for an OTel instrumented application of mine, I noticed that TraceContextPropagator.Extract() came up in the top 5 OTel-related samples.

This makes some improvements, mostly written by Copilot, by:

  • Avoiding LINQ.
  • Use spans where possible.

Benchmark results

Copilot Summary

For the 6 TraceContextPropagatorBenchmarks.Extract cases, this PR is better in every case: lower mean execution time and lower allocations.

LongListMember MembersCount main Mean pr-7060 Mean Time change main Alloc pr-7060 Alloc Alloc change
False 0 81.50 ns 55.37 ns -32.1% 392 B 192 B -51.0%
False 4 369.72 ns 273.15 ns -26.1% 2240 B 192 B -91.4%
False 32 2020.38 ns 1900.42 ns -5.9% 12312 B 192 B -98.4%
True 0 86.94 ns 59.33 ns -31.8% 392 B 192 B -51.0%
True 4 2103.91 ns 1595.41 ns -24.2% 15456 B 192 B -98.8%
True 32 14382.64 ns 13094.73 ns -9.0% 100792 B 192 B -99.8%

Takeaway

The benchmark suite:

  • improves execution time by roughly 6% to 32%
  • reduces allocations by roughly 51% to
    99.8%
  • makes allocation effectively flat at 192 B across all tested cases

The largest wins are in the MembersCount = 4 scenarios and in the LongListMember = True cases with previously high allocations.

Expand to see

main

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8117/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.201
  [Host]     : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
Method LongListMember MembersCount Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Extract False 0 81.50 ns 1.153 ns 1.079 ns 1.00 0.02 0.0312 - 392 B 1.00
Extract False 4 369.72 ns 6.632 ns 6.204 ns 1.00 0.02 0.1783 0.0010 2240 B 1.00
Extract False 32 2,020.38 ns 29.656 ns 27.740 ns 1.00 0.02 0.9804 - 12312 B 1.00
Extract True 0 86.94 ns 0.513 ns 0.428 ns 1.00 0.01 0.0312 - 392 B 1.00
Extract True 4 2,103.91 ns 40.700 ns 57.056 ns 1.00 0.04 1.2283 0.0496 15456 B 1.00
Extract True 32 14,382.64 ns 119.399 ns 105.844 ns 1.00 0.01 8.0261 1.6022 100792 B 1.00

This PR

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8117/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700H 2.90GHz, 1 CPU, 20 logical and 14 physical cores
.NET SDK 10.0.201
  [Host]     : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.5 (10.0.5, 10.0.526.15411), X64 RyuJIT x86-64-v3
Method LongListMember MembersCount Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
Extract False 0 55.37 ns 0.468 ns 0.438 ns 1.00 0.01 0.0153 192 B 1.00
Extract False 4 273.15 ns 1.275 ns 1.193 ns 1.00 0.01 0.0153 192 B 1.00
Extract False 32 1,900.42 ns 6.314 ns 5.597 ns 1.00 0.00 0.0153 192 B 1.00
Extract True 0 59.33 ns 1.214 ns 1.662 ns 1.00 0.04 0.0153 192 B 1.00
Extract True 4 1,595.41 ns 8.493 ns 7.529 ns 1.00 0.01 0.0153 192 B 1.00
Extract True 32 13,094.73 ns 42.607 ns 39.855 ns 1.00 0.00 0.0153 192 B 1.00

Merge requirement checklist

  • CONTRIBUTING guidelines followed (license requirements, nullable enabled, static analysis, etc.)
  • Unit tests added/updated
  • Appropriate CHANGELOG.md files updated for non-trivial changes
  • Changes in public API reviewed (if applicable)

@github-actions github-actions bot added the pkg:OpenTelemetry.Api Issues related to OpenTelemetry.Api NuGet package label Apr 10, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 90.05848% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.02%. Comparing base (c9158d6) to head (d08e100).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
....Api/Context/Propagation/TraceContextPropagator.cs 90.05% 17 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7060      +/-   ##
==========================================
- Coverage   89.04%   89.02%   -0.02%     
==========================================
  Files         271      271              
  Lines       12935    13068     +133     
==========================================
+ Hits        11518    11634     +116     
- Misses       1417     1434      +17     
Flag Coverage Δ
unittests-Project-Experimental 88.89% <90.05%> (-0.15%) ⬇️
unittests-Project-Stable 88.83% <90.05%> (-0.15%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
....Api/Context/Propagation/TraceContextPropagator.cs 91.08% <90.05%> (-2.29%) ⬇️

... and 1 file with indirect coverage changes

@github-actions github-actions bot added the pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package label Apr 11, 2026
@martincostello martincostello marked this pull request as ready for review April 11, 2026 09:56
@martincostello martincostello requested a review from a team as a code owner April 11, 2026 09:56
Copilot AI review requested due to automatic review settings April 11, 2026 09:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes TraceContextPropagator.Extract() to reduce overhead during context extraction (particularly avoiding LINQ and using span-based parsing), and expands test coverage around carrier value shapes.

Changes:

  • Reworked TraceContextPropagator.Extract() to avoid LINQ and add helper paths for “single value” extraction.
  • Reimplemented tracestate extraction to support IEnumerable<string> directly, including an optimized single-header parsing path.
  • Added/updated unit tests to cover IReadOnlyList<string> and general IEnumerable<string> carrier values, plus new tracestate parsing cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs Removes LINQ from extraction, adds helpers, and introduces span-based tracestate parsing with an IEnumerable<string> overload.
test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs Updates getters/assertions and adds new tests validating extraction/parsing behavior for additional carrier value types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs Outdated
Comment thread src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs Outdated
@martincostello
Copy link
Copy Markdown
Member Author

martincostello commented Apr 11, 2026

Need to cross-check this against #6161/GHSA-8785-wc3w-h8q6 as well to make sure the issues there aren't re-introduced.

@martincostello
Copy link
Copy Markdown
Member Author

Prompt

A previous attempt to improve the performance this branch is trying to achieve cause GHSA-8785-wc3w-h8q6 and had to be reverted. Investigate whether the changes made here would create the same issues.

Response

Review agrees: this branch does not reintroduce GHSA-8785-wc3w-h8q6. It found the same root distinction: the vulnerable #5749 code used a non-progressing IndexOf(key) loop for duplicate
detection, while this branch uses bounded prior-key comparisons in the single-header path and HashSet in the
multi-header path. It also confirmed the new IEnumerable support is single-pass rather than
Count()/First()/Any()-driven. The only substantive gap is test coverage: there still isn’t an explicit GHSA-style
regression case where a later key appears inside an earlier value, such as foo=bar,bar=1.

Test code

test_dos.cs

using System;
using System.Diagnostics;
using System.Collections.Generic;
using OpenTelemetry.Context.Propagation;

// Test case: many keys where the key appears as substring in values
var propagator = new TraceContextPropagator();

// Create a tracestate with keys that appear as substrings in values
// This was the pathological case for the vulnerable version
var tracestateValue = string.Join(",", Enumerable.Range(0, 30).Select(i => $"key{i}=valuekey{i}key{i}key{i}"));

var carrier = new Dictionary<string, string[]>
{
    ["traceparent"] = new[] { "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01" },
    ["tracestate"] = new[] { tracestateValue }
};

var sw = Stopwatch.StartNew();
var ctx = propagator.Extract(default, carrier, static (c, name) => c.TryGetValue(name, out var v) ? v : Array.Empty<string>());
sw.Stop();

Console.WriteLine($"Time taken: {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"Valid: {ctx.ActivityContext.IsValid()}");
Console.WriteLine($"TraceState length: {ctx.ActivityContext.TraceState?.Length ?? 0}");

if (sw.ElapsedMilliseconds > 1000)
{
    Console.WriteLine("WARNING: Took longer than 1 second - potential DoS vulnerability!");
    Environment.Exit(1);
}
else
{
    Console.WriteLine("OK: Performance is acceptable");
}

test_pathological.cs

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using OpenTelemetry.Context.Propagation;

class Program
{
    static void Main()
    {
        TestPathologicalCase();
        TestLazyEnumerable();
    }

    static void TestPathologicalCase()
    {
        Console.WriteLine("Testing pathological case...");
        var propagator = new TraceContextPropagator();

        // Create a tracestate with keys that appear as substrings in values
        // This was the pathological case for the vulnerable version
        var tracestateValue = string.Join(",", Enumerable.Range(0, 30).Select(i => $"key{i}=valuekey{i}key{i}key{i}"));

        var carrier = new Dictionary<string, string[]>
        {
            ["traceparent"] = new[] { "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01" },
            ["tracestate"] = new[] { tracestateValue }
        };

        var sw = Stopwatch.StartNew();
        var ctx = propagator.Extract(default, carrier, static (c, name) => c.TryGetValue(name, out var v) ? v : Array.Empty<string>());
        sw.Stop();

        Console.WriteLine($"Time taken: {sw.ElapsedMilliseconds}ms");
        Console.WriteLine($"Valid: {ctx.ActivityContext.IsValid()}");
        Console.WriteLine($"TraceState length: {ctx.ActivityContext.TraceState?.Length ?? 0}");

        if (sw.ElapsedMilliseconds > 1000)
        {
            Console.WriteLine("FAIL: Took longer than 1 second - potential DoS vulnerability!");
            Environment.Exit(1);
        }
        else
        {
            Console.WriteLine("PASS: Performance is acceptable\n");
        }
    }

    static void TestLazyEnumerable()
    {
        Console.WriteLine("Testing lazy enumerable multiple enumeration...");
        var propagator = new TraceContextPropagator();

        int enumerationCount = 0;
        var lazyEnumerable = GenerateLazy(ref enumerationCount);

        var carrier = new Dictionary<string, IEnumerable<string>>
        {
            ["traceparent"] = new[] { "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01" },
            ["tracestate"] = lazyEnumerable
        };

        var sw = Stopwatch.StartNew();
        var ctx = propagator.Extract(default, carrier, static (c, name) => c.TryGetValue(name, out var v) ? v : Array.Empty<string>());
        sw.Stop();

        Console.WriteLine($"Time taken: {sw.ElapsedMilliseconds}ms");
        Console.WriteLine($"Enumeration count: {enumerationCount}");
        Console.WriteLine($"Valid: {ctx.ActivityContext.IsValid()}");

        if (enumerationCount > 1)
        {
            Console.WriteLine("FAIL: Enumerable was enumerated multiple times - potential performance issue!");
            Environment.Exit(1);
        }
        else if (sw.ElapsedMilliseconds > 1000)
        {
            Console.WriteLine("FAIL: Took longer than 1 second!");
            Environment.Exit(1);
        }
        else
        {
            Console.WriteLine("PASS: Enumerable was only enumerated once");
        }
    }

    static IEnumerable<string> GenerateLazy(ref int counter)
    {
        return GenerateInner();
        
        IEnumerable<string> GenerateInner()
        {
            counter++;
            Console.WriteLine($"  Lazy enumerable accessed (count: {counter})");
            yield return "key1=value1,key2=value2";
        }
    }
}

test_pathological.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="src\OpenTelemetry.Api\OpenTelemetry.Api.csproj" />
  </ItemGroup>
</Project>

@martincostello martincostello changed the title [OpenTelemetry] Optimize TraceContextPropagator [Api] Optimize TraceContextPropagator Apr 11, 2026
@martincostello martincostello marked this pull request as draft April 13, 2026 13:03
@martincostello

This comment was marked as resolved.

Refactor based on suggestions from Copilot to improve throughput and reduce allocations by avoiding LINQ.
Extend test coverage for `TraceContextPropagator`.
Address Copilot code review feedback.
Add test that appears to relate to GHSA-8785-wc3w-h8q6, although it passes before and after that change was reverted.
Fix-up the regression test so it actually detects the issue caused by open-telemetry#5749.
Add missing field.
@martincostello martincostello force-pushed the optimize-TraceContextPropagator branch from c509aa3 to d32ff17 Compare April 13, 2026 14:32
@martincostello martincostello marked this pull request as ready for review April 13, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:OpenTelemetry.Api Issues related to OpenTelemetry.Api NuGet package pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants