Skip to content

PoC: Bound Instruments: Add WithAttributes to instruments.#7790

Draft
dashpole wants to merge 14 commits intoopen-telemetry:mainfrom
dashpole:bound_instrument_poc
Draft

PoC: Bound Instruments: Add WithAttributes to instruments.#7790
dashpole wants to merge 14 commits intoopen-telemetry:mainfrom
dashpole:bound_instrument_poc

Conversation

@dashpole
Copy link
Contributor

@dashpole dashpole commented Jan 15, 2026

Prototype for open-telemetry/opentelemetry-specification#4835

Notes for reviewers

  • For examples of what usage of this API looks like, see changes in metric/example_test.go
  • This currently destroys the performance of the existing WithAttributes and WithAttributeSet options, but that can be easily fixed.
  • The delegating global API will have an allocation to store attributes. It has to do this because it can't/shouldn't store state. I don't think this is any worse than it is today, but isn't captured in these benchmarks.
  • I chose not to allow binding async instruments outside of a callback, but to allow binding observables within a callback. This allows an interesting pattern where it is easy to record multiple metrics with the same attributes. We could also allow binding async instruments outside of a callback if we wanted, with some added complexity. I'm not sure it is worth it given the performance-insensitive nature of async instruments.
  • This currently maintains an additional sync.Map for storing bound measure functions. This duplication could be removed with some refactoring.

TODO:

  • Demonstrate that this can work for delta as well.

