Skip to content

Lazily compute filtered and dropped attributes only when required#8230

Open
dashpole wants to merge 11 commits into
open-telemetry:mainfrom
dashpole:lasy_dropped
Open

Lazily compute filtered and dropped attributes only when required#8230
dashpole wants to merge 11 commits into
open-telemetry:mainfrom
dashpole:lasy_dropped

Conversation

@dashpole
Copy link
Copy Markdown
Collaborator

@dashpole dashpole commented Apr 19, 2026

Part of #7743

This takes a slightly different approach from #8179, and is first just trying to apply some of the optimizations to the current WithAttributeSet / WithAttributes path.

The basic optimization pattern is that we pass the original attribute.Set and the attribute.Filter as far as possible to defer creating the filtered and dropped attributes until absolutely necessary. We use a new type, attribute.LazyFilteredSet, to encapsulate this state and guarantee that the filter is evaluated exactly once.

There are two places where exemplars can be discarded: By the ExemplarFilter (e.g. AlwaysOn vs AlwaysOff vs TraceBased), and by the ExemplarReservoir itself (the time-weighted algorithm discards Offer calls). The current Filtered benchmark only exercises the first filter by passing an AlwaysOff filter. I've added a FilteredWithExemplars case to demonstrate improvements to the case where exemplars are Offered to the reservoir.

Public API Additions

  • In attribute: LazyFilteredSet type.
    • Used to support efficient, lazy evaluation of filtered attributes and their hash, guaranteeing that the filter is evaluated exactly once.
  • In sdk/metric/exemplar: FixedSizeReservoir.OfferLazy takes an attribute.LazyFilteredSet instead of an attribute.Set and attribute.Filter.
    • Used to avoid allocations unless the exemplar is sampled by delaying computing the dropped attribute slice, using the cached decisions in LazyFilteredSet.

Considerations and Alternatives

The above public API additions are specific to the path where attributes are provided using attribute.Set. For a future x.WithUnsafeAttributes, where the attributes are passed as a slice, we would need to add equivalent lazy evaluation mechanisms for slices. Unfortunately, you lose all of the performance gains in this PR if we have to convert the attribute.Set to an []attribute.KeyValue before getting the Distinct or before Offering the attributes to the reservoir.

Alternative considered: We could skip this PR, and just focus on optimizing the []attribute.KeyValue path, since that will eventually be more performant, but it seems valuable to me to get this performance win without the new option.

Alternative considered: We originally considered adding set.NewDistinctWithFilter(filter attribute.Filter) to compute the hash of a filtered attribute set without recomputing the full set. However, this resulted in multiple evaluations of the filter function. We replaced it with LazyFilteredSet to ensure the filter is evaluated exactly once and to cleanly encapsulate all state needed for lazy evaluation.

Benchmarks

This PR removes one allocation from the Filtered path across the board (from computing the new attribute.Set after a filter is applied), and removes a second allocation from the FilteredWithExemplars (new) path by not computing the dropped attribute set unless the reservoir samples the observation.

For Precomputed cases, this eliminates most of the performance difference between the NoFilter and Filtered or FilteredWithExemplars cases -- meaning applying a filter to a metric no longer imposes an ~order-of-magnitude performance penalty, and is now just slightly slower

For dynamic cases, this is a substantial improvement, but the cost of attribute.NewSet accounts for most of the difference now (as was already the case in the NoFilter cases).

goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/metric
cpu: AMD EPYC 7B12
                                                                                       │   main.txt    │               new.txt               │
                                                                                       │    sec/op     │    sec/op     vs base               │
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24                 119.80n ±  3%   59.12n ± 17%  -50.65% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24                   118.45n ±  4%   57.06n ± 13%  -51.83% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24                      218.5n ±  4%   150.8n ± 11%  -31.02% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24                        216.8n ±  2%   151.9n ±  2%  -29.90% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                          295.9n ±  5%   230.0n ±  9%  -22.29% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24                 435.00n ±  3%   62.65n ±  7%  -85.60% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24                   436.15n ±  3%   62.00n ±  5%  -85.79% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24                      855.1n ±  4%   441.0n ±  3%  -48.42% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24                        844.8n ±  3%   433.4n ±  9%  -48.69% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                         1163.0n ±  5%   753.5n ±  3%  -35.21% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24                863.15n ±  9%   70.49n ±  8%  -91.83% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24                  870.30n ±  3%   68.26n ±  4%  -92.16% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24                    1661.5n ±  5%   828.6n ±  8%  -50.13% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24                      1655.0n ± 11%   826.2n ±  5%  -50.08% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24                         2.286µ ± 12%   1.536µ ±  3%  -32.83% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributeSet-24     227.7n ±  5%   187.4n ±  1%  -17.68% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributes-24       227.3n ±  2%   185.9n ±  5%  -18.23% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributeSet-24         309.5n ±  6%   281.8n ±  9%   -8.97% (p=0.015 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributes-24           334.1n ±  7%   283.4n ± 20%  -15.17% (p=0.015 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Naive/WithAttributes-24             382.1n ±  3%   336.2n ±  5%  -12.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributeSet-24     521.1n ±  4%   200.4n ±  3%  -61.54% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributes-24       527.8n ± 13%   202.3n ±  2%  -61.66% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributeSet-24         904.8n ±  7%   540.1n ±  8%  -40.31% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributes-24           918.5n ±  4%   533.0n ± 10%  -41.97% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Naive/WithAttributes-24            1216.5n ±  4%   862.9n ±  3%  -29.07% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributeSet-24    991.5n ±  5%   206.9n ±  1%  -79.13% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributes-24      925.6n ±  6%   205.0n ± 43%  -77.85% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributeSet-24       1693.5n ±  8%   895.5n ±  5%  -47.12% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributes-24         1644.0n ±  9%   950.9n ±  5%  -42.16% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Naive/WithAttributes-24            2.365µ ±  8%   1.620µ ±  3%  -31.50% (p=0.002 n=6)
geomean                                                                                   617.1n         280.6n        -54.53%

                                                                                       │   main.txt   │                 new.txt                  │
                                                                                       │     B/op     │     B/op      vs base                    │
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24                   64.00 ± 0%      0.00 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24                     64.00 ± 0%      0.00 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24                      128.00 ± 0%     64.00 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24                        128.00 ± 0%     64.00 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                           296.0 ± 0%     232.0 ± 0%   -21.62% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24                   576.0 ± 0%       0.0 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24                     576.0 ± 0%       0.0 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24                       899.0 ± 0%     321.0 ± 0%   -64.29% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24                         899.0 ± 0%     321.0 ± 0%   -64.29% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                          1576.0 ± 0%    1000.0 ± 0%   -36.55% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24                1.312Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24                  1.312Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24                     2053.0 ± 0%     706.0 ± 0%   -65.61% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24                       2053.0 ± 0%     706.0 ± 0%   -65.61% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24                        3.414Ki ± 0%   2.102Ki ± 0%   -38.44% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributeSet-24      64.00 ± 0%      0.00 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributes-24        64.00 ± 0%      0.00 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributeSet-24         128.00 ± 0%     64.00 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributes-24           128.00 ± 0%     64.00 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Naive/WithAttributes-24              296.0 ± 0%     232.0 ± 0%   -21.62% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributeSet-24      576.0 ± 0%       0.0 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributes-24        576.0 ± 0%       0.0 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributeSet-24          899.0 ± 0%     321.0 ± 0%   -64.29% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributes-24            899.0 ± 0%     321.0 ± 0%   -64.29% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Naive/WithAttributes-24             1576.0 ± 0%    1000.0 ± 0%   -36.55% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributeSet-24   1.312Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributes-24     1.312Ki ± 0%   0.000Ki ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributeSet-24        2053.5 ± 0%     707.0 ± 0%   -65.57% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributes-24          2053.0 ± 0%     707.0 ± 0%   -65.56% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Naive/WithAttributes-24           3.414Ki ± 0%   2.102Ki ± 0%   -38.44% (p=0.002 n=6)
geomean                                                                                    571.0                      ?                      ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

                                                                                       │  main.txt  │                new.txt                 │
                                                                                       │ allocs/op  │ allocs/op   vs base                    │
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributeSet-24                 1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Precomputed/WithAttributes-24                   1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributeSet-24                     2.000 ± 0%   1.000 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Dynamic/WithAttributes-24                       2.000 ± 0%   1.000 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/1/Naive/WithAttributes-24                         6.000 ± 0%   5.000 ± 0%   -16.67% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributeSet-24                 2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Precomputed/WithAttributes-24                   2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributeSet-24                     3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Dynamic/WithAttributes-24                       3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/5/Naive/WithAttributes-24                         7.000 ± 0%   5.000 ± 0%   -28.57% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributeSet-24                2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Precomputed/WithAttributes-24                  2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributeSet-24                    3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Dynamic/WithAttributes-24                      3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/Filtered/Attributes/10/Naive/WithAttributes-24                        7.000 ± 0%   5.000 ± 0%   -28.57% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributeSet-24    1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Precomputed/WithAttributes-24      1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributeSet-24        2.000 ± 0%   1.000 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Dynamic/WithAttributes-24          2.000 ± 0%   1.000 ± 0%   -50.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/1/Naive/WithAttributes-24            6.000 ± 0%   5.000 ± 0%   -16.67% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributeSet-24    2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Precomputed/WithAttributes-24      2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributeSet-24        3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Dynamic/WithAttributes-24          3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/5/Naive/WithAttributes-24            7.000 ± 0%   5.000 ± 0%   -28.57% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributeSet-24   2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Precomputed/WithAttributes-24     2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributeSet-24       3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Dynamic/WithAttributes-24         3.000 ± 0%   1.000 ± 0%   -66.67% (p=0.002 n=6)
EndToEndCounterAdd/FilteredWithExemplars/Attributes/10/Naive/WithAttributes-24           7.000 ± 0%   5.000 ± 0%   -28.57% (p=0.002 n=6)
geomean                                                                                  2.583                    ?                      ¹ ²
¹ summaries must be >0 to compute geomean
² ratios must be >0 to compute geomean

Gemini helped implement this.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 19, 2026

Codecov Report

❌ Patch coverage is 98.35165% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.1%. Comparing base (4086701) to head (1bb90a9).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
attribute/lazy_set.go 98.2% 1 Missing and 1 partial ⚠️
sdk/metric/internal/aggregate/drop.go 0.0% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##            main   #8230     +/-   ##
=======================================
+ Coverage   82.9%   83.1%   +0.1%     
=======================================
  Files        314     315      +1     
  Lines      24985   25126    +141     
=======================================
+ Hits       20730   20882    +152     
+ Misses      3882    3870     -12     
- Partials     373     374      +1     
Files with missing lines Coverage Δ
sdk/metric/exemplar/fixed_size_reservoir.go 95.9% <100.0%> (+1.1%) ⬆️
sdk/metric/internal/aggregate/aggregate.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/atomic.go 90.4% <100.0%> (+0.4%) ⬆️
...metric/internal/aggregate/exponential_histogram.go 100.0% <100.0%> (ø)
...dk/metric/internal/aggregate/filtered_reservoir.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/histogram.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/lastvalue.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/sum.go 100.0% <100.0%> (ø)
sdk/metric/internal/aggregate/drop.go 83.3% <0.0%> (ø)
attribute/lazy_set.go 98.2% <98.2%> (ø)

... and 2 files with indirect coverage changes

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

@dashpole dashpole force-pushed the lasy_dropped branch 2 times, most recently from 56e479f to 18523eb Compare April 19, 2026 18:34
Comment thread sdk/metric/internal/aggregate/exponential_histogram_test.go Outdated
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go Outdated
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go Outdated
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go Outdated
Comment thread sdk/metric/internal/aggregate/histogram.go Outdated
Comment thread sdk/metric/internal/aggregate/histogram.go Outdated
Comment thread sdk/metric/internal/aggregate/lastvalue.go Outdated
Comment thread sdk/metric/internal/aggregate/lastvalue.go
Comment thread sdk/metric/meter_test.go Outdated
@dashpole dashpole force-pushed the lasy_dropped branch 3 times, most recently from 077b835 to a23ccc6 Compare April 19, 2026 19:33
@dashpole dashpole requested a review from Copilot April 19, 2026 19:45
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

This PR optimizes the metrics SDK’s attribute-filtered measurement path by deferring creation of filtered sets and dropped-attribute slices until they’re actually needed (notably, only when an exemplar is sampled). It also introduces small public API additions to support hashing and lazy exemplar offering without forcing intermediate allocations.

Changes:

  • Add attribute.Set.NewDistinctWithFilter to compute a Distinct hash for a filtered view of a set without constructing a new filtered attribute.Set.
  • Thread the original attribute.Set + attribute.Filter deeper into aggregators and exemplar offering, deferring filtered-set/dropped-slice construction.
  • Add FixedSizeReservoir.OfferLazy and wire FilteredExemplarReservoir to use it when supported, plus add a new benchmark scenario for filtered exemplars.

Reviewed changes

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

Show a summary per file
File Description
attribute/hash.go Adds Set.NewDistinctWithFilter to hash filtered attributes without building a filtered set.
attribute/hash_test.go Adds unit tests for NewDistinctWithFilter behavior.
sdk/metric/exemplar/fixed_size_reservoir.go Adds OfferLazy and refactors sampling logic to compute dropped attrs only on sampling.
sdk/metric/internal/aggregate/aggregate.go Changes the filter plumbing to pass (attribute.Set, attribute.Filter) instead of eagerly filtering.
sdk/metric/internal/aggregate/atomic.go Updates limitedSyncMap.LoadOrStoreAttr to key by filtered Distinct without constructing filtered sets on hot path.
sdk/metric/internal/aggregate/atomic_test.go Adds a regression test for overflow behavior in limitedSyncMap.
sdk/metric/internal/aggregate/sum.go Updates sum aggregation to use lazy filtered-attr handling and new reservoir Offer signature.
sdk/metric/internal/aggregate/lastvalue.go Updates lastvalue aggregation to use lazy filtered-attr handling and new reservoir Offer signature.
sdk/metric/internal/aggregate/histogram.go Updates explicit bucket histogram aggregation to use lazy filtered-attr handling and new reservoir Offer signature.
sdk/metric/internal/aggregate/exponential_histogram.go Uses NewDistinctWithFilter for fast map lookup before allocating filtered sets; updates exemplar offering signature.
sdk/metric/internal/aggregate/drop.go Updates drop reservoir to new FilteredExemplarReservoir.Offer signature.
sdk/metric/internal/aggregate/filtered_reservoir.go Changes FilteredExemplarReservoir.Offer to accept (Set, Filter) and adds lazy-reservoir dispatch + dropped attr computation.
sdk/metric/internal/aggregate/filtered_reservoir_test.go Extends tests to validate that reservoirs implementing OfferLazy use the lazy path.
sdk/metric/internal/aggregate/aggregate_test.go Updates filter tests to validate filtered/dropped attrs derived from (Set, Filter) instead of eager filtering.
sdk/metric/benchmark_test.go Adds FilteredWithExemplars benchmark scenario.
CHANGELOG.md Notes the performance improvement in the unreleased changelog.

Comment thread attribute/hash.go Outdated
Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go Outdated
Comment thread CHANGELOG.md
@dashpole dashpole requested a review from Copilot April 19, 2026 20:47
@dashpole dashpole changed the title Lazily construct dropped attributes only when exemplars are Offered Lazily compute filtered and dropped attributes only when required Apr 19, 2026
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

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
Comment thread sdk/metric/internal/aggregate/filtered_reservoir.go
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

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

Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
@MrAlias
Copy link
Copy Markdown
Contributor

MrAlias commented Apr 30, 2026

side note: the branch name is on point 👨‍🍳

Copy link
Copy Markdown
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.

This optimization introduces a correctness problem in the first-seen series path.

The implementation now evaluates attribute.Filter more than once for a single measurement: first to derive the filtered identity, and later to materialize the filtered attributes on the miss path. That is not safe under the current API contract. attribute.Filter can wrap mutable closure state, so those evaluations can disagree for the same measurement.

Once that happens, the aggregation key and the stored or reported attributes can diverge. That breaks the invariant that metric state is keyed by the post-filtered attribute set. Given that, I do not think this lazy strategy is viable unless the filter contract is narrowed or the filtering result is computed once per measurement.

Comment thread sdk/metric/internal/aggregate/atomic.go Outdated
Comment thread sdk/metric/internal/aggregate/exponential_histogram.go Outdated
@dashpole
Copy link
Copy Markdown
Collaborator Author

dashpole commented May 1, 2026

Good points. The alternative I had been considering was to try and avoid the copy within Set itself. E.g.

func (s *Set) WithFilter(f Filter) *Set {
    // return a set that uses a reference to the old set's storage instead of making a copy.
    // But the filter is still only evaluated when the set is created.
}

func (s *Set) Dropped() []KeyValue {
    // returns the set of labels that were dropped if the set was created using WithFilter.  Otherwise, returns the empty set.
}

This will be an issue (although I think easier to solve) for the new WithUnsafeAttributes or Bind functions as well, but will need a different solution.

@dashpole dashpole marked this pull request as draft May 1, 2026 13:58
@dashpole dashpole force-pushed the lasy_dropped branch 3 times, most recently from 21c08e2 to c1596b7 Compare May 6, 2026 19:15
@dashpole dashpole force-pushed the lasy_dropped branch 2 times, most recently from 46fa4c2 to f8af860 Compare May 6, 2026 20:18
@dashpole dashpole marked this pull request as ready for review May 6, 2026 20:18
@dashpole dashpole requested a review from Copilot May 6, 2026 20:19
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

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

Comment thread attribute/lazy_set.go
Comment thread attribute/lazy_set_test.go Outdated
Comment thread sdk/metric/exemplar/fixed_size_reservoir.go Outdated
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

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

Comment thread attribute/lazy_set.go
Comment thread attribute/lazy_set_test.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@dashpole
Copy link
Copy Markdown
Collaborator Author

dashpole commented May 6, 2026

I think this is ready now! I looked at a few ways to try and lazily compute attribute sets, but the cleanest way was to make it a separate thing in the attribute package. It can be done without doing that, but it is very complex (you pass around the mask, a bool, the filter, the original set, etc). I would've kept it in the sdk/metric package, but the sdk/metric/exemplar package needs to accept it.

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.

3 participants