diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f533a68e1..ae3747cc0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed +- Optimize memory allocation when creation a new `Set` using `NewSet` or `NewSetWithFiltered` in `go.opentelemetry.io/otel/attribute`. (#3832) +- Optimize memory allocation when creation new metric instruments in `go.opentelemetry.io/otel/sdk/metric`. (#3832) - Avoid creating new objects on all calls to `WithDeferredSetup` and `SkipContextSetup` in OpenTracing bridge. (#3833) - The `New` and `Detect` functions from `go.opentelemetry.io/otel/sdk/resource` return errors that wrap underlying errors instead of just containing the underlying error strings. (#3844) diff --git a/attribute/set.go b/attribute/set.go index 26be5983223..f9b6dc3f505 100644 --- a/attribute/set.go +++ b/attribute/set.go @@ -18,6 +18,7 @@ import ( "encoding/json" "reflect" "sort" + "sync" ) type ( @@ -62,6 +63,12 @@ var ( iface: [0]KeyValue{}, }, } + + // sortables is a pool of Sortables used to create Sets with a user does + // not provide one. + sortables = sync.Pool{ + New: func() interface{} { return new(Sortable) }, + } ) // EmptySet returns a reference to a Set with no elements. @@ -191,7 +198,9 @@ func NewSet(kvs ...KeyValue) Set { if len(kvs) == 0 { return empty() } - s, _ := NewSetWithSortableFiltered(kvs, new(Sortable), nil) + srt := sortables.Get().(*Sortable) + s, _ := NewSetWithSortableFiltered(kvs, srt, nil) + sortables.Put(srt) return s } @@ -218,7 +227,10 @@ func NewSetWithFiltered(kvs []KeyValue, filter Filter) (Set, []KeyValue) { if len(kvs) == 0 { return empty(), nil } - return NewSetWithSortableFiltered(kvs, new(Sortable), filter) + srt := sortables.Get().(*Sortable) + s, filtered := NewSetWithSortableFiltered(kvs, srt, filter) + sortables.Put(srt) + return s, filtered } // NewSetWithSortableFiltered returns a new Set. diff --git a/sdk/metric/instrument.go b/sdk/metric/instrument.go index 33183d22ea9..92e2e3780ed 100644 --- a/sdk/metric/instrument.go +++ b/sdk/metric/instrument.go @@ -194,8 +194,12 @@ func (i *instrumentImpl[N]) aggregate(ctx context.Context, val N, attrs []attrib if err := ctx.Err(); err != nil { return } + // Do not use single attribute.Sortable and attribute.NewSetWithSortable, + // this method needs to be concurrent safe. Let the sync.Pool in the + // attribute package handle allocations of the Sortable. + s := attribute.NewSet(attrs...) for _, agg := range i.aggregators { - agg.Aggregate(val, attribute.NewSet(attrs...)) + agg.Aggregate(val, s) } } @@ -260,8 +264,12 @@ func newObservable[N int64 | float64](scope instrumentation.Scope, kind Instrume // observe records the val for the set of attrs. func (o *observable[N]) observe(val N, attrs []attribute.KeyValue) { + // Do not use single attribute.Sortable and attribute.NewSetWithSortable, + // this method needs to be concurrent safe. Let the sync.Pool in the + // attribute package handle allocations of the Sortable. + s := attribute.NewSet(attrs...) for _, agg := range o.aggregators { - agg.Aggregate(val, attribute.NewSet(attrs...)) + agg.Aggregate(val, s) } } diff --git a/sdk/metric/instrument_test.go b/sdk/metric/instrument_test.go new file mode 100644 index 00000000000..4315effbfd0 --- /dev/null +++ b/sdk/metric/instrument_test.go @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metric + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/metric/internal" +) + +func BenchmarkInstrument(b *testing.B) { + attr := func(id int) []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("user", "Alice"), + attribute.Bool("admin", true), + attribute.Int("id", id), + } + } + + b.Run("instrumentImpl/aggregate", func(b *testing.B) { + inst := instrumentImpl[int64]{aggregators: []internal.Aggregator[int64]{ + internal.NewLastValue[int64](), + internal.NewCumulativeSum[int64](true), + internal.NewDeltaSum[int64](true), + }} + ctx := context.Background() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + inst.aggregate(ctx, int64(i), attr(i)) + } + }) + + b.Run("observable/observe", func(b *testing.B) { + o := observable[int64]{aggregators: []internal.Aggregator[int64]{ + internal.NewLastValue[int64](), + internal.NewCumulativeSum[int64](true), + internal.NewDeltaSum[int64](true), + }} + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + o.observe(int64(i), attr(i)) + } + }) +}