Skip to content
Closed
34 changes: 34 additions & 0 deletions attribute/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,40 @@ func (l *Set) Encoded(encoder Encoder) string {
return encoder.Encode(l.Iter())
}

// NewDistinct returns a Distinct from key values that can be used as a map
// key.
func NewDistinct(kvs ...KeyValue) Distinct {
// Check for empty set.
if len(kvs) == 0 {
return Distinct{hash: emptyHash}
}

// Stable sort so the following de-duplication can implement
// last-value-wins semantics.
slices.SortStableFunc(kvs, func(a, b KeyValue) int {
return cmp.Compare(a.Key, b.Key)
})

position := len(kvs) - 1
offset := position - 1

// The requirements stated above require that the stable
// result be placed in the end of the input slice, while
// overwritten values are swapped to the beginning.
//
// De-duplicate with last-value-wins semantics. Preserve
// duplicate values at the beginning of the input slice.
for ; offset >= 0; offset-- {
if kvs[offset].Key == kvs[position].Key {
continue
}
position--
kvs[offset], kvs[position] = kvs[position], kvs[offset]
}
kvs = kvs[position:]
return Distinct{hash: hashKVs(kvs)}
}

// NewSet returns a new Set. See the documentation for
// NewSetWithSortableFiltered for more details.
//
Expand Down
213 changes: 213 additions & 0 deletions internal/benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package benchmark // import "go.opentelemetry.io/otel/internal/benchmark"

import (
"fmt"
"sync"
"testing"

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/exemplar"
)

const scopeName = "go.opentelemetry.op/otel/internal/benchmark"

func testCounter(b *testing.B, mp metric.MeterProvider) metric.Float64Counter {
meter := mp.Meter(scopeName)
counter, err := meter.Float64Counter("test.counter")
assert.NoError(b, err)
return counter
}

var addOptPool = &sync.Pool{
New: func() any {
const n = 1 // WithAttributeSet
o := make([]metric.AddOption, 0, n)
// Return a pointer to avoid extra allocation on Put().
return &o
},
}

func BenchmarkCounterAdd(b *testing.B) {
ctx := b.Context()
for _, mp := range []struct {
name string
provider func() metric.MeterProvider
}{
{
name: "NoFilter",
provider: func() metric.MeterProvider {
return sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewManualReader()), sdkmetric.WithExemplarFilter(exemplar.AlwaysOffFilter))
},
},
{
name: "Filtered",
provider: func() metric.MeterProvider {
view := sdkmetric.NewView(
sdkmetric.Instrument{
Name: "test.counter",
},
// Filter out one attribute from each call.
sdkmetric.Stream{AttributeFilter: attribute.NewDenyKeysFilter("a")},
)
return sdkmetric.NewMeterProvider(
sdkmetric.WithView(view),
sdkmetric.WithReader(sdkmetric.NewManualReader()),
sdkmetric.WithExemplarFilter(exemplar.AlwaysOffFilter))
},
},
} {
b.Run(mp.name, func(b *testing.B) {
for _, attrsLen := range []int{1, 5, 10} {
attrPool := sync.Pool{
New: func() any {
// Pre-allocate common capacity
s := make([]attribute.KeyValue, 0, attrsLen)
// Return a pointer to avoid extra allocation on Put().
return &s
},
}
b.Run(fmt.Sprintf("Attributes/%d", attrsLen), func(b *testing.B) {
// This case shows the performance of our API + SDK when
// following our contributor guidance for recording
// cached attributes by passing attribute.Set:
// https: //github.com/open-telemetry/opentelemetry-go/blob/main/CONTRIBUTING.md#cache-common-attribute-sets-for-repeated-measurements
b.Run("Precomputed/WithAttributeSet", func(b *testing.B) {
counter := testCounter(b, mp.provider())
precomputedOpts := []metric.AddOption{
metric.WithAttributeSet(attribute.NewSet(getAttributes(attrsLen)...)),
}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.Add(ctx, 1, precomputedOpts...)
}
})
})
// This case shows the performance of our API + SDK when
// following our contributor guidance for recording
// cached attributes by passing []attribute.KeyValue:
// https: //github.com/open-telemetry/opentelemetry-go/blob/main/CONTRIBUTING.md#cache-common-attribute-sets-for-repeated-measurements
b.Run("Precomputed/WithAttributes", func(b *testing.B) {
counter := testCounter(b, mp.provider()).WithAttributes(getAttributes(attrsLen)...)
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.Add(ctx, 1)
}
})
})
// This case shows the performance of our API + SDK when
// following our contributor guidance for recording
// varying attributes by passing attribute.Set:
// https://github.com/open-telemetry/opentelemetry-go/blob/main/CONTRIBUTING.md#attribute-and-option-allocation-management
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...)
}()
}
})
})
Comment on lines +110 to +134
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.

