You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
876f7c51e4
Towards https://github.com/open-telemetry/opentelemetry-go/issues/7810 Fixes https://github.com/open-telemetry/opentelemetry-go/issues/8143 String representation follows: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols This uses optimizations like https://github.com/open-telemetry/opentelemetry-go/pull/8039 and we inline the JSON-array/string encoding logic so we avoid the extra allocations and reflection overhead of marshaling through encoding/json (the code is inlined here not to reimplement JSON broadly, but to provide a spec-specific, allocation-conscious formatter for a constrained data model). Benchmarks of both `String` and `Emit` (that is going to be deprecated) showcase that `String` is even more efficient. ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/attribute cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkBool/String-20 100000000 10.20 ns/op 0 B/op 0 allocs/op BenchmarkBool/Emit-20 100000000 10.33 ns/op 0 B/op 0 allocs/op BenchmarkBoolSlice/Len2/String-20 28427863 36.15 ns/op 16 B/op 1 allocs/op BenchmarkBoolSlice/Len2/Emit-20 5433291 201.8 ns/op 40 B/op 5 allocs/op BenchmarkBoolSlice/Len8/String-20 12453201 99.46 ns/op 48 B/op 1 allocs/op BenchmarkBoolSlice/Len8/Emit-20 2185160 546.0 ns/op 88 B/op 11 allocs/op BenchmarkInt/String-20 100000000 10.73 ns/op 0 B/op 0 allocs/op BenchmarkInt/Emit-20 100000000 11.03 ns/op 0 B/op 0 allocs/op BenchmarkIntSlice/Len2/String-20 17855926 61.57 ns/op 48 B/op 1 allocs/op BenchmarkIntSlice/Len2/Emit-20 6237072 184.9 ns/op 56 B/op 4 allocs/op BenchmarkIntSlice/Len8/String-20 6573506 192.1 ns/op 176 B/op 1 allocs/op BenchmarkIntSlice/Len8/Emit-20 3620901 332.8 ns/op 136 B/op 4 allocs/op BenchmarkInt64/String-20 100000000 10.90 ns/op 0 B/op 0 allocs/op BenchmarkInt64/Emit-20 100000000 10.91 ns/op 0 B/op 0 allocs/op BenchmarkInt64Slice/Len2/String-20 20924970 59.59 ns/op 48 B/op 1 allocs/op BenchmarkInt64Slice/Len2/Emit-20 6755516 184.2 ns/op 56 B/op 4 allocs/op BenchmarkInt64Slice/Len8/String-20 6033630 207.9 ns/op 176 B/op 1 allocs/op BenchmarkInt64Slice/Len8/Emit-20 3491808 327.2 ns/op 136 B/op 4 allocs/op BenchmarkFloat64/String-20 23607802 52.21 ns/op 2 B/op 1 allocs/op BenchmarkFloat64/Emit-20 13578472 93.34 ns/op 16 B/op 2 allocs/op BenchmarkFloat64Slice/Len2/String-20 12066591 111.0 ns/op 64 B/op 1 allocs/op BenchmarkFloat64Slice/Len2/Emit-20 5177293 234.3 ns/op 56 B/op 4 allocs/op BenchmarkFloat64Slice/Len8/String-20 3041408 381.9 ns/op 208 B/op 1 allocs/op BenchmarkFloat64Slice/Len8/Emit-20 2369974 548.3 ns/op 136 B/op 4 allocs/op BenchmarkString/String-20 137506468 8.578 ns/op 0 B/op 0 allocs/op BenchmarkString/Emit-20 139229646 8.542 ns/op 0 B/op 0 allocs/op BenchmarkStringSlice/Len2/Emit-20 5809321 228.9 ns/op 120 B/op 4 allocs/op BenchmarkStringSlice/Len8/String-20 5089977 240.0 ns/op 96 B/op 1 allocs/op BenchmarkStringSlice/Len8/Emit-20 2569848 480.0 ns/op 344 B/op 4 allocs/op BenchmarkByteSlice/String-20 32244670 34.31 ns/op 16 B/op 1 allocs/op BenchmarkByteSlice/Emit-20 36643321 34.63 ns/op 16 B/op 1 allocs/op ```
481 lines
11 KiB
Go
481 lines
11 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package attribute_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
)
|
|
|
|
// Store results in a file scope var to ensure compiler does not optimize the
|
|
// test away.
|
|
var (
|
|
outV attribute.Value
|
|
outKV attribute.KeyValue
|
|
|
|
outBool bool
|
|
outBoolSlice []bool
|
|
outInt64 int64
|
|
outInt64Slice []int64
|
|
outFloat64 float64
|
|
outFloat64Slice []float64
|
|
outStr string
|
|
outStrSlice []string
|
|
)
|
|
|
|
func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStr = kv.Value.Emit()
|
|
}
|
|
}
|
|
}
|
|
|
|
func benchmarkString(kv attribute.KeyValue) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStr = kv.Value.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkBool(b *testing.B) {
|
|
k, v := "bool", true
|
|
kv := attribute.Bool(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.BoolValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Bool(k, v)
|
|
}
|
|
})
|
|
b.Run("AsBool", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outBool = kv.Value.AsBool()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkBoolSlice(b *testing.B) {
|
|
for _, bench := range []struct {
|
|
name string
|
|
v []bool
|
|
}{
|
|
{name: "Len2", v: []bool{true, false}},
|
|
{name: "Len8", v: []bool{true, false, true, false, true, false, true, false}},
|
|
} {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
k, v := "bool slice", bench.v
|
|
kv := attribute.BoolSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.BoolSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.BoolSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsBoolSlice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outBoolSlice = kv.Value.AsBoolSlice()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkInt(b *testing.B) {
|
|
k, v := "int", int(42)
|
|
kv := attribute.Int(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.IntValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int(k, v)
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkIntSlice(b *testing.B) {
|
|
for _, bench := range []struct {
|
|
name string
|
|
v []int
|
|
}{
|
|
{name: "Len2", v: []int{42, -3}},
|
|
{name: "Len8", v: []int{42, -3, 12, 7, 9, 11, -5, 0}},
|
|
} {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
k, v := "int slice", bench.v
|
|
kv := attribute.IntSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.IntSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.IntSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkInt64(b *testing.B) {
|
|
k, v := "int64", int64(42)
|
|
kv := attribute.Int64(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Int64Value(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int64(k, v)
|
|
}
|
|
})
|
|
b.Run("AsInt64", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outInt64 = kv.Value.AsInt64()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkInt64Slice(b *testing.B) {
|
|
for _, bench := range []struct {
|
|
name string
|
|
v []int64
|
|
}{
|
|
{name: "Len2", v: []int64{42, -3}},
|
|
{name: "Len8", v: []int64{42, -3, 12, 7, 9, 11, -5, 0}},
|
|
} {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
k, v := "int64 slice", bench.v
|
|
kv := attribute.Int64Slice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Int64SliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int64Slice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsInt64Slice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outInt64Slice = kv.Value.AsInt64Slice()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFloat64(b *testing.B) {
|
|
k, v := "float64", float64(42)
|
|
kv := attribute.Float64(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Float64Value(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Float64(k, v)
|
|
}
|
|
})
|
|
b.Run("AsFloat64", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outFloat64 = kv.Value.AsFloat64()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkFloat64Slice(b *testing.B) {
|
|
for _, bench := range []struct {
|
|
name string
|
|
v []float64
|
|
}{
|
|
{name: "Len2", v: []float64{42, -3}},
|
|
{name: "Len8", v: []float64{42, -3, 12, 7, 9, 11, -5, 0}},
|
|
} {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
k, v := "float64 slice", bench.v
|
|
kv := attribute.Float64Slice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Float64SliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Float64Slice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsFloat64Slice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outFloat64Slice = kv.Value.AsFloat64Slice()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkString(b *testing.B) {
|
|
k, v := "string", "42"
|
|
kv := attribute.String(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.StringValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.String(k, v)
|
|
}
|
|
})
|
|
b.Run("AsString", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStr = kv.Value.AsString()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkStringSlice(b *testing.B) {
|
|
for _, bench := range []struct {
|
|
name string
|
|
v []string
|
|
}{
|
|
{name: "Len2", v: []string{"forty-two", "negative three"}},
|
|
{name: "Len8", v: []string{"forty-two", "negative three", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen"}},
|
|
} {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
k, v := "string slice", bench.v
|
|
kv := attribute.StringSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.StringSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.StringSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsStringSlice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStrSlice = kv.Value.AsStringSlice()
|
|
}
|
|
})
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkByteSlice(b *testing.B) {
|
|
k, v := "bytes", []byte("forty-two")
|
|
kv := attribute.ByteSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
attribute.ByteSliceValue(v)
|
|
}
|
|
})
|
|
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
attribute.ByteSlice(k, v)
|
|
}
|
|
})
|
|
|
|
b.Run("AsByteSlice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
kv.Value.AsByteSlice()
|
|
}
|
|
})
|
|
|
|
b.Run("String", benchmarkString(kv))
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkSetEquals(b *testing.B) {
|
|
b.Run("Empty", func(b *testing.B) {
|
|
benchmarkSetEquals(b, attribute.EmptySet())
|
|
})
|
|
b.Run("1 string attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.String("string", "42"))
|
|
benchmarkSetEquals(b, &set)
|
|
})
|
|
b.Run("10 string attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.String("a", "42"),
|
|
attribute.String("b", "42"),
|
|
attribute.String("c", "42"),
|
|
attribute.String("d", "42"),
|
|
attribute.String("e", "42"),
|
|
attribute.String("f", "42"),
|
|
attribute.String("g", "42"),
|
|
attribute.String("h", "42"),
|
|
attribute.String("i", "42"),
|
|
attribute.String("j", "42"),
|
|
)
|
|
benchmarkSetEquals(b, &set)
|
|
})
|
|
b.Run("1 int attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.Int("string", 42))
|
|
benchmarkSetEquals(b, &set)
|
|
})
|
|
b.Run("10 int attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.Int("a", 42),
|
|
attribute.Int("b", 42),
|
|
attribute.Int("c", 42),
|
|
attribute.Int("d", 42),
|
|
attribute.Int("e", 42),
|
|
attribute.Int("f", 42),
|
|
attribute.Int("g", 42),
|
|
attribute.Int("h", 42),
|
|
attribute.Int("i", 42),
|
|
attribute.Int("j", 42),
|
|
)
|
|
benchmarkSetEquals(b, &set)
|
|
})
|
|
}
|
|
|
|
func benchmarkSetEquals(b *testing.B, set *attribute.Set) {
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
if !set.Equals(set) {
|
|
b.Fatal("not equal")
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkEquivalentMapAccess measures how expensive it is to use
|
|
// Equivalent() as a map key. This is on the hot path for making synchronous
|
|
// measurements on the metrics API/SDK. It will likely be on the hot path for
|
|
// the trace and logs API/SDK in the future.
|
|
func BenchmarkEquivalentMapAccess(b *testing.B) {
|
|
b.Run("Empty", func(b *testing.B) {
|
|
benchmarkEquivalentMapAccess(b, attribute.EmptySet())
|
|
})
|
|
b.Run("1 string attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.String("string", "42"))
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("10 string attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.String("a", "42"),
|
|
attribute.String("b", "42"),
|
|
attribute.String("c", "42"),
|
|
attribute.String("d", "42"),
|
|
attribute.String("e", "42"),
|
|
attribute.String("f", "42"),
|
|
attribute.String("g", "42"),
|
|
attribute.String("h", "42"),
|
|
attribute.String("i", "42"),
|
|
attribute.String("j", "42"),
|
|
)
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("1 int attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.Int("string", 42))
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("10 int attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.Int("a", 42),
|
|
attribute.Int("b", 42),
|
|
attribute.Int("c", 42),
|
|
attribute.Int("d", 42),
|
|
attribute.Int("e", 42),
|
|
attribute.Int("f", 42),
|
|
attribute.Int("g", 42),
|
|
attribute.Int("h", 42),
|
|
attribute.Int("i", 42),
|
|
attribute.Int("j", 42),
|
|
)
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
}
|
|
|
|
func benchmarkEquivalentMapAccess(b *testing.B, set *attribute.Set) {
|
|
values := map[attribute.Distinct]int{}
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
values[set.Equivalent()]++
|
|
}
|
|
}
|