Skip to content

Conversation

@iliar-turdushev
Copy link
Contributor

@iliar-turdushev iliar-turdushev commented Nov 25, 2025

Fixes #5976

Changes

The PR moves BucketLookup tree to AggregateStore. A logic that builds BucketLookup tree and decides whether a linear or binary search should be used has been moved to a class called HistogramExplicitBounds.

Benchmark results

HistogramBenchmarks measure performance of histograms with explicit bucket boundaries. Below are sample benchmark results comparing performance "before" and "after".

BEFORE:

BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171)
AMD Ryzen AI 7 PRO 350 w/ Radeon 860M 2.00GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100
  [Host]     : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
  DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4

| Method                      | BoundCount | Mean      | Error     | StdDev     | Median    |
|---------------------------- |----------- |----------:|----------:|-----------:|----------:|
| HistogramHotPath            | 10         |  43.28 ns |  0.727 ns |   0.568 ns |  43.34 ns |
| HistogramWith1LabelHotPath  | 10         |  97.21 ns |  4.526 ns |  13.344 ns |  98.04 ns |
| HistogramWith3LabelsHotPath | 10         | 173.01 ns |  5.966 ns |  16.331 ns | 167.71 ns |
| HistogramWith5LabelsHotPath | 10         | 234.35 ns | 11.753 ns |  33.150 ns | 223.43 ns |
| HistogramWith7LabelsHotPath | 10         | 255.74 ns |  4.940 ns |   8.389 ns | 255.55 ns |
| HistogramHotPath            | 49         |  44.88 ns |  0.916 ns |   1.370 ns |  44.46 ns |
| HistogramWith1LabelHotPath  | 49         |  93.82 ns |  1.496 ns |   1.400 ns |  93.93 ns |
| HistogramWith3LabelsHotPath | 49         | 200.73 ns |  4.227 ns |  11.642 ns | 197.86 ns |
| HistogramWith5LabelsHotPath | 49         | 253.97 ns |  8.755 ns |  24.838 ns | 249.11 ns |
| HistogramWith7LabelsHotPath | 49         | 285.84 ns |  5.685 ns |  13.400 ns | 286.48 ns |
| HistogramHotPath            | 50         |  55.65 ns |  1.130 ns |   2.231 ns |  54.87 ns |
| HistogramWith1LabelHotPath  | 50         | 120.27 ns |  2.961 ns |   7.903 ns | 119.97 ns |
| HistogramWith3LabelsHotPath | 50         | 270.13 ns | 11.557 ns |  32.407 ns | 255.96 ns |
| HistogramWith5LabelsHotPath | 50         | 373.36 ns | 18.914 ns |  53.962 ns | 356.76 ns |
| HistogramWith7LabelsHotPath | 50         | 417.53 ns | 25.891 ns |  73.868 ns | 396.03 ns |
| HistogramHotPath            | 1000       |  86.23 ns |  2.101 ns |   5.821 ns |  84.36 ns |
| HistogramWith1LabelHotPath  | 1000       | 141.14 ns |  2.794 ns |   6.075 ns | 140.33 ns |
| HistogramWith3LabelsHotPath | 1000       | 702.68 ns | 26.049 ns |  75.987 ns | 681.34 ns |
| HistogramWith5LabelsHotPath | 1000       | 665.76 ns | 13.336 ns |  24.047 ns | 663.69 ns |
| HistogramWith7LabelsHotPath | 1000       | 874.01 ns | 52.336 ns | 154.314 ns | 829.17 ns |

AFTER:

BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171)
AMD Ryzen AI 7 PRO 350 w/ Radeon 860M 2.00GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100
  [Host]     : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
  DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4