// This case shows the performance of our API + SDK when
// following our contributor guidance for recording
// varying attributes by passing []attribute.KeyValue:
// https://github.com/open-telemetry/opentelemetry-go/blob/main/CONTRIBUTING.md#attribute-and-option-allocation-management
b.Run("Dynamic/WithAttributes", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
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)
counter.WithAttributes(*attrsSlice...).Add(ctx, 1)
}()
i++
}
})
})
// This case shows the performance of our API + SDK when
// users use it in the "obvious" way, without explicitly
// trying to optimize for performance.
b.Run("Naive/WithAttributes", func(b *testing.B) {
counter := testCounter(b, mp.provider())
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.WithAttributes(getAttributes(attrsLen)...).Add(ctx, 1)
}
})
})
})
}
})
}
}

func getAttributes(number int) []attribute.KeyValue {
kvs := make([]attribute.KeyValue, 0, number)
appendAttributes(number, &kvs)
return kvs
}

func appendAttributes(number int, kvs *[]attribute.KeyValue) {
switch number {
case 1:
*kvs = append(*kvs,
attribute.String("a", "a"),
)
case 5:
*kvs = append(*kvs,
attribute.String("a", "a"),
attribute.String("b", "b"),
attribute.String("c", "c"),
attribute.String("d", "d"),
attribute.String("e", "e"),
)
case 10:
*kvs = append(*kvs,
attribute.String("a", "a"),
attribute.String("b", "b"),
attribute.String("c", "c"),
attribute.String("d", "d"),
attribute.String("e", "e"),
attribute.String("f", "f"),
attribute.String("g", "g"),
attribute.String("h", "h"),
attribute.String("i", "i"),
attribute.String("j", "j"),
)
default:
panic("unknown number of attributes")
}
}
34 changes: 34 additions & 0 deletions internal/benchmark/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module go.opentelemetry.io/otel/internal/benchmark

go 1.24.0

require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/metric v1.39.0
go.opentelemetry.io/otel/sdk/metric v1.39.0
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/sys v0.40.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace go.opentelemetry.io/otel => ../..

replace go.opentelemetry.io/otel/metric => ../../metric

replace go.opentelemetry.io/otel/trace => ../../trace

replace go.opentelemetry.io/otel/sdk => ../../sdk

replace go.opentelemetry.io/otel/sdk/metric => ../../sdk/metric
32 changes: 32 additions & 0 deletions internal/benchmark/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 5 additions & 0 deletions internal/global/alternate_meter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/embedded"
"go.opentelemetry.io/otel/metric/noop"
Expand Down Expand Up @@ -224,6 +225,10 @@ func (ao *altObserver) ObserveInt64(inst metric.Int64Observable, _ int64, _ ...m
ao.observe(inst)
}

func (ao *altObserver) WithAttributes(...attribute.KeyValue) metric.Observer {
return ao
}

func (ao *altObserver) observe(inst any) {
switch inst.(type) {
case *testAiCounter, *testAfCounter,
Expand Down
Loading
Loading