1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-11-27 22:49:15 +02:00
Files
opentelemetry-go/attribute/hash_test.go
David Ashpole 9d52bde6d6 Use Set hash in Distinct (2nd attempt) (#7175)
Re-opening https://github.com/open-telemetry/opentelemetry-go/pull/5028
with new benchmarks. For cases with 10 attributes, this reduces the
overhead of metric measurements by ~80-90% (depending on lock
contention). It introduces a small probability of collision for
attribute sets in the metrics SDK. For an instrument with 1 million
different attribute sets, the probability of a collision is
approximately 2 * 10^-8. For a more "normal" cardinality of 1000 on an
instrument, it is approximately 2 * 10^-17.

```
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/attribute
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                            │   main.txt    │              hash.txt               │
                                            │    sec/op     │    sec/op     vs base               │
EquivalentMapAccess/Empty-24                    32.01n ± 2%   10.12n ±  4%  -68.37% (p=0.002 n=6)
EquivalentMapAccess/1_string_attribute-24      106.25n ± 2%   10.01n ±  5%  -90.58% (p=0.002 n=6)
EquivalentMapAccess/10_string_attributes-24   826.250n ± 1%   9.982n ± 11%  -98.79% (p=0.002 n=6)
EquivalentMapAccess/1_int_attribute-24         106.65n ± 2%   10.13n ±  3%  -90.50% (p=0.002 n=6)
EquivalentMapAccess/10_int_attributes-24       833.25n ± 2%   10.04n ±  5%  -98.80% (p=0.002 n=6)
geomean                                         190.3n        10.06n        -94.72%
```

Parallel benchmarks:

```
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/metric
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                                         │  main24.txt   │              new24.txt              │
                                                         │    sec/op     │    sec/op     vs base               │
SyncMeasure/NoView/Int64Counter/Attributes/0-24             288.4n ± 13%   267.0n ± 16%        ~ (p=0.180 n=6)
SyncMeasure/NoView/Int64Counter/Attributes/1-24             372.7n ± 24%   303.3n ±  6%  -18.61% (p=0.002 n=6)
SyncMeasure/NoView/Int64Counter/Attributes/10-24           1862.5n ± 11%   302.2n ±  6%  -83.77% (p=0.002 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/0-24           288.2n ±  5%   291.8n ± 14%        ~ (p=0.589 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/1-24           374.8n ± 22%   326.2n ± 15%  -12.98% (p=0.002 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/10-24         1984.0n ± 10%   277.9n ± 15%  -85.99% (p=0.002 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/0-24       286.8n ± 13%   279.4n ± 14%        ~ (p=0.818 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/1-24       415.4n ± 14%   309.5n ± 11%  -25.47% (p=0.002 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/10-24     1923.0n ± 19%   294.1n ± 17%  -84.71% (p=0.002 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/0-24     284.9n ±  5%   271.6n ± 11%        ~ (p=0.240 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/1-24     382.9n ± 23%   295.7n ± 13%  -22.78% (p=0.002 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/10-24   1787.0n ± 28%   289.2n ± 12%  -83.81% (p=0.002 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/0-24           283.4n ±  8%   269.9n ±  9%        ~ (p=0.589 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/1-24           300.7n ±  8%   270.1n ± 15%  -10.16% (p=0.026 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/10-24         1046.8n ± 24%   299.2n ± 16%  -71.42% (p=0.002 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/0-24         264.3n ± 12%   295.9n ±  5%  +11.93% (p=0.026 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/1-24         321.0n ±  8%   269.4n ± 11%  -16.09% (p=0.002 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/10-24       1052.2n ± 10%   274.6n ±  5%  -73.90% (p=0.002 n=6)
geomean                                                     540.0n         287.7n        -46.72%
```

Single-threaded benchmarks:

```
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/metric
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
                                                      │   main1.txt   │              new1.txt               │
                                                      │    sec/op     │    sec/op     vs base               │
SyncMeasure/NoView/Int64Counter/Attributes/0            130.95n ±  1%   97.99n ± 21%  -25.17% (p=0.002 n=6)
SyncMeasure/NoView/Int64Counter/Attributes/1             300.8n ±  7%   104.6n ±  3%  -65.21% (p=0.002 n=6)
SyncMeasure/NoView/Int64Counter/Attributes/10           1646.0n ±  2%   105.8n ±  2%  -93.58% (p=0.002 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/0          132.65n ±  1%   99.28n ±  4%  -25.16% (p=0.002 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/1           295.4n ±  3%   107.7n ±  3%  -63.54% (p=0.002 n=6)
SyncMeasure/NoView/Float64Counter/Attributes/10         1620.0n ±  1%   109.6n ±  4%  -93.23% (p=0.002 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/0      132.85n ± 80%   99.34n ±  1%  -25.22% (p=0.002 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/1       300.4n ±  1%   106.0n ±  1%  -64.71% (p=0.002 n=6)
SyncMeasure/NoView/Int64UpDownCounter/Attributes/10     1622.0n ±  1%   105.8n ±  1%  -93.48% (p=0.002 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/0    134.90n ± 51%   99.16n ±  4%  -26.49% (p=0.002 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/1     312.4n ± 34%   107.8n ±  2%  -65.51% (p=0.002 n=6)
SyncMeasure/NoView/Float64UpDownCounter/Attributes/10   1613.0n ± 23%   106.1n ±  1%  -93.43% (p=0.002 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/0          103.50n ± 17%   88.53n ±  1%  -14.46% (p=0.002 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/1          199.50n ± 16%   95.44n ±  2%  -52.16% (p=0.002 n=6)
SyncMeasure/NoView/Int64Histogram/Attributes/10         878.70n ±  2%   95.78n ±  2%  -89.10% (p=0.002 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/0        108.55n ± 54%   88.45n ±  1%  -18.51% (p=0.002 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/1        257.30n ± 14%   95.05n ±  2%  -63.06% (p=0.002 n=6)
SyncMeasure/NoView/Float64Histogram/Attributes/10       882.70n ± 18%   96.28n ±  1%  -89.09% (p=0.002 n=6)
geomean                                                  355.2n         100.3n        -71.77%
```

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Robert Pająk <pellared@hotmail.com>
2025-09-16 16:04:50 -04:00

318 lines
8.8 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package attribute // import "go.opentelemetry.io/otel/attribute"
import (
"cmp"
"fmt"
"math"
"reflect"
"slices"
"strings"
"testing"
"go.opentelemetry.io/otel/attribute/internal/fnv"
)
// keyVals is all the KeyValue generators that are used for testing. This is
// not []KeyValue so different keys can be used with the test Values.
var keyVals = []func(string) KeyValue{
func(k string) KeyValue { return Bool(k, true) },
func(k string) KeyValue { return Bool(k, false) },
func(k string) KeyValue { return BoolSlice(k, []bool{false, true}) },
func(k string) KeyValue { return BoolSlice(k, []bool{true, true, false}) },
func(k string) KeyValue { return Int(k, -1278) },
func(k string) KeyValue { return Int(k, 0) }, // Should be different than false above.
func(k string) KeyValue { return IntSlice(k, []int{3, 23, 21, -8, 0}) },
func(k string) KeyValue { return IntSlice(k, []int{1}) },
func(k string) KeyValue { return Int64(k, 1) }, // Should be different from true and []int{1}.
func(k string) KeyValue { return Int64(k, 29369) },
func(k string) KeyValue { return Int64Slice(k, []int64{3826, -38, -29, -1}) },
func(k string) KeyValue { return Int64Slice(k, []int64{8, -328, 29, 0}) },
func(k string) KeyValue { return Float64(k, -0.3812381) },
func(k string) KeyValue { return Float64(k, 1e32) },
func(k string) KeyValue { return Float64Slice(k, []float64{0.1, -3.8, -29., 0.3321}) },
func(k string) KeyValue { return Float64Slice(k, []float64{-13e8, -32.8, 4., 1e28}) },
func(k string) KeyValue { return String(k, "foo") },
func(k string) KeyValue { return String(k, "bar") },
func(k string) KeyValue { return StringSlice(k, []string{"foo", "bar", "baz"}) },
func(k string) KeyValue { return StringSlice(k, []string{"[]i1"}) },
}
func TestHashKVsEquality(t *testing.T) {
type testcase struct {
hash fnv.Hash
kvs []KeyValue
}
keys := []string{"k0", "k1"}
// Test all combinations up to length 3.
n := len(keyVals)
result := make([]testcase, 0, 1+len(keys)*(n+(n*n)+(n*n*n)))
result = append(result, testcase{hashKVs(nil), nil})
for _, key := range keys {
for i := 0; i < len(keyVals); i++ {
kvs := []KeyValue{keyVals[i](key)}
hash := hashKVs(kvs)
result = append(result, testcase{hash, kvs})
for j := 0; j < len(keyVals); j++ {
kvs := []KeyValue{
keyVals[i](key),
keyVals[j](key),
}
hash := hashKVs(kvs)
result = append(result, testcase{hash, kvs})
for k := 0; k < len(keyVals); k++ {
kvs := []KeyValue{
keyVals[i](key),
keyVals[j](key),
keyVals[k](key),
}
hash := hashKVs(kvs)
result = append(result, testcase{hash, kvs})
}
}
}
}
for i := 0; i < len(result); i++ {
hI, kvI := result[i].hash, result[i].kvs
for j := 0; j < len(result); j++ {
hJ, kvJ := result[j].hash, result[j].kvs
m := msg{i: i, j: j, hI: hI, hJ: hJ, kvI: kvI, kvJ: kvJ}
if i == j {
m.cmp = "=="
if hI != hJ {
t.Errorf("hashes not equal: %s", m)
}
} else {
m.cmp = "!="
if hI == hJ {
// Do not use testify/assert here. It is slow.
t.Errorf("hashes equal: %s", m)
}
}
}
}
}
type msg struct {
cmp string
i, j int
hI, hJ fnv.Hash
kvI, kvJ []KeyValue
}
func (m msg) String() string {
return fmt.Sprintf(
"(%d: %d)%s %s (%d: %d)%s",
m.i, m.hI, slice(m.kvI), m.cmp, m.j, m.hJ, slice(m.kvJ),
)
}
func slice(kvs []KeyValue) string {
if len(kvs) == 0 {
return "[]"
}
var b strings.Builder
_, _ = b.WriteRune('[')
_, _ = b.WriteString(string(kvs[0].Key))
_, _ = b.WriteRune(':')
_, _ = b.WriteString(kvs[0].Value.Emit())
for _, kv := range kvs[1:] {
_, _ = b.WriteRune(',')
_, _ = b.WriteString(string(kv.Key))
_, _ = b.WriteRune(':')
_, _ = b.WriteString(kv.Value.Emit())
}
_, _ = b.WriteRune(']')
return b.String()
}
func BenchmarkHashKVs(b *testing.B) {
attrs := make([]KeyValue, len(keyVals))
for i := range keyVals {
attrs[i] = keyVals[i]("k")
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
hashKVs(attrs)
}
}
func FuzzHashKVs(f *testing.F) {
// Add seed inputs to ensure coverage of edge cases.
f.Add("", "", "", "", "", "", 0, int64(0), 0.0, false, uint8(0))
f.Add("key", "value", "🌍", "test", "bool", "float", -1, int64(-1), -1.0, true, uint8(1))
f.Add("duplicate", "duplicate", "duplicate", "duplicate", "duplicate", "NaN",
0, int64(0), math.Inf(1), false, uint8(2))
f.Fuzz(func(t *testing.T, k1, k2, k3, k4, k5, s string, i int, i64 int64, fVal float64, b bool, sliceType uint8) {
// Test variable number of attributes (0-10).
numAttrs := len(k1) % 11 // Use key length to determine number of attributes.
if numAttrs == 0 && k1 == "" {
// Test empty set.
h := hashKVs(nil)
if h == 0 {
t.Error("hash of empty slice should not be zero")
}
return
}
var kvs []KeyValue
// Add basic types.
if numAttrs > 0 {
kvs = append(kvs, String(k1, s))
}
if numAttrs > 1 {
kvs = append(kvs, Int(k2, i))
}
if numAttrs > 2 {
kvs = append(kvs, Int64(k3, i64))
}
if numAttrs > 3 {
kvs = append(kvs, Float64(k4, fVal))
}
if numAttrs > 4 {
kvs = append(kvs, Bool(k5, b))
}
// Add slice types based on sliceType parameter
if numAttrs > 5 {
switch sliceType % 4 {
case 0:
// Test BoolSlice with variable length.
bools := make([]bool, len(s)%5) // 0-4 elements
for i := range bools {
bools[i] = (i+len(k1))%2 == 0
}
kvs = append(kvs, BoolSlice("boolslice", bools))
case 1:
// Test IntSlice with variable length.
ints := make([]int, len(s)%6) // 0-5 elements
for i := range ints {
ints[i] = i + len(k2)
}
kvs = append(kvs, IntSlice("intslice", ints))
case 2:
// Test Int64Slice with variable length.
int64s := make([]int64, len(s)%4) // 0-3 elements
for i := range int64s {
int64s[i] = int64(i) + i64
}
kvs = append(kvs, Int64Slice("int64slice", int64s))
case 3:
// Test Float64Slice with variable length and special values.
float64s := make([]float64, len(s)%5) // 0-4 elements
for i := range float64s {
switch i % 4 {
case 0:
float64s[i] = fVal
case 1:
float64s[i] = math.Inf(1) // +Inf
case 2:
float64s[i] = math.Inf(-1) // -Inf
case 3:
float64s[i] = math.NaN() // NaN
}
}
kvs = append(kvs, Float64Slice("float64slice", float64s))
}
}
// Add StringSlice.
if numAttrs > 6 {
strings := make([]string, len(k1)%4) // 0-3 elements
for i := range strings {
strings[i] = fmt.Sprintf("%s_%d", s, i)
}
kvs = append(kvs, StringSlice("stringslice", strings))
}
// Test duplicate keys (should be handled by Set construction).
if numAttrs > 7 && k1 != "" {
kvs = append(kvs, String(k1, "duplicate_key_value"))
}
// Add more attributes with Unicode keys.
if numAttrs > 8 {
kvs = append(kvs, String("🔑", "unicode_key"))
}
if numAttrs > 9 {
kvs = append(kvs, String("empty", ""))
}
// Sort to ensure consistent ordering (as Set would do).
slices.SortFunc(kvs, func(a, b KeyValue) int {
return cmp.Compare(string(a.Key), string(b.Key))
})
// Remove duplicates (as Set will do).
if len(kvs) > 1 {
j := 0
for i := 1; i < len(kvs); i++ {
if kvs[j].Key != kvs[i].Key {
j++
kvs[j] = kvs[i]
} else {
// Keep the later value for duplicate keys.
kvs[j] = kvs[i]
}
}
kvs = kvs[:j+1]
}
// Hash the key-value pairs.
h1 := hashKVs(kvs)
h2 := hashKVs(kvs) // Should be deterministic
if h1 != h2 {
t.Errorf("hash is not deterministic: %d != %d for kvs=%v", h1, h2, kvs)
}
if h1 == 0 && len(kvs) > 0 {
t.Errorf("hash should not be zero for non-empty input: kvs=%v", kvs)
}
// Test that different inputs produce different hashes (most of the time).
// This is a probabilistic test - collisions are possible but rare.
if len(kvs) > 0 {
// Modify one value slightly.
modifiedKvs := make([]KeyValue, len(kvs))
copy(modifiedKvs, kvs)
if len(modifiedKvs) > 0 {
switch modifiedKvs[0].Value.Type() {
case STRING:
modifiedKvs[0] = String(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsString()+"_modified")
case INT64:
modifiedKvs[0] = Int64(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsInt64()+1)
case BOOL:
modifiedKvs[0] = Bool(string(modifiedKvs[0].Key), !modifiedKvs[0].Value.AsBool())
case FLOAT64:
val := modifiedKvs[0].Value.AsFloat64()
if !math.IsNaN(val) && !math.IsInf(val, 0) {
modifiedKvs[0] = Float64(string(modifiedKvs[0].Key), val+1.0)
}
}
h3 := hashKVs(modifiedKvs)
// Note: We don't assert h1 != h3 because hash collisions are theoretically possible
// but we can log suspicious cases for manual review.
if h1 == h3 && !reflect.DeepEqual(kvs, modifiedKvs) {
t.Logf("Potential hash collision detected: original=%v, modified=%v, hash=%d", kvs, modifiedKvs, h1)
}
}
}
})
}