@@ -18,37 +18,42 @@ import (
1818type buckets [N int64 | float64 ] struct {
1919 count uint64
2020 counts []uint64
21+ noSum bool
22+ noMinMax bool
2123 min , max atomic.Value
22- total * counter [N ]
24+ total atomicSum [N ]
2325
2426 attrs attribute.Set
2527 res FilteredExemplarReservoir [N ]
2628}
2729
28- // newBuckets returns buckets with n bins.
29- func newBuckets [ N int64 | float64 ]( attrs attribute. Set , n int ) * buckets [ N ] {
30- return & buckets [ N ]{ attrs : attrs , counts : make ([] uint64 , n ), total : & counter [ N ]{}}
31- }
32-
33- func ( b * buckets [ N ]) bin ( idx int ) {
30+ func ( b * buckets [ N ]) measure (
31+ ctx context. Context ,
32+ value N ,
33+ idx int ,
34+ droppedAttr []attribute. KeyValue ,
35+ ) {
3436 atomic .AddUint64 (& b .counts [idx ], 1 )
3537 atomic .AddUint64 (& b .count , 1 )
36- }
37-
38- func ( b * buckets [ N ]) minMax ( value N ) {
39- for {
40- minLoaded := b . min . Load ()
41- if value < minLoaded .( N ) && ! b . min . CompareAndSwap ( minLoaded , value ) {
42- // We got a new min value, but lost the race. Try again.
43- continue
44- }
45- maxLoaded := b . max . Load ()
46- if value > maxLoaded .( N ) && ! b . max . CompareAndSwap ( maxLoaded , value ) {
47- // We got a new max value, but lost the race. Try again.
48- continue
38+ if ! b . noMinMax {
39+ for {
40+ minLoaded := b . min . Load ()
41+ if ( minLoaded == nil || value < minLoaded .( N )) && ! b . min . CompareAndSwap ( minLoaded , value ) {
42+ // We got a new min value, but lost the race. Try again.
43+ continue
44+ }
45+ maxLoaded := b . max . Load ()
46+ if ( maxLoaded == nil || value > maxLoaded .( N )) && ! b . max . CompareAndSwap ( maxLoaded , value ) {
47+ // We got a new max value, but lost the race. Try again.
48+ continue
49+ }
50+ break
4951 }
50- return
5152 }
53+ if ! b .noSum {
54+ b .total .add (value )
55+ }
56+ b .res .Offer (ctx , value , droppedAttr )
5257}
5358
5459// histValues summarizes a set of measurements as an histValues with
@@ -58,10 +63,10 @@ type histValues[N int64 | float64] struct {
5863 noMinMax bool
5964 bounds []float64
6065
61- newRes func (attribute.Set ) FilteredExemplarReservoir [N ]
62- limit limiter [buckets [N ]]
63- values map [attribute.Distinct ]* buckets [N ]
64- valuesMu sync.RWMutex
66+ newRes func (attribute.Set ) FilteredExemplarReservoir [N ]
67+ limit limiter [buckets [N ]]
68+ values map [attribute.Distinct ]* buckets [N ]
69+ sync.RWMutex
6570}
6671
6772func newHistValues [N int64 | float64 ](
@@ -102,39 +107,42 @@ func (s *histValues[N]) measure(
102107 // (s.bounds[len(s.bounds)-1], +∞).
103108 idx := sort .SearchFloat64s (s .bounds , float64 (value ))
104109
105- s .valuesMu .RLock ()
106-
110+ // Hold the RLock even after we are done reading from the values map to
111+ // ensure we don't race with collection.
112+ s .RLock ()
107113 attr := s .limit .Attributes (fltrAttr , s .values )
108114 b , ok := s .values [attr .Equivalent ()]
109- s .valuesMu .RUnlock ()
110- if ! ok {
115+ if ok {
116+ b .measure (ctx , value , idx , droppedAttr )
117+ s .RUnlock ()
118+ return
119+ }
120+ s .RUnlock ()
121+ // Switch to a full lock to add a new element to the map.
122+ s .Lock ()
123+ defer s .Unlock ()
124+ // Check that the element wasn't added since we last checked.
125+ b , ok = s .values [attr .Equivalent ()]
126+ if ok {
127+ b .measure (ctx , value , idx , droppedAttr )
128+ return
129+ }
130+ b = & buckets [N ]{
131+ attrs : attr ,
111132 // N+1 buckets. For example:
112133 //
113134 // bounds = [0, 5, 10]
114135 //
115136 // Then,
116137 //
117138 // buckets = (-∞, 0], (0, 5.0], (5.0, 10.0], (10.0, +∞)
118- b = newBuckets [N ](attr , len (s .bounds )+ 1 )
119- b .res = s .newRes (attr )
120-
121- // Ensure min and max are recorded values (not zero), for new buckets.
122- if ! s .noMinMax {
123- b .min .Store (value )
124- b .max .Store (value )
125- }
126- s .valuesMu .Lock ()
127- s .values [attr .Equivalent ()] = b
128- s .valuesMu .Unlock ()
129- }
130- b .bin (idx )
131- if ! s .noMinMax {
132- b .minMax (value )
139+ counts : make ([]uint64 , len (s .bounds )+ 1 ),
140+ res : s .newRes (attr ),
141+ noSum : s .noSum ,
142+ noMinMax : s .noMinMax ,
133143 }
134- if ! s .noSum {
135- b .total .add (value )
136- }
137- b .res .Offer (ctx , value , droppedAttr )
144+ b .measure (ctx , value , idx , droppedAttr )
145+ s .values [attr .Equivalent ()] = b
138146}
139147
140148// newHistogram returns an Aggregator that summarizes a set of measurements as
@@ -169,8 +177,11 @@ func (s *histogram[N]) delta(
169177 h , _ := (* dest ).(metricdata.Histogram [N ])
170178 h .Temporality = metricdata .DeltaTemporality
171179
172- s .valuesMu .Lock ()
173- defer s .valuesMu .Unlock ()
180+ // Aquire a full lock to ensure there are no concurrent measure() calls.
181+ // If we only used a RLock, we could observe "partial" measurements, such
182+ // as a histogram count increment without a histogram total increment.
183+ s .Lock ()
184+ defer s .Unlock ()
174185
175186 // Do not allow modification of our copy of bounds.
176187 bounds := slices .Clone (s .bounds )
@@ -221,8 +232,11 @@ func (s *histogram[N]) cumulative(
221232 h , _ := (* dest ).(metricdata.Histogram [N ])
222233 h .Temporality = metricdata .CumulativeTemporality
223234
224- s .valuesMu .Lock ()
225- defer s .valuesMu .Unlock ()
235+ // Aquire a full lock to ensure there are no concurrent measure() calls.
236+ // If we only used a RLock, we could observe "partial" measurements, such
237+ // as a histogram count increment without a histogram total increment.
238+ s .Lock ()
239+ defer s .Unlock ()
226240
227241 // Do not allow modification of our copy of bounds.
228242 bounds := slices .Clone (s .bounds )
0 commit comments