Skip to content

[OpenTelemetry] Use AlternateLookup#7467

Merged
martincostello merged 2 commits into
open-telemetry:mainfrom
martincostello:gh-5777
Jul 7, 2026
Merged

[OpenTelemetry] Use AlternateLookup#7467
martincostello merged 2 commits into
open-telemetry:mainfrom
martincostello:gh-5777

Conversation

@martincostello

@martincostello martincostello commented Jun 29, 2026

Copy link
Copy Markdown
Member

Fixes #5777

Changes

Optimize metrics tag using alternate lookup when targeting .NET 9+.

Benchmarks

I got Claude to write a throwaway benchmark (I don't think it's worth committing as it's very focussed on this exact change). Claude's summary plus the benchmark's code are below.

Summary

Avoids copying the incoming tags ReadOnlySpan into thread-static storage on the metrics hot path by looking the metric point up directly from the span via ConcurrentDictionary.GetAlternateLookup (net9.0+). Applies to both cumulative and the delta-with-reclaim lookup paths.

Benchmark: branch vs main baseline (net10.0)

MetricsLookupBenchmarks.CounterHotPathExistingSeries — recording into an already-existing time series (the steady-state lookup-hit path). Both runs executed on the same machine, sequentially, default job, MemoryDiagnoser enabled.

Labels Temporality main (baseline) branch (optimized) Δ Speed-up
1 Cumulative 35.01 ns 23.65 ns −11.4 ns 32%
2 Cumulative 49.55 ns 36.39 ns −13.2 ns 27%
3 Cumulative 67.01 ns 44.99 ns −22.0 ns 33%
5 Cumulative 82.87 ns 64.73 ns −18.1 ns 22%
7 Cumulative 110.20 ns 84.28 ns −25.9 ns 24%
1 Delta 46.49 ns 30.23 ns −16.3 ns 35%
2 Delta 62.51 ns 43.41 ns −19.1 ns 31%
3 Delta 87.72 ns 53.50 ns −34.2 ns 39%
5 Delta 115.56 ns 73.52 ns −42.0 ns 36%
7 Delta 153.14 ns 93.50 ns −59.6 ns 39%

Allocated: - (zero) on both sides in every case — the optimization is pure CPU; the hit path was already allocation-free (thread-static reuse), so this removes the per-tag array copy, not allocations.

Takeaways

  • Consistent ~22–39% faster on the lookup-hit hot path across tag counts and both temporalities.
  • Delta benefits more than Cumulative (~31–39% vs ~22–33%): delta's baseline does the thread-static copy plus wraps a throwaway Tags and resolves LookupData, so skipping the copy is proportionally bigger; absolute savings reach −60 ns at 7 tags.
  • Gains scale with label count, confirming the win is the avoided per-tag array copy (SplitToKeysAndValues).

Notes

  • Only the lookup-hit path is changed; the miss/creation path is unchanged by design.
  • Single-machine, single-run measurements — treat the percentages as solid directional evidence rather than ±1 ns precision.

Code

Expand to view
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Metrics;
using BenchmarkDotNet.Attributes;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Tests;

/*
This benchmark isolates the metrics lookup hot path for issue #5777.

It records measurements whose tag sets have already been seen (the common
steady-state), so every Add resolves to an existing MetricPoint. On .NET 9.0+
this exercises the ConcurrentDictionary alternate lookup that resolves the
MetricPoint directly from the incoming tags span (for both Cumulative and
Delta temporality), avoiding the copy of the tags into thread-static storage
and the construction of a Tags key.

Run on `main` and on the branch to compare.
*/

namespace Benchmarks.Metrics;

[MemoryDiagnoser]
#pragma warning disable CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup
public class MetricsLookupBenchmarks
#pragma warning restore CA1001 // Types that own disposable fields should be disposable - handled by GlobalCleanup
{
    private readonly string[] dimensionValues = ["Value1", "Value2", "Value3", "Value4", "Value5"];
    private Counter<long>? counter;
    private MeterProvider? meterProvider;
    private Meter? meter;
    private KeyValuePair<string, object?>[]? tags;

    [Params(1, 2, 3, 5, 7)]
    public int LabelsCount { get; set; }

    [Params(MetricReaderTemporalityPreference.Cumulative, MetricReaderTemporalityPreference.Delta)]
    public MetricReaderTemporalityPreference AggregationTemporality { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        this.meter = new Meter(Utils.GetCurrentMethodName());

        var exportedItems = new List<Metric>();
        this.meterProvider = Sdk.CreateMeterProviderBuilder()
            .AddMeter(this.meter.Name)
            .AddInMemoryExporter(exportedItems, metricReaderOptions =>
            {
                metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000;
                metricReaderOptions.TemporalityPreference = this.AggregationTemporality;
            })
            .Build();

        this.counter = this.meter.CreateCounter<long>("counter");

        // A single, fixed tag set so every recorded measurement hits an
        // existing MetricPoint (the lookup-hit fast path).
        this.tags = new KeyValuePair<string, object?>[this.LabelsCount];
        for (var i = 0; i < this.LabelsCount; i++)
        {
            this.tags[i] = new KeyValuePair<string, object?>($"DimName{i + 1}", this.dimensionValues[i % this.dimensionValues.Length]);
        }

        // Prime the time series so the first measured Add already resolves to
        // an existing MetricPoint.
        this.counter.Add(1, this.tags);
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        this.meter?.Dispose();
        this.meterProvider?.Dispose();
    }

    [Benchmark]
    public void CounterHotPathExistingSeries()
    {
        this.counter!.Add(100, this.tags!);
    }
}

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)

Optimize metrics tag using alternate lookup when targeting .NET 9+.

Contributes to open-telemetry#5777.
@github-actions github-actions Bot added the pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package label Jun 29, 2026
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (816ccc1) to head (f2b59ce).
⚠️ Report is 24 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #7467      +/-   ##
==========================================
+ Coverage   90.05%   90.06%   +0.01%     
==========================================
  Files         284      285       +1     
  Lines       15230    15256      +26     
==========================================
+ Hits        13716    13741      +25     
- Misses       1514     1515       +1     
Flag Coverage Δ
unittests-Project-Experimental 89.94% <100.00%> (-0.15%) ⬇️
unittests-Project-Stable 90.06% <100.00%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
src/OpenTelemetry/Metrics/AggregatorStore.cs 89.87% <100.00%> (+0.21%) ⬆️
src/OpenTelemetry/Metrics/Tags.cs 51.72% <100.00%> (+1.13%) ⬆️
src/OpenTelemetry/Metrics/TagsComparer.cs 100.00% <100.00%> (ø)

Add an alternate lookup-based fast path for delta temporality.
@martincostello martincostello marked this pull request as ready for review June 30, 2026 08:52
@martincostello martincostello requested a review from a team as a code owner June 30, 2026 08:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes the metrics tag-set lookup hot path for .NET 9+ by using ConcurrentDictionary.GetAlternateLookup to probe the existing time-series map directly from the incoming ReadOnlySpan<KeyValuePair<string, object?>>, avoiding the thread-static copy on lookup hits.

Changes:

  • Add TagsComparer implementing IAlternateEqualityComparer (NET9+) to enable span-based alternate lookups for Tags keys.
  • Extend Tags with span-based equality and span-based hash code computation to ensure alternate lookups are correct and consistent with Tags instances.
  • Use alternate lookup fast paths in AggregatorStore (both cumulative and delta-with-reclaim hit paths) and add unit tests covering the new comparer/hash semantics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/OpenTelemetry.Tests/Metrics/TagsTests.cs Adds tests for span-based Tags.Equals and span hash parity with instance hash.
test/OpenTelemetry.Tests/Metrics/TagsComparerTests.cs Adds NET9+ tests validating TagsComparer alternate hash/equals and an end-to-end ConcurrentDictionary alternate lookup hit.
src/OpenTelemetry/Metrics/TagsComparer.cs Introduces a singleton comparer enabling NET9+ alternate lookup from a tags span to Tags.
src/OpenTelemetry/Metrics/Tags.cs Adds span-based equality and ComputeHashCode(ReadOnlySpan<...>) to support alternate lookups.
src/OpenTelemetry/Metrics/AggregatorStore.cs Wires in TagsComparer and uses alternate lookups for NET9+ lookup-hit paths (cumulative and delta-with-reclaim).

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

@martincostello martincostello added the perf Performance related label Jun 30, 2026
@martincostello martincostello added this to the v1.17.0 milestone Jul 3, 2026
@martincostello martincostello added this pull request to the merge queue Jul 7, 2026
Merged via the queue into open-telemetry:main with commit 73f9785 Jul 7, 2026
75 checks passed
@martincostello martincostello deleted the gh-5777 branch July 7, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf Performance related pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[metrics] Optimize lookup costs using .NET 9 alternative lookup

3 participants