Skip to content

Commit 207587b

Browse files
jmacdBogdan DrutuXSAMAneurysm9
authored
Metric histogram aggregator: Swap in SynchronizedMove to avoid allocations (open-telemetry#1435)
* Move emptyState() allocations outside lock * Add more testing * Re-comment; add CHANGELOG * Add CHANGELOG PR number * Update CHANGELOG.md Co-authored-by: Sam Xie <[email protected]> Co-authored-by: Bogdan Drutu <[email protected]> Co-authored-by: Sam Xie <[email protected]> Co-authored-by: Anthony Mirabella <[email protected]>
1 parent c29c6fd commit 207587b

File tree

3 files changed

+76
-69
lines changed

3 files changed

+76
-69
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3232
- `NewExporter` from `exporters/otlp` now takes a `ProtocolDriver` as a parameter. (#1369)
3333
- Many OTLP Exporter options became gRPC ProtocolDriver options. (#1369)
3434
- Unify endpoint API that related to OTel exporter. (#1401)
35+
- Optimize metric histogram aggregator to re-use its slice of buckets. (#1435)
3536
- Metric aggregator Count() and histogram Bucket.Counts are consistently `uint64`. (1430)
3637
- `SamplingResult` now passed a `Tracestate` from the parent `SpanContext` (#1432)
3738
- Moved gRPC driver for OTLP exporter to `exporters/otlp/otlpgrpc`. (#1420)

sdk/metric/aggregator/histogram/histogram.go

+29-7
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type (
3838
lock sync.Mutex
3939
boundaries []float64
4040
kind number.Kind
41-
state state
41+
state *state
4242
}
4343

4444
// state represents the state of a histogram, consisting of
@@ -78,8 +78,8 @@ func New(cnt int, desc *metric.Descriptor, boundaries []float64) []Aggregator {
7878
aggs[i] = Aggregator{
7979
kind: desc.NumberKind(),
8080
boundaries: sortedBoundaries,
81-
state: emptyState(sortedBoundaries),
8281
}
82+
aggs[i].state = aggs[i].newState()
8383
}
8484
return aggs
8585
}
@@ -123,20 +123,42 @@ func (c *Aggregator) SynchronizedMove(oa export.Aggregator, desc *metric.Descrip
123123
return aggregator.NewInconsistentAggregatorError(c, oa)
124124
}
125125

126+
if o != nil {
127+
// Swap case: This is the ordinary case for a
128+
// synchronous instrument, where the SDK allocates two
129+
// Aggregators and lock contention is anticipated.
130+
// Reset the target state before swapping it under the
131+
// lock below.
132+
o.clearState()
133+
}
134+
126135
c.lock.Lock()
127136
if o != nil {
128-
o.state = c.state
137+
c.state, o.state = o.state, c.state
138+
} else {
139+
// No swap case: This is the ordinary case for an
140+
// asynchronous instrument, where the SDK allocates a
141+
// single Aggregator and there is no anticipated lock
142+
// contention.
143+
c.clearState()
129144
}
130-
c.state = emptyState(c.boundaries)
131145
c.lock.Unlock()
132146

133147
return nil
134148
}
135149

136-
func emptyState(boundaries []float64) state {
137-
return state{
138-
bucketCounts: make([]uint64, len(boundaries)+1),
150+
func (c *Aggregator) newState() *state {
151+
return &state{
152+
bucketCounts: make([]uint64, len(c.boundaries)+1),
153+
}
154+
}
155+
156+
func (c *Aggregator) clearState() {
157+
for i := range c.state.bucketCounts {
158+
c.state.bucketCounts[i] = 0
139159
}
160+
c.state.sum = 0
161+
c.state.count = 0
140162
}
141163

142164
// Update adds the recorded measurement to the current data set.

sdk/metric/aggregator/histogram/histogram_test.go

+46-62
Original file line numberDiff line numberDiff line change
@@ -115,42 +115,23 @@ func testHistogram(t *testing.T, profile aggregatortest.Profile, policy policy)
115115

116116
agg, ckpt := new2(descriptor)
117117

118-
all := aggregatortest.NewNumbers(profile.NumberKind)
119-
120-
for i := 0; i < count; i++ {
121-
x := profile.Random(policy.sign())
122-
all.Append(x)
123-
aggregatortest.CheckedUpdate(t, agg, x, descriptor)
124-
}
125-
126-
require.NoError(t, agg.SynchronizedMove(ckpt, descriptor))
127-
128-
checkZero(t, agg, descriptor)
129-
130-
all.Sort()
131-
132-
asum, err := ckpt.Sum()
133-
sum := all.Sum()
134-
require.InEpsilon(t,
135-
sum.CoerceToFloat64(profile.NumberKind),
136-
asum.CoerceToFloat64(profile.NumberKind),
137-
0.000000001,
138-
"Same sum - "+policy.name)
139-
require.NoError(t, err)
118+
// This needs to repeat at least 3 times to uncover a failure to reset
119+
// for the overall sum and count fields, since the third time through
120+
// is the first time a `histogram.state` object is reused.
121+
for repeat := 0; repeat < 3; repeat++ {
122+
all := aggregatortest.NewNumbers(profile.NumberKind)
140123

141-
count, err := ckpt.Count()
142-
require.Equal(t, all.Count(), count, "Same count -"+policy.name)
143-
require.NoError(t, err)
124+
for i := 0; i < count; i++ {
125+
x := profile.Random(policy.sign())
126+
all.Append(x)
127+
aggregatortest.CheckedUpdate(t, agg, x, descriptor)
128+
}
144129

145-
buckets, err := ckpt.Histogram()
146-
require.NoError(t, err)
130+
require.NoError(t, agg.SynchronizedMove(ckpt, descriptor))
147131

148-
require.Equal(t, len(buckets.Counts), len(boundaries)+1, "There should be b + 1 counts, where b is the number of boundaries")
132+
checkZero(t, agg, descriptor)
149133

150-
counts := calcBuckets(all.Points(), profile)
151-
for i, v := range counts {
152-
bCount := uint64(buckets.Counts[i])
153-
require.Equal(t, v, bCount, "Wrong bucket #%d count: %v != %v", i, counts, buckets.Counts)
134+
checkHistogram(t, all, profile, ckpt)
154135
}
155136
}
156137

@@ -191,31 +172,7 @@ func TestHistogramMerge(t *testing.T) {
191172

192173
aggregatortest.CheckedMerge(t, ckpt1, ckpt2, descriptor)
193174

194-
all.Sort()
195-
196-
asum, err := ckpt1.Sum()
197-
sum := all.Sum()
198-
require.InEpsilon(t,
199-
sum.CoerceToFloat64(profile.NumberKind),
200-
asum.CoerceToFloat64(profile.NumberKind),
201-
0.000000001,
202-
"Same sum - absolute")
203-
require.NoError(t, err)
204-
205-
count, err := ckpt1.Count()
206-
require.Equal(t, all.Count(), count, "Same count - absolute")
207-
require.NoError(t, err)
208-
209-
buckets, err := ckpt1.Histogram()
210-
require.NoError(t, err)
211-
212-
require.Equal(t, len(buckets.Counts), len(boundaries)+1, "There should be b + 1 counts, where b is the number of boundaries")
213-
214-
counts := calcBuckets(all.Points(), profile)
215-
for i, v := range counts {
216-
bCount := uint64(buckets.Counts[i])
217-
require.Equal(t, v, bCount, "Wrong bucket #%d count: %v != %v", i, counts, buckets.Counts)
218-
}
175+
checkHistogram(t, all, profile, ckpt1)
219176
})
220177
}
221178

@@ -233,22 +190,49 @@ func TestHistogramNotSet(t *testing.T) {
233190
})
234191
}
235192

236-
func calcBuckets(points []number.Number, profile aggregatortest.Profile) []uint64 {
237-
sortedBoundaries := make([]float64, len(boundaries))
193+
// checkHistogram ensures the correct aggregated state between `all`
194+
// (test aggregator) and `agg` (code under test).
195+
func checkHistogram(t *testing.T, all aggregatortest.Numbers, profile aggregatortest.Profile, agg *histogram.Aggregator) {
196+
197+
all.Sort()
198+
199+
asum, err := agg.Sum()
200+
require.NoError(t, err)
201+
202+
sum := all.Sum()
203+
require.InEpsilon(t,
204+
sum.CoerceToFloat64(profile.NumberKind),
205+
asum.CoerceToFloat64(profile.NumberKind),
206+
0.000000001)
238207

208+
count, err := agg.Count()
209+
require.NoError(t, err)
210+
require.Equal(t, all.Count(), count)
211+
212+
buckets, err := agg.Histogram()
213+
require.NoError(t, err)
214+
215+
require.Equal(t, len(buckets.Counts), len(boundaries)+1,
216+
"There should be b + 1 counts, where b is the number of boundaries")
217+
218+
sortedBoundaries := make([]float64, len(boundaries))
239219
copy(sortedBoundaries, boundaries)
240220
sort.Float64s(sortedBoundaries)
241221

222+
require.EqualValues(t, sortedBoundaries, buckets.Boundaries)
223+
242224
counts := make([]uint64, len(sortedBoundaries)+1)
243225
idx := 0
244-
for _, p := range points {
226+
for _, p := range all.Points() {
245227
for idx < len(sortedBoundaries) && p.CoerceToFloat64(profile.NumberKind) >= sortedBoundaries[idx] {
246228
idx++
247229
}
248230
counts[idx]++
249231
}
250-
251-
return counts
232+
for i, v := range counts {
233+
bCount := uint64(buckets.Counts[i])
234+
require.Equal(t, v, bCount, "Wrong bucket #%d count: %v != %v", i, counts, buckets.Counts)
235+
}
252236
}
253237

254238
func TestSynchronizedMoveReset(t *testing.T) {

0 commit comments

Comments
 (0)