| Method                      | BoundCount | Mean      | Error    | StdDev    | Median    |
|---------------------------- |----------- |----------:|---------:|----------:|----------:|
| HistogramHotPath            | 10         |  42.64 ns | 1.088 ns |  2.808 ns |  42.94 ns |
| HistogramWith1LabelHotPath  | 10         |  92.51 ns | 1.817 ns |  2.362 ns |  92.14 ns |
| HistogramWith3LabelsHotPath | 10         | 163.94 ns | 3.304 ns |  5.144 ns | 163.25 ns |
| HistogramWith5LabelsHotPath | 10         | 222.49 ns | 4.473 ns |  5.817 ns | 221.39 ns |
| HistogramWith7LabelsHotPath | 10         | 268.24 ns | 4.758 ns |  5.843 ns | 269.82 ns |
| HistogramHotPath            | 49         |  47.98 ns | 0.958 ns |  0.941 ns |  48.11 ns |
| HistogramWith1LabelHotPath  | 49         | 110.55 ns | 1.723 ns |  1.527 ns | 110.87 ns |
| HistogramWith3LabelsHotPath | 49         | 179.00 ns | 3.389 ns |  4.968 ns | 179.27 ns |
| HistogramWith5LabelsHotPath | 49         | 237.62 ns | 4.768 ns |  7.423 ns | 237.49 ns |
| HistogramWith7LabelsHotPath | 49         | 291.57 ns | 5.750 ns | 10.658 ns | 293.77 ns |
| HistogramHotPath            | 50         |  56.80 ns | 1.126 ns |  1.382 ns |  56.24 ns |
| HistogramWith1LabelHotPath  | 50         | 108.26 ns | 2.157 ns |  1.801 ns | 107.97 ns |
| HistogramWith3LabelsHotPath | 50         | 207.34 ns | 3.589 ns |  4.407 ns | 207.46 ns |
| HistogramWith5LabelsHotPath | 50         | 253.32 ns | 4.531 ns |  4.239 ns | 253.17 ns |
| HistogramWith7LabelsHotPath | 50         | 317.08 ns | 9.062 ns | 26.577 ns | 307.90 ns |
| HistogramHotPath            | 1000       |  85.44 ns | 1.714 ns |  1.974 ns |  85.29 ns |
| HistogramWith1LabelHotPath  | 1000       | 139.51 ns | 2.507 ns |  2.222 ns | 139.65 ns |
| HistogramWith3LabelsHotPath | 1000       | 306.76 ns | 5.566 ns |  9.748 ns | 305.89 ns |
| HistogramWith5LabelsHotPath | 1000       | 373.47 ns | 7.277 ns | 11.329 ns | 372.39 ns |
| HistogramWith7LabelsHotPath | 1000       | 424.75 ns | 8.376 ns | 15.104 ns | 423.88 ns |

The benchmark tests were run several times and the results were the same as above:

  • for a number of explicit bucket bounds (BoundsCount) less than 50 the performance "before" and "after" is more or less the same;
  • for BoundsCount >= 50 the performance "after" is better than "before", and the higher the cardinality of a metric the better performance.

Memory consumption

A test described in this comment was used to compare memory consumption "before" and "after":

  • memory consumption "before" is ~750-800 MB;
  • "after" - 170-180 MB.

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) - NOT APPLICABLE

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Nov 25, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

@github-actions github-actions bot added the pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package label Nov 25, 2025
@iliar-turdushev iliar-turdushev changed the title [DRAFT, DO NOT REVIEW] Move BucketLookup to AggregateStore [DRAFT, DO NOT REVIEW] Move BucketLookup tree to AggregateStore Nov 25, 2025
@iliar-turdushev iliar-turdushev changed the title [DRAFT, DO NOT REVIEW] Move BucketLookup tree to AggregateStore [DRAFT, DO NOT REVIEW] Move BucketLookup tree to AggregatorStore Nov 26, 2025
@iliar-turdushev iliar-turdushev changed the title [DRAFT, DO NOT REVIEW] Move BucketLookup tree to AggregatorStore Move BucketLookup tree to AggregatorStore Nov 26, 2025
@iliar-turdushev iliar-turdushev marked this pull request as ready for review November 26, 2025 15:29
@iliar-turdushev iliar-turdushev requested a review from a team as a code owner November 26, 2025 15:29
@martincostello martincostello changed the title Move BucketLookup tree to AggregatorStore [OpenTelemetry] Move BucketLookup tree to AggregatorStore Nov 26, 2025
@codecov
Copy link

codecov bot commented Nov 26, 2025

Codecov Report

❌ Patch coverage is 98.36066% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 86.73%. Comparing base (a685fee) to head (7b27dcb).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/OpenTelemetry/Metrics/AggregatorStore.cs 91.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #6715      +/-   ##
==========================================
- Coverage   86.87%   86.73%   -0.14%     
==========================================
  Files         258      259       +1     
  Lines       12057    12066       +9     
==========================================
- Hits        10474    10465       -9     
- Misses       1583     1601      +18     
Flag Coverage Δ
unittests-Project-Experimental 86.72% <98.36%> (+<0.01%) ⬆️
unittests-Project-Stable 86.69% <98.36%> (-0.06%) ⬇️

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

Files with missing lines Coverage Δ
...c/OpenTelemetry/Metrics/HistogramExplicitBounds.cs 100.00% <100.00%> (ø)
...nTelemetry/Metrics/MetricPoint/HistogramBuckets.cs 100.00% <100.00%> (ø)
...c/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs 94.17% <100.00%> (ø)
src/OpenTelemetry/Metrics/AggregatorStore.cs 85.63% <91.66%> (-1.87%) ⬇️

... and 4 files with indirect coverage changes

@iliar-turdushev
Copy link
Contributor Author

iliar-turdushev commented Nov 27, 2025

@martincostello @Kielek @cijothomas Codecov says that one changed line is not covered with tests, this is the line (you need to expand the AggregatorStore file and scroll to the line 779). The line wasn't covered with test even before my changes. How should we proceed? Can we merge the PR as-is? Thank you.

@iliar-turdushev
Copy link
Contributor Author

@martincostello @Kielek @cijothomas @rajkumar-rangaraj Gentle ping on this comment. Can we proceed with the PR and merge it? Thank you.

Copy link
Member

@rajkumar-rangaraj rajkumar-rangaraj left a comment

Choose a reason for hiding this comment

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

LGTM

@iliar-turdushev Thanks for your contributions!

Copy link

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

This PR improves the performance and memory efficiency of OpenTelemetry's histogram metrics by centralizing the BucketLookup tree structure. The bucket lookup logic (binary search tree and linear search) has been moved from individual MetricPoint instances to a shared HistogramExplicitBounds class stored in AggregatorStore, reducing memory consumption from ~750-800MB to ~170-180MB while maintaining or improving performance.

Key Changes

  • Created new HistogramExplicitBounds class to encapsulate bucket boundary lookup logic and BST construction
  • Modified AggregatorStore to maintain a single shared HistogramExplicitBounds instance for all metric points
  • Refactored HistogramBuckets to delegate bucket index lookups to HistogramExplicitBounds

Reviewed changes

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

Show a summary per file
File Description
src/OpenTelemetry/Metrics/HistogramExplicitBounds.cs New class encapsulating bucket boundary lookup with binary search tree for ≥50 boundaries
src/OpenTelemetry/Metrics/AggregatorStore.cs Centralizes HistogramExplicitBounds instance and passes it to all MetricPoint creations
src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs Updated constructor parameter from double[] to HistogramExplicitBounds
src/OpenTelemetry/Metrics/MetricPoint/HistogramBuckets.cs Removed bucket lookup logic and delegated to HistogramExplicitBounds
test/OpenTelemetry.Tests/Metrics/AggregatorTests.cs Updated tests to instantiate HistogramExplicitBounds objects
src/OpenTelemetry/CHANGELOG.md Documented performance and memory improvements

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

Comment on lines +56 to +65
for (var i = 0; i < bounds.Length; i++)
{
if (double.IsNegativeInfinity(bounds[i]) || double.IsPositiveInfinity(bounds[i]))
{
return bounds
.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b))
.ToArray();
}
}

Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The CleanUpInfinitiesFromExplicitBounds method performs two passes over the array when infinities are found: first to detect them, then LINQ operations (Where + ToArray) to filter them. For better performance, consider collecting filtered values in a single pass using a List or ArrayPool, especially since this runs in the hot path during construction.

Suggested change
for (var i = 0; i < bounds.Length; i++)
{
if (double.IsNegativeInfinity(bounds[i]) || double.IsPositiveInfinity(bounds[i]))
{
return bounds
.Where(b => !double.IsNegativeInfinity(b) && !double.IsPositiveInfinity(b))
.ToArray();
}
}
List<double>? filtered = null;
for (var i = 0; i < bounds.Length; i++)
{
var b = bounds[i];
if (double.IsNegativeInfinity(b) || double.IsPositiveInfinity(b))
{
if (filtered == null)
{
filtered = new List<double>(bounds.Length - 1);
for (var j = 0; j < i; j++)
{
filtered.Add(bounds[j]);
}
}
// skip this value
}
else
{
if (filtered != null)
{
filtered.Add(b);
}
}
}
if (filtered != null)
{
return filtered.ToArray();
}

Copilot uses AI. Check for mistakes.
Copy link
Member

@Kielek Kielek left a comment

Choose a reason for hiding this comment

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

LGTM.
@iliar-turdushev, I left one comment opened from Copilot for your consideration. If you thin that it will be beneficial, please consider creating follow up PR,

Adding to merge queue.

@Kielek Kielek enabled auto-merge December 4, 2025 09:32
This was referenced Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:OpenTelemetry Issues related to OpenTelemetry NuGet package ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Metrics][Perf] Move BucketLookup tree to AggregatorStore to reduce memory usage

5 participants