Benchmarks

  • NoFilter + Precomputed: 48ns -> 12 ns; 0 allocs -> 0 allocs
  • NoFilter + Dynamic: 713ns -> 330 ns; 2 allocs -> 0 allocs
  • NoFilter + Naive: 881ns -> 528 ns; 5 allocs -> 1 allocs
  • Filtered + Precomputed: 255ns -> 12 ns; 2 allocs -> 0 allocs
  • Filtered + Dynamic: 1602ns -> 334 ns; 4 allocs -> 0 allocs
  • Filtered + Naive: 1608ns -> 537 ns; 7 allocs -> 1 allocs
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/internal/benchmark
cpu: AMD EPYC 7B12
                                                               │    main.txt    │                bound.txt                │
                                                               │     sec/op     │     sec/op      vs base                 │
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet      47.79n ±  1%    223.55n ±  1%   +367.78% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes        47.70n ±  2%     11.76n ±  2%    -75.34% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet          277.9n ±  3%     446.6n ±  7%    +60.72% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes            349.1n ±  3%     102.6n ±  6%    -70.61% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Naive/WithAttributes              326.5n ±  2%     137.2n ± 18%    -57.96% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet      47.79n ±  4%    698.30n ±  8%  +1361.03% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes        48.02n ±  2%     11.72n ±  3%    -75.61% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet          713.1n ±  2%    1415.0n ± 10%    +98.44% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes            904.5n ± 16%     330.4n ±  1%    -63.47% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Naive/WithAttributes              881.8n ±  3%     528.3n ± 15%    -40.09% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet     47.23n ±  4%   1281.00n ±  4%  +2611.97% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes       47.67n ±  1%     11.87n ±  2%    -75.10% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet         1.259µ ±  5%     2.600µ ±  1%   +106.56% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes          1512.0n ±  4%     621.5n ±  2%    -58.90% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Naive/WithAttributes             1.569µ ±  8%     1.014µ ±  7%    -35.35% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet      255.9n ±  3%     525.2n ±  2%   +105.20% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes       254.50n ±  3%     11.92n ±  2%    -95.31% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet          482.8n ±  3%     764.8n ±  6%    +58.39% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes            587.5n ±  3%     102.0n ±  2%    -82.63% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Naive/WithAttributes              578.5n ±  9%     128.7n ±  8%    -77.76% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet      790.0n ±  3%    2100.0n ± 10%   +165.81% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes       770.55n ± 11%     11.97n ±  4%    -98.45% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet          1.602µ ± 21%     2.913µ ±  3%    +81.86% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes           1639.5n ±  3%     334.5n ±  3%    -79.60% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Naive/WithAttributes             1608.0n ±  1%     537.3n ±  4%    -66.59% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet     1.479µ ±  8%     4.217µ ± 34%   +185.19% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes     1485.50n ±  5%     11.86n ±  2%    -99.20% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet         2.710µ ±  3%     5.382µ ±  6%    +98.62% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes          2994.0n ±  5%     615.6n ±  2%    -79.44% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Naive/WithAttributes             3.027µ ± 69%     1.018µ ±  6%    -66.39% (p=0.002 n=6)
geomean                                                            500.1n           294.3n          -41.15%

                                                               │    main.txt    │               bound.txt                │
                                                               │      B/op      │     B/op      vs base                  │
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet        0.0 ± 0%       112.0 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes        0.000 ± 0%       0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet          88.00 ± 0%      200.00 ± 0%  +127.27% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes            168.0 ± 0%         0.0 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Naive/WithAttributes             232.00 ± 0%       64.00 ± 0%   -72.41% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet        0.0 ± 0%       368.0 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes        0.000 ± 0%       0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet          344.0 ± 0%       712.0 ± 0%  +106.98% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes            680.0 ± 0%         0.0 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Naive/WithAttributes             1000.0 ± 0%       320.0 ± 0%   -68.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet       0.0 ± 0%       752.0 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes       0.000 ± 0%       0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet         728.0 ± 0%      1480.0 ± 0%  +103.30% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes         1.414Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Naive/WithAttributes            2152.0 ± 0%       704.0 ± 0%   -67.29% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet      64.00 ± 0%      240.00 ± 0%  +275.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes        64.00 ± 0%        0.00 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet          152.0 ± 0%       328.0 ± 0%  +115.79% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes            232.0 ± 0%         0.0 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Naive/WithAttributes             296.00 ± 0%       64.00 ± 0%   -78.38% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet      576.0 ± 0%      1520.0 ± 0%  +163.89% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes        576.0 ± 0%         0.0 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet          920.0 ± 0%      1864.0 ± 0%  +102.61% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes          1.227Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Naive/WithAttributes             1576.0 ± 0%       320.0 ± 0%   -79.70% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet   1.312Ki ± 0%     3.359Ki ± 0%  +155.95% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes     1.312Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet       2.023Ki ± 0%     4.070Ki ± 0%  +101.16% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes         2.727Ki ± 0%     0.000Ki ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Naive/WithAttributes            3496.0 ± 0%       704.0 ± 0%   -79.86% (p=0.002 n=6)
geomean                                                                       ²                 ?                      ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                                               │   main.txt   │              bound.txt               │
                                                               │  allocs/op   │ allocs/op   vs base                  │
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributeSet    0.000 ± 0%     2.000 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Precomputed/WithAttributes      0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributeSet        2.000 ± 0%     4.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Dynamic/WithAttributes          4.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/1/Naive/WithAttributes            5.000 ± 0%     1.000 ± 0%   -80.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributeSet    0.000 ± 0%     2.000 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Precomputed/WithAttributes      0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributeSet        2.000 ± 0%     4.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Dynamic/WithAttributes          4.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/5/Naive/WithAttributes            5.000 ± 0%     1.000 ± 0%   -80.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributeSet   0.000 ± 0%     2.000 ± 0%         ? (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Precomputed/WithAttributes     0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=6) ¹
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributeSet       2.000 ± 0%     4.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Dynamic/WithAttributes         4.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/NoFilter/Attributes/10/Naive/WithAttributes           5.000 ± 0%     1.000 ± 0%   -80.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet    1.000 ± 0%     4.000 ± 0%  +300.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes      1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet        3.000 ± 0%     6.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes          5.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/1/Naive/WithAttributes            6.000 ± 0%     1.000 ± 0%   -83.33% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet    2.000 ± 0%     6.000 ± 0%  +200.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes      2.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet        4.000 ± 0%     8.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes          6.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/5/Naive/WithAttributes            7.000 ± 0%     1.000 ± 0%   -85.71% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet   2.000 ± 0%     6.000 ± 0%  +200.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes     2.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet       4.000 ± 0%     8.000 ± 0%  +100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes         6.000 ± 0%     0.000 ± 0%  -100.00% (p=0.002 n=6)
CounterAdd/Filtered/Attributes/10/Naive/WithAttributes           7.000 ± 0%     1.000 ± 0%   -85.71% (p=0.002 n=6)
geomean                                                                     ²               ?                      ²
¹ all samples are equal
² summaries must be >0 to compute geomean

@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

❌ Patch coverage is 62.75862% with 108 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.3%. Comparing base (aec1082) to head (c8fc6a1).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
sdk/metric/instrument.go 20.5% 54 Missing ⚠️
attribute/set.go 0.0% 15 Missing ⚠️
sdk/metric/internal/aggregate/sum.go 39.1% 14 Missing ⚠️
internal/global/instruments.go 73.4% 11 Missing and 2 partials ⚠️
sdk/metric/meter.go 64.7% 12 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##            main   #7790     +/-   ##
=======================================
- Coverage   86.1%   81.3%   -4.8%     
=======================================
  Files        302     304      +2     
  Lines      22046   23439   +1393     
=======================================
+ Hits       18992   19073     +81     
- Misses      2674    3981   +1307     
- Partials     380     385      +5     
Files with missing lines Coverage Δ
internal/global/meter.go 99.3% <100.0%> (+<0.1%) ⬆️
metric/asyncfloat64.go 100.0% <ø> (ø)
metric/asyncint64.go 100.0% <ø> (ø)
metric/noop/noop.go 100.0% <100.0%> (ø)
metric/syncfloat64.go 100.0% <ø> (ø)
metric/syncint64.go 100.0% <ø> (ø)
sdk/metric/internal/aggregate/aggregate.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/atomic.go 88.5% <100.0%> (-1.5%) ⬇️
...metric/internal/aggregate/exponential_histogram.go 97.4% <100.0%> (+<0.1%) ⬆️
sdk/metric/internal/aggregate/histogram.go 100.0% <100.0%> (ø)
... and 7 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@MrAlias MrAlias mentioned this pull request Jan 16, 2026
39 tasks
@dashpole dashpole force-pushed the bound_instrument_poc branch from 8d8ccaa to 877267a Compare January 16, 2026 21:09
@dashpole dashpole force-pushed the bound_instrument_poc branch from a8e61ed to 92a47d3 Compare January 20, 2026 17:20
@dashpole dashpole force-pushed the bound_instrument_poc branch from 92a47d3 to 3c5d935 Compare January 20, 2026 17:35
@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

What about adding WithAttributes to be an InstrumentOption to bind attributes to an instrument?

}
http.HandleFunc("/", func(_ http.ResponseWriter, r *http.Request) {
apiCounter.Add(r.Context(), 1)
apiCounter.WithAttributes(attribute.String("http.method", r.Method)).Add(r.Context(), 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this will also need an externally managed sync.Pool to avoid the []attribute.KeyValue allocation here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes,. This is currently the "naive" benchmark case from the description, where users just use the API in the simplest way. The performance without the pooling optimization (in either case) does improve with the bound instrument pattern, but does still have an allocation as you point out:

  • NoFilter + Naive: 881ns -> 528 ns; 5 allocs -> 1 allocs
  • Filtered + Naive: 1608ns -> 537 ns; 7 allocs -> 1 allocs

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm still not following the logic here.

The criticism of the current API is that users who want to use the API in a performant way need to manage an external sync.Pool or cache.

For users to use the proposed API here they will need to use an external sync.Pool or cache.

Why doesn't the same criticism apply to the new API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't consider sync.Pools and a sync.Map to be the same FWIW.

I would summarize my criticisms of the current API as:

  • Slow with two allocations even when you use sync.Pools.
  • Really slow, and has lots of allocations if you don't use sync.Pools.

The bound instrument API is:

  • Fast, and has no allocations when you use sync.Pools.
  • Slower, with one allocation, if you don't use sync.pools.

@dashpole
Copy link
Contributor Author

dashpole commented Jan 22, 2026

What about adding WithAttributes to be an InstrumentOption to bind attributes to an instrument?

That is also an option. It will improve the performance of the "Precomputed" case in-line with this PR. It will not improve the performance of the "Dynamic" or "Naive" case as the attributes are still provided using the option pattern.

@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

the "Precomputed" case in-line

Is this a real use case?

If attributes are pre-computed why are they being added in line?

@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

It will not improve the performance of the "Dynamic" or "Naive" case as the attributes are still provided using the option pattern.

This is not a valid assertion: #7770 (comment)

@dashpole
Copy link
Contributor Author

Sorry, by "in-line" I just meant "similar to the results of". I.e. "It will improve the performance of the "Precomputed" case similar to the results of this PR." It would have this result:

  • NoFilter + Precomputed: 48ns -> 12 ns; 0 allocs -> 0 allocs
  • NoFilter + Dynamic: No Change
  • NoFilter + Naive: No Change
  • Filtered + Precomputed: 255ns -> 12 ns; 2 allocs -> 0 allocs
  • Filtered + Dynamic: No Change
  • Filtered + Naive: No Change

@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

by "in-line" I just meant "similar to the results of". I.e. "It will improve the performance of the "Precomputed" case similar to the results of this PR."

I see, thanks for the clarification 👍

@dashpole
Copy link
Contributor Author

This is not a valid assertion: #7770 (comment)

I believe my point is correct that moving the option from the counter.Add call to the meter.NewInt64Counter call would not make a difference in the Dynamic or Naive case, regardless of whether we use a sync.Map for caching or not.

Lets move the caching discussion to #7768, if that is OK, and make sure we agree on what our recommendation is for performant code with dynamic attributes.

@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

It seems like the motivation proposed in this PRs description is based on performance. I'm happy to have a consolidated discussion about performance in #7768, but it means that this PR needs to be motivated solely on ergonimics and indiomatic code which motivates #7790 (comment) even more.

@dashpole
Copy link
Contributor Author

dashpole commented Jan 22, 2026

I just want to agree on how we measure the performance of our recommended usage of the API + SDK in #7768. This PR can still be motivated by performance, if it shows performance improvements over the current benchmarks. Once that is merged, we can have more productive discussions about alternatives.

@MrAlias
Copy link
Contributor

MrAlias commented Jan 22, 2026

I just want to agree on how we measure the performance of our recommended usage of the API + SDK in #7768. This PR can still be motivated by performance, if it shows performance improvements over the current benchmarks. Once that is merged, we can have more productive discussions about alternatives.

... well it seems like that means my comments would still apply(?). If we want to motivate this approach based on performance it needs to show clear and considerable performance improvements over other API designs, including the existing one, when both designs are used as optimal as possible in real use-cases.

Comment on lines +110 to +134
b.Run("Dynamic/WithAttributeSet", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Wrap in a function so we can use defer.
func() {
attrsSlice := attrPool.Get().(*[]attribute.KeyValue)
defer func() {
*attrsSlice = (*attrsSlice)[:0] // Reset.
attrPool.Put(attrsSlice)
}()
appendAttributes(attrsLen, attrsSlice)
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
set := attribute.NewSet(*attrsSlice...)
*addOpt = append(*addOpt, metric.WithAttributeSet(set))
counter.Add(ctx, 1, *addOpt...)
}()
}
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like we are talking in circles at this point 😑

Suggested change
b.Run("Dynamic/WithAttributeSet", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Wrap in a function so we can use defer.
func() {
attrsSlice := attrPool.Get().(*[]attribute.KeyValue)
defer func() {
*attrsSlice = (*attrsSlice)[:0] // Reset.
attrPool.Put(attrsSlice)
}()
appendAttributes(attrsLen, attrsSlice)
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
set := attribute.NewSet(*attrsSlice...)
*addOpt = append(*addOpt, metric.WithAttributeSet(set))
counter.Add(ctx, 1, *addOpt...)
}()
}
})
})
var optCache sync.Map
b.Run("Dynamic/WithAttributeSet", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Wrap in a function so we can use defer.
func() {
attrsSlice := attrPool.Get().(*[]attribute.KeyValue)
defer func() {
*attrsSlice = (*attrsSlice)[:0] // Reset.
attrPool.Put(attrsSlice)
}()
appendAttributes(attrsLen, attrsSlice)
addOpt := addOptPool.Get().(*[]metric.AddOption)
defer func() {
*addOpt = (*addOpt)[:0]
addOptPool.Put(addOpt)
}()
d := attribute.NewDistinct(attrs...)
set, loaded := setCache.Load(d)
if !loaded {
set, _ = setCache.LoadOrStore(d, attribute.NewSet(attrs...))
}
*addOpt = append(*addOpt, metric.WithAttributeSet(set))
counter.Add(ctx, 1, *addOpt...)
}()
}
})
})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@MrAlias MrAlias left a comment

Choose a reason for hiding this comment

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

I'm going to block this so it is clear that as a Maintainer of this code I do not agree this is the direction we should take the API.

This is adding an additional and orthogonal approach to configuring attributes being recorded during measurements. It is contrary to our policy and all other users of our options.

The conversation on performance motivating this PR remains hyperbolic. I have not found it an effective argument. I do not see it motivating this change as the argument has been laid out.

In regards to prototyping an API change that would motivate bound attributes at the OpenTelemetry specification level, I see this proposal as a viable alternative. I can understand the value of bound instruments and this PR review in no way should be understood as opposition to finding a solution that would support this feature.

@dashpole
Copy link
Contributor Author

What about adding WithAttributes to be an InstrumentOption to bind attributes to an instrument?

This does not address the issue described in #7743 at all. That proposal would improve the precomputed case in similar fashion to this PoC, but doesn't alleviate the core issue i'm interested in.

The conversation on performance motivating this PR remains hyperbolic.

I'm sorry. I've put the cart before the horse a bit here. From my point of view, we just have differences of opinion about what the correct apples-to-apples benchmarks are. I thought I was on solid footing copying guidance from our contributing guide, but clearly you disagree. If you accept my benchmarks, then these results are actually really good, and (IMO) would justify a new API method.

But yeah, if all of this was just to shave the precomputed case from 42 ns to 12 ns, it wouldn't be enough of a performance improvement to be worth it.

Please take a look at #7743 (comment) when you have time. If we can't find agreement there, then we can close the issue and related PRs and focus on more productive things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments