From 6c5c797ec979de0b938cf88982c1ddceb51647c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Tue, 23 Jul 2024 16:34:19 +0200 Subject: [PATCH 01/14] ddtrace/tracer: dereference pointers to supported types in span.SetTag --- .gitlab-ci.yml | 2 +- ddtrace/tracer/span.go | 3 ++ ddtrace/tracer/span_test.go | 27 ++++++++++++++--- ddtrace/tracer/util.go | 52 +++++++++++++++++++++++++++++++++ ddtrace/tracer/util_test.go | 58 +++++++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00f8b74e72..16c4c75d68 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: INDEX_FILE: index.txt KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: dd-trace-go FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true" - BENCHMARK_TARGETS: "BenchmarkStartRequestSpan|BenchmarkHttpServeTrace|BenchmarkTracerAddSpans|BenchmarkStartSpan|BenchmarkSingleSpanRetention|BenchmarkOTelApiWithCustomTags|BenchmarkInjectW3C|BenchmarkExtractW3C|BenchmarkPartialFlushing|BenchmarkGraphQL|BenchmarkSampleWAFContext|BenchmarkCaptureStackTrace" + BENCHMARK_TARGETS: "BenchmarkStartRequestSpan|BenchmarkHttpServeTrace|BenchmarkTracerAddSpans|BenchmarkStartSpan|BenchmarkSingleSpanRetention|BenchmarkOTelApiWithCustomTags|BenchmarkInjectW3C|BenchmarkExtractW3C|BenchmarkPartialFlushing|BenchmarkGraphQL|BenchmarkSampleWAFContext|BenchmarkCaptureStackTrace|BenchmarkSetTagString|BenchmarkSetTagStringPtr|BenchmarkSetTagMetric" include: - ".gitlab/benchmarks.yml" diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 14f816f9f1..323003e23a 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -111,6 +111,9 @@ func (s *span) BaggageItem(key string) string { // SetTag adds a set of key/value metadata to the span. func (s *span) SetTag(key string, value interface{}) { + // To avoid dumping the memory address in case value is a pointer, we dereference it. + // Any pointer value that is a pointer to a pointer will be dumped as a string. + value = dereference(value) s.Lock() defer s.Unlock() // We don't lock spans when flushing, so we could have a data race when diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 19e8edb8a4..e918acf45a 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -369,6 +369,13 @@ func TestSpanSetTag(t *testing.T) { span.SetTag("struct", sharedinternal.MetaStructValue{Value: testValue}) require.Equal(t, testValue, span.MetaStruct["struct"]) + s := "string" + span.SetTag("str_ptr", &s) + assert.Equal(s, span.Meta["str_ptr"]) + + span.SetTag("nil_str_ptr", (*string)(nil)) + assert.Equal("", span.Meta["nil_str_ptr"]) + assert.Panics(func() { span.SetTag("panicStringer", &panicStringer{}) }) @@ -1024,18 +1031,18 @@ func TestSetUserPropagatedUserID(t *testing.T) { func BenchmarkSetTagMetric(b *testing.B) { span := newBasicSpan("bench.span") - keys := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") b.ResetTimer() for i := 0; i < b.N; i++ { - k := string(keys[i%len(keys)]) + k := keys[i%len(keys)] span.SetTag(k, float64(12.34)) } } func BenchmarkSetTagString(b *testing.B) { span := newBasicSpan("bench.span") - keys := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") b.ResetTimer() for i := 0; i < b.N; i++ { @@ -1044,9 +1051,21 @@ func BenchmarkSetTagString(b *testing.B) { } } +func BenchmarkSetTagStringPtr(b *testing.B) { + span := newBasicSpan("bench.span") + keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") + v := makePointer("some text") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + k := keys[i%len(keys)] + span.SetTag(k, v) + } +} + func BenchmarkSetTagStringer(b *testing.B) { span := newBasicSpan("bench.span") - keys := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") value := &stringer{} b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/ddtrace/tracer/util.go b/ddtrace/tracer/util.go index 67ee16149f..9a2da9fb0d 100644 --- a/ddtrace/tracer/util.go +++ b/ddtrace/tracer/util.go @@ -20,6 +20,8 @@ import ( func toFloat64(value interface{}) (f float64, ok bool) { const max = (int64(1) << 53) - 1 const min = -max + // If any other type is added here, remember to add it to the type switch in + // the `span.SetTag` function to handle pointers to these supported types. switch i := value.(type) { case byte: return float64(i), true @@ -122,3 +124,53 @@ func parsePropagatableTraceTags(s string) (map[string]string, error) { tags[key] = s[start:] return tags, nil } + +func dereference(value any) any { + // Falling into one of the cases will dereference the pointer and return the + // value of the pointer. It adds one allocation due to casting. + switch value.(type) { + case *bool: + return dereferenceGeneric(value.(*bool)) + case *string: + return dereferenceGeneric(value.(*string)) + // Supported type by toFloat64 + case *byte: + return dereferenceGeneric(value.(*byte)) + case *float32: + return dereferenceGeneric(value.(*float32)) + case *float64: + return dereferenceGeneric(value.(*float64)) + case *int: + return dereferenceGeneric(value.(*int)) + case *int8: + return dereferenceGeneric(value.(*int8)) + case *int16: + return dereferenceGeneric(value.(*int16)) + case *int32: + return dereferenceGeneric(value.(*int32)) + case *int64: + return dereferenceGeneric(value.(*int64)) + case *uint: + return dereferenceGeneric(value.(*uint)) + case *uint16: + return dereferenceGeneric(value.(*uint16)) + case *uint32: + return dereferenceGeneric(value.(*uint32)) + case *uint64: + return dereferenceGeneric(value.(*uint64)) + case *samplernames.SamplerName: + v := value.(*samplernames.SamplerName) + if v == nil { + return samplernames.Unknown + } + return *v + } + return value +} + +func dereferenceGeneric[T any](value *T) T { + if value == nil { + return *new(T) + } + return *value +} diff --git a/ddtrace/tracer/util_test.go b/ddtrace/tracer/util_test.go index 87e4d9ef19..8c40681ff4 100644 --- a/ddtrace/tracer/util_test.go +++ b/ddtrace/tracer/util_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames" ) func TestToFloat64(t *testing.T) { @@ -115,3 +116,60 @@ func TestParsePropagatableTraceTags(t *testing.T) { }) } } + +func TestDereference(t *testing.T) { + for i, tt := range []struct { + value interface{} + expected interface{} + }{ + {makePointer(1), 1}, + {makePointer(byte(1)), byte(1)}, + {makePointer(int16(1)), int16(1)}, + {makePointer(int32(1)), int32(1)}, + {makePointer(int64(1)), int64(1)}, + {makePointer(uint(1)), uint(1)}, + {makePointer(uint16(1)), uint16(1)}, + {makePointer(uint32(1)), uint32(1)}, + {makePointer(uint64(1)), uint64(1)}, + {makePointer("a"), "a"}, + {makePointer(float32(1.25)), float32(1.25)}, + {makePointer(float64(1.25)), float64(1.25)}, + {makePointer(true), true}, + {makePointer(false), false}, + {makePointer(samplernames.SingleSpan), samplernames.SingleSpan}, + {(*int)(nil), 0}, + {(*byte)(nil), byte(0)}, + {(*int16)(nil), int16(0)}, + {(*int32)(nil), int32(0)}, + {(*int64)(nil), int64(0)}, + {(*uint)(nil), uint(0)}, + {(*uint16)(nil), uint16(0)}, + {(*uint32)(nil), uint32(0)}, + {(*uint64)(nil), uint64(0)}, + {(*string)(nil), ""}, + {(*float32)(nil), float32(0)}, + {(*float64)(nil), float64(0)}, + {(*bool)(nil), false}, + {(*samplernames.SamplerName)(nil), samplernames.Unknown}, + {newSpan("test", "service", "resource", 1, 2, 0), "itself"}, + } { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + actual := dereference(tt.value) + // This is a special case where we want to compare the value itself + // because the dereference function returns the given value. + if tt.expected == "itself" { + if actual != tt.value { + t.Fatalf("expected: %#v, got: %#v", tt.value, actual) + } + return + } + if actual != tt.expected { + t.Fatalf("expected: %#v, got: %#v", tt.expected, actual) + } + }) + } +} + +func makePointer[T any](value T) *T { + return &value +} From 46d44dc060017df2d35c5935d32a3e2a295eb1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Thu, 25 Jul 2024 14:22:42 +0200 Subject: [PATCH 02/14] ddtrace/tracer: basic implementation for pooled linked span tag list --- ddtrace/tracer/span.go | 18 ++++---- ddtrace/tracer/span_test.go | 34 +++++++++++--- ddtrace/tracer/tag.go | 90 +++++++++++++++++++++++++++++++++++++ ddtrace/tracer/tag_test.go | 87 +++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 ddtrace/tracer/tag.go create mode 100644 ddtrace/tracer/tag_test.go diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 323003e23a..9ca46f8958 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -74,6 +74,7 @@ type span struct { Meta map[string]string `msg:"meta,omitempty"` // arbitrary map of metadata MetaStruct metaStructMap `msg:"meta_struct,omitempty"` // arbitrary map of metadata with structured values Metrics map[string]float64 `msg:"metrics,omitempty"` // arbitrary map of numeric metrics + tags *spanTags `msg:"-"` // metadata and numeric metrics tags' optimized storage SpanID uint64 `msg:"span_id"` // identifier of this span TraceID uint64 `msg:"trace_id"` // lower 64-bits of the root span identifier ParentID uint64 `msg:"parent_id"` // identifier of the span's direct parent @@ -122,6 +123,9 @@ func (s *span) SetTag(key string, value interface{}) { if s.finished { return } + if s.tags == nil { + s.tags = &spanTags{} + } switch key { case ext.Error: s.setTagError(value, errorConfig{ @@ -386,10 +390,9 @@ func takeStacktrace(n, skip uint) string { // setMeta sets a string tag. This method is not safe for concurrent use. func (s *span) setMeta(key, v string) { - if s.Meta == nil { - s.Meta = make(map[string]string, 1) + if s.tags == nil { + s.tags = &spanTags{} } - delete(s.Metrics, key) switch key { case ext.SpanName: s.Name = v @@ -400,7 +403,7 @@ func (s *span) setMeta(key, v string) { case ext.SpanType: s.Type = v default: - s.Meta[key] = v + s.tags.append(key, v) } } @@ -440,10 +443,9 @@ func (s *span) setTagBool(key string, v bool) { // setMetric sets a numeric tag, in our case called a metric. This method // is not safe for concurrent use. func (s *span) setMetric(key string, v float64) { - if s.Metrics == nil { - s.Metrics = make(map[string]float64, 1) + if s.tags == nil { + s.tags = &spanTags{} } - delete(s.Meta, key) switch key { case ext.ManualKeep: if v == float64(samplernames.AppSec) { @@ -454,7 +456,7 @@ func (s *span) setMetric(key string, v float64) { // We have it here for backward compatibility. s.setSamplingPriorityLocked(int(v), samplernames.Manual) default: - s.Metrics[key] = v + s.tags.append(key, v) } } diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index e918acf45a..d7543972db 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -1032,7 +1032,9 @@ func TestSetUserPropagatedUserID(t *testing.T) { func BenchmarkSetTagMetric(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") - + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1043,10 +1045,12 @@ func BenchmarkSetTagMetric(b *testing.B) { func BenchmarkSetTagString(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") - + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } b.ResetTimer() for i := 0; i < b.N; i++ { - k := string(keys[i%len(keys)]) + k := keys[i%len(keys)] span.SetTag(k, "some text") } } @@ -1055,7 +1059,9 @@ func BenchmarkSetTagStringPtr(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") v := makePointer("some text") - + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1067,6 +1073,9 @@ func BenchmarkSetTagStringer(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") value := &stringer{} + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } b.ResetTimer() for i := 0; i < b.N; i++ { k := string(keys[i%len(keys)]) @@ -1077,7 +1086,9 @@ func BenchmarkSetTagStringer(b *testing.B) { func BenchmarkSetTagField(b *testing.B) { span := newBasicSpan("bench.span") keys := []string{ext.ServiceName, ext.ResourceName, ext.SpanType} - + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1126,3 +1137,16 @@ func testConcurrentSpanSetTag(t *testing.T) { } wg.Wait() } + +func BenchmarkSpanFinish(b *testing.B) { + tracer := newTracer(withTransport(newDefaultTransport())) + tracer.config.partialFlushEnabled = false + defer tracer.Stop() + span := tracer.newRootSpan("pylons.request", "pylons", "/") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + span.finished = false + span.Finish() + } +} diff --git a/ddtrace/tracer/tag.go b/ddtrace/tracer/tag.go new file mode 100644 index 0000000000..b41f3f42b3 --- /dev/null +++ b/ddtrace/tracer/tag.go @@ -0,0 +1,90 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package tracer + +import "sync" + +type tag struct { + key string + value any // This adds an overhead of 8 bytes when storing a float64. + sibling *tag +} + +type tagPool struct { + mu sync.Mutex + tail *tag +} + +var tagsPool = &tagPool{} + +func (tp *tagPool) Get() *tag { + tp.mu.Lock() + defer tp.mu.Unlock() + + if tp.tail == nil { + return &tag{} + } + tt := tp.tail + tp.tail = tt.sibling + tt.sibling = nil + return tt +} + +func (tp *tagPool) Put(tt *tag) { + tp.mu.Lock() + defer tp.mu.Unlock() + + tt.sibling = tp.tail + tp.tail = tt +} + +func getTagFromPool() *tag { + return tagsPool.Get() +} + +func putTagToPool(tt *tag) { + tt.key = "" + tt.value = nil + tt.sibling = nil + tagsPool.Put(tt) +} + +// spanTags is an append-only linked list of tags. +// Its usage assumes the following: +// - only works under a locked span. +// - pool is pre-allocated to a sensible size, taking into account that the tracer may be used concurrently. +type spanTags struct { + head *tag + tail *tag +} + +func (st *spanTags) append(key string, value any) { + tt := getTagFromPool() + tt.key = key + tt.value = value + if st.head == nil { + st.head = tt + st.tail = tt + return + } + tail := st.tail + tail.sibling = tt + st.tail = tt +} + +func (st *spanTags) reset() { + tt := st.head + for { + nt := tt.sibling + putTagToPool(tt) + if nt == nil { + break + } + tt = nt + } + st.head = nil + st.tail = nil +} diff --git a/ddtrace/tracer/tag_test.go b/ddtrace/tracer/tag_test.go new file mode 100644 index 0000000000..e20d54b602 --- /dev/null +++ b/ddtrace/tracer/tag_test.go @@ -0,0 +1,87 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package tracer + +import ( + "testing" +) + +func TestSpanTagsSet(t *testing.T) { + var st spanTags + st.append("key1", "value1") + st.append("key2", 0.1) + st.append("key3", 1) + st.append("key2", "value2") + if st.head == nil { + t.Fatal() + } + if st.head.key != "key1" { + t.Fatal() + } + if st.head.value != "value1" { + t.Fatal() + } + if st.tail == nil { + t.Fatal() + } + if st.tail.key != "key2" { + t.Fatal() + } + if st.tail.value != "value2" { + t.Fatal() + } +} + +func TestSpanTagsReset(t *testing.T) { + var ( + st spanTags + elems = 100 + ) + for i := 0; i < elems; i++ { + st.append("key", "value") + } + tags := make([]*tag, elems) + tt := st.head + for i := range tags { + tags[i] = tt + tt = tt.sibling + } + st.reset() + if st.head != nil { + t.Fatal("head not nil") + } + if st.tail != nil { + t.Fatal("tail not nil") + } + for i, tag := range tags { + if tag.key != "" { + t.Fatalf("key not empty at %d", i) + } + if tag.value != nil { + t.Fatalf("value not nil at %d", i) + } + } +} + +func BenchmarkSpanTagsSet(b *testing.B) { + var st spanTags + b.ResetTimer() + for i := 0; i < b.N; i++ { + st.append("key", "value") + st.reset() + } +} + +func BenchmarkSpanTagsSetPreallocated(b *testing.B) { + var st spanTags + for i := 0; i < b.N; i++ { + tagsPool.Put(&tag{}) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + st.append("key", "value") + } +} From cebf2899ea2621aab01b54bb67f97504b5f6c97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Fri, 26 Jul 2024 08:30:38 +0200 Subject: [PATCH 03/14] ddtrace/tracer: rename tag.go to spantags.go --- ddtrace/tracer/{tag.go => spantags.go} | 0 ddtrace/tracer/{tag_test.go => spantags_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename ddtrace/tracer/{tag.go => spantags.go} (100%) rename ddtrace/tracer/{tag_test.go => spantags_test.go} (100%) diff --git a/ddtrace/tracer/tag.go b/ddtrace/tracer/spantags.go similarity index 100% rename from ddtrace/tracer/tag.go rename to ddtrace/tracer/spantags.go diff --git a/ddtrace/tracer/tag_test.go b/ddtrace/tracer/spantags_test.go similarity index 100% rename from ddtrace/tracer/tag_test.go rename to ddtrace/tracer/spantags_test.go From af4d58632683d4fc1a878fa28eace68588875068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Fri, 26 Jul 2024 10:29:22 +0200 Subject: [PATCH 04/14] ddtrace/tracer: embracing unsafe.Pointer and generic types --- ddtrace/tracer/span.go | 4 +- ddtrace/tracer/span_test.go | 11 ++-- ddtrace/tracer/spantags.go | 93 +++++++++++++++++++++++---------- ddtrace/tracer/spantags_test.go | 45 +++++++++------- 4 files changed, 98 insertions(+), 55 deletions(-) diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 9ca46f8958..ace1aede9d 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -403,7 +403,7 @@ func (s *span) setMeta(key, v string) { case ext.SpanType: s.Type = v default: - s.tags.append(key, v) + s.tags.AppendMeta(key, v) } } @@ -456,7 +456,7 @@ func (s *span) setMetric(key string, v float64) { // We have it here for backward compatibility. s.setSamplingPriorityLocked(int(v), samplernames.Manual) default: - s.tags.append(key, v) + s.tags.appendMetric(key, v) } } diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index d7543972db..38a13bfb04 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "testing" "time" + "unsafe" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal" @@ -1033,7 +1034,7 @@ func BenchmarkSetTagMetric(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -1046,7 +1047,7 @@ func BenchmarkSetTagString(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -1060,7 +1061,7 @@ func BenchmarkSetTagStringPtr(b *testing.B) { keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") v := makePointer("some text") for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -1074,7 +1075,7 @@ func BenchmarkSetTagStringer(b *testing.B) { keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") value := &stringer{} for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { @@ -1087,7 +1088,7 @@ func BenchmarkSetTagField(b *testing.B) { span := newBasicSpan("bench.span") keys := []string{ext.ServiceName, ext.ResourceName, ext.SpanType} for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/ddtrace/tracer/spantags.go b/ddtrace/tracer/spantags.go index b41f3f42b3..007928a730 100644 --- a/ddtrace/tracer/spantags.go +++ b/ddtrace/tracer/spantags.go @@ -5,51 +5,67 @@ package tracer -import "sync" +import ( + "sync" + "unsafe" +) -type tag struct { +type meta struct { + value string +} + +type metric struct { + magicNumber int64 + value float64 +} + +type tag[T metric | meta] struct { key string - value any // This adds an overhead of 8 bytes when storing a float64. - sibling *tag + value T // This adds an overhead of 8 bytes when storing a float64. + sibling unsafe.Pointer } type tagPool struct { mu sync.Mutex - tail *tag + tail unsafe.Pointer } var tagsPool = &tagPool{} -func (tp *tagPool) Get() *tag { +func (tp *tagPool) Get() unsafe.Pointer { tp.mu.Lock() defer tp.mu.Unlock() if tp.tail == nil { - return &tag{} + return unsafe.Pointer(&tag[meta]{}) } - tt := tp.tail + ptr := tp.tail + tt := (*tag[meta])(ptr) tp.tail = tt.sibling tt.sibling = nil - return tt + return ptr } -func (tp *tagPool) Put(tt *tag) { +func (tp *tagPool) Put(ptr unsafe.Pointer) { tp.mu.Lock() defer tp.mu.Unlock() + tt := (*tag[meta])(ptr) tt.sibling = tp.tail - tp.tail = tt + tp.tail = ptr } -func getTagFromPool() *tag { - return tagsPool.Get() +func getTagFromPool[T meta | metric]() *tag[T] { + ptr := tagsPool.Get() + return (*tag[T])(ptr) } -func putTagToPool(tt *tag) { +func putTagToPool[T meta | metric](tt *tag[T]) { + var zero T tt.key = "" - tt.value = nil + tt.value = zero tt.sibling = nil - tagsPool.Put(tt) + tagsPool.Put(unsafe.Pointer(tt)) } // spanTags is an append-only linked list of tags. @@ -57,33 +73,54 @@ func putTagToPool(tt *tag) { // - only works under a locked span. // - pool is pre-allocated to a sensible size, taking into account that the tracer may be used concurrently. type spanTags struct { - head *tag - tail *tag + head unsafe.Pointer + tail unsafe.Pointer } -func (st *spanTags) append(key string, value any) { - tt := getTagFromPool() +func (st *spanTags) Head() *tag[meta] { + return (*tag[meta])(st.head) +} + +func (st *spanTags) Tail() *tag[meta] { + return (*tag[meta])(st.tail) +} + +func (st *spanTags) AppendMeta(key string, value string) { + tt := getTagFromPool[meta]() + tt.key = key + tt.value.value = value + ptr := unsafe.Pointer(tt) + st.updateTail(ptr) +} + +func (st *spanTags) appendMetric(key string, value float64) { + tt := getTagFromPool[metric]() tt.key = key - tt.value = value + tt.value.value = value + ptr := unsafe.Pointer(tt) + st.updateTail(ptr) +} + +func (st *spanTags) updateTail(ptr unsafe.Pointer) { if st.head == nil { - st.head = tt - st.tail = tt + st.head = ptr + st.tail = ptr return } - tail := st.tail - tail.sibling = tt - st.tail = tt + tail := (*tag[meta])(st.tail) + tail.sibling = ptr + st.tail = ptr } func (st *spanTags) reset() { - tt := st.head + tt := st.Head() for { nt := tt.sibling putTagToPool(tt) if nt == nil { break } - tt = nt + tt = (*tag[meta])(nt) } st.head = nil st.tail = nil diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go index e20d54b602..69ea380b15 100644 --- a/ddtrace/tracer/spantags_test.go +++ b/ddtrace/tracer/spantags_test.go @@ -7,30 +7,33 @@ package tracer import ( "testing" + "unsafe" ) func TestSpanTagsSet(t *testing.T) { var st spanTags - st.append("key1", "value1") - st.append("key2", 0.1) - st.append("key3", 1) - st.append("key2", "value2") - if st.head == nil { + st.AppendMeta("key1", "value1") + st.appendMetric("key2", 0.1) + st.appendMetric("key3", 1) + st.AppendMeta("key2", "value2") + head := st.Head() + if head == nil { t.Fatal() } - if st.head.key != "key1" { + if head.key != "key1" { t.Fatal() } - if st.head.value != "value1" { + if head.value.value != "value1" { t.Fatal() } - if st.tail == nil { + tail := st.Tail() + if tail == nil { t.Fatal() } - if st.tail.key != "key2" { + if tail.key != "key2" { t.Fatal() } - if st.tail.value != "value2" { + if tail.value.value != "value2" { t.Fatal() } } @@ -41,26 +44,28 @@ func TestSpanTagsReset(t *testing.T) { elems = 100 ) for i := 0; i < elems; i++ { - st.append("key", "value") + st.AppendMeta("key", "value") } - tags := make([]*tag, elems) - tt := st.head + tags := make([]*tag[meta], elems) + tt := st.Head() for i := range tags { tags[i] = tt - tt = tt.sibling + tt = (*tag[meta])(tt.sibling) } st.reset() - if st.head != nil { + head := st.Head() + if head != nil { t.Fatal("head not nil") } - if st.tail != nil { + tail := st.Tail() + if tail != nil { t.Fatal("tail not nil") } for i, tag := range tags { if tag.key != "" { t.Fatalf("key not empty at %d", i) } - if tag.value != nil { + if tag.value.value != "" { t.Fatalf("value not nil at %d", i) } } @@ -70,7 +75,7 @@ func BenchmarkSpanTagsSet(b *testing.B) { var st spanTags b.ResetTimer() for i := 0; i < b.N; i++ { - st.append("key", "value") + st.AppendMeta("key", "value") st.reset() } } @@ -78,10 +83,10 @@ func BenchmarkSpanTagsSet(b *testing.B) { func BenchmarkSpanTagsSetPreallocated(b *testing.B) { var st spanTags for i := 0; i < b.N; i++ { - tagsPool.Put(&tag{}) + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) } b.ResetTimer() for i := 0; i < b.N; i++ { - st.append("key", "value") + st.AppendMeta("key", "value") } } From 2f610fdda193cd483fd3aa498de9de063bebadcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Fri, 26 Jul 2024 11:29:30 +0200 Subject: [PATCH 05/14] ddtrace/tracer: add comments to clarify spanTags implementation --- ddtrace/tracer/span.go | 2 +- ddtrace/tracer/spantags.go | 36 +++++++++++++++++++++++++++------ ddtrace/tracer/spantags_test.go | 8 ++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index ace1aede9d..9b1d2fb52a 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -456,7 +456,7 @@ func (s *span) setMetric(key string, v float64) { // We have it here for backward compatibility. s.setSamplingPriorityLocked(int(v), samplernames.Manual) default: - s.tags.appendMetric(key, v) + s.tags.AppendMetric(key, v) } } diff --git a/ddtrace/tracer/spantags.go b/ddtrace/tracer/spantags.go index 007928a730..5d26c652ff 100644 --- a/ddtrace/tracer/spantags.go +++ b/ddtrace/tracer/spantags.go @@ -19,19 +19,31 @@ type metric struct { value float64 } +// tag is a key-value pair with a sibling pointer to the next tag. +// This is the foundational block of the linked list that spanTags +// relies on. +// +// It's a generic type but the allowed types must have a size of 16 bytes. +// This is because the linked list uses unsafe.Pointer to link the tags, +// and we need to introduce a magic number for any value that is not string +// to identify it from the memory pointer type tag[T metric | meta] struct { key string - value T // This adds an overhead of 8 bytes when storing a float64. + value T sibling unsafe.Pointer } +// tagPool is a linked list of tags that are not in use. +// It's used to recycle tags and avoid unnecessary allocations. +// +// It's a better suit than sync.Pool because the tags can be linked +// together and we can avoid the overhead of sync.Pool's more complex logic. type tagPool struct { mu sync.Mutex tail unsafe.Pointer } -var tagsPool = &tagPool{} - +// Get returns a tag's unsafe.Pointer from the pool. func (tp *tagPool) Get() unsafe.Pointer { tp.mu.Lock() defer tp.mu.Unlock() @@ -46,6 +58,7 @@ func (tp *tagPool) Get() unsafe.Pointer { return ptr } +// Put puts a tag's unsafe.Pointer back into the pool. func (tp *tagPool) Put(ptr unsafe.Pointer) { tp.mu.Lock() defer tp.mu.Unlock() @@ -55,11 +68,15 @@ func (tp *tagPool) Put(ptr unsafe.Pointer) { tp.tail = ptr } +var tagsPool = &tagPool{} + +// getTagFromPool returns a tag from the pool, casting it to the desired type. func getTagFromPool[T meta | metric]() *tag[T] { ptr := tagsPool.Get() return (*tag[T])(ptr) } +// putTagToPool puts a tag back into the pool. func putTagToPool[T meta | metric](tt *tag[T]) { var zero T tt.key = "" @@ -70,21 +87,26 @@ func putTagToPool[T meta | metric](tt *tag[T]) { // spanTags is an append-only linked list of tags. // Its usage assumes the following: -// - only works under a locked span. +// - it works under a locked span. // - pool is pre-allocated to a sensible size, taking into account that the tracer may be used concurrently. type spanTags struct { head unsafe.Pointer tail unsafe.Pointer } +// Head returns the first tag in the linked list. func (st *spanTags) Head() *tag[meta] { + // This is safe because we know that all tags have the same memory layout. return (*tag[meta])(st.head) } +// Tail returns the last tag in the linked list. func (st *spanTags) Tail() *tag[meta] { + // This is safe because we know that all tags have the same memory layout. return (*tag[meta])(st.tail) } +// AppendMeta appends a string tag to the linked list. func (st *spanTags) AppendMeta(key string, value string) { tt := getTagFromPool[meta]() tt.key = key @@ -93,7 +115,8 @@ func (st *spanTags) AppendMeta(key string, value string) { st.updateTail(ptr) } -func (st *spanTags) appendMetric(key string, value float64) { +// AppendMetric appends a float64 tag to the linked list. +func (st *spanTags) AppendMetric(key string, value float64) { tt := getTagFromPool[metric]() tt.key = key tt.value.value = value @@ -112,7 +135,8 @@ func (st *spanTags) updateTail(ptr unsafe.Pointer) { st.tail = ptr } -func (st *spanTags) reset() { +// Reset releases all tags in the linked list back to the pool. +func (st *spanTags) Reset() { tt := st.Head() for { nt := tt.sibling diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go index 69ea380b15..c567eeabbe 100644 --- a/ddtrace/tracer/spantags_test.go +++ b/ddtrace/tracer/spantags_test.go @@ -13,8 +13,8 @@ import ( func TestSpanTagsSet(t *testing.T) { var st spanTags st.AppendMeta("key1", "value1") - st.appendMetric("key2", 0.1) - st.appendMetric("key3", 1) + st.AppendMetric("key2", 0.1) + st.AppendMetric("key3", 1) st.AppendMeta("key2", "value2") head := st.Head() if head == nil { @@ -52,7 +52,7 @@ func TestSpanTagsReset(t *testing.T) { tags[i] = tt tt = (*tag[meta])(tt.sibling) } - st.reset() + st.Reset() head := st.Head() if head != nil { t.Fatal("head not nil") @@ -76,7 +76,7 @@ func BenchmarkSpanTagsSet(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { st.AppendMeta("key", "value") - st.reset() + st.Reset() } } From e0f5da3741355fdd8da558c2035de70e7ab7590e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 31 Jul 2024 09:31:51 +0200 Subject: [PATCH 06/14] ddtrace/tracer: add benchmark for concurrently setting tags in a span --- ddtrace/tracer/span_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 38a13bfb04..13064bf1ba 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -1151,3 +1151,18 @@ func BenchmarkSpanFinish(b *testing.B) { span.Finish() } } + +func BenchmarkConcurrentSpanSetTag(b *testing.B) { + span := newBasicSpan("root") + defer span.Finish() + + wg := sync.WaitGroup{} + wg.Add(b.N) + for i := 0; i < b.N; i++ { + go func() { + span.SetTag("key", "value") + wg.Done() + }() + } + wg.Wait() +} From 399d412f719ff28ca044e43149b40682bb7440d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 31 Jul 2024 10:14:16 +0200 Subject: [PATCH 07/14] ddtrace/tracer: preallocate pool in BenchmarkConcurrentSpanSetTag --- ddtrace/tracer/span_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 13064bf1ba..d5f6c5ab7c 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -1155,9 +1155,12 @@ func BenchmarkSpanFinish(b *testing.B) { func BenchmarkConcurrentSpanSetTag(b *testing.B) { span := newBasicSpan("root") defer span.Finish() - + for i := 0; i < b.N; i++ { + tagsPool.Put(unsafe.Pointer(&tag[meta]{})) + } wg := sync.WaitGroup{} wg.Add(b.N) + b.ResetTimer() for i := 0; i < b.N; i++ { go func() { span.SetTag("key", "value") From 5860d9d970e49960c8950b8a1157db705f03f84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Tue, 6 Aug 2024 10:22:56 +0200 Subject: [PATCH 08/14] ddtrace/tracer: revert to map-based implementation --- ddtrace/tracer/span.go | 18 ++-- ddtrace/tracer/span_test.go | 25 ++---- ddtrace/tracer/spantags.go | 151 -------------------------------- ddtrace/tracer/spantags_test.go | 92 ------------------- 4 files changed, 14 insertions(+), 272 deletions(-) delete mode 100644 ddtrace/tracer/spantags.go delete mode 100644 ddtrace/tracer/spantags_test.go diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 9b1d2fb52a..323003e23a 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -74,7 +74,6 @@ type span struct { Meta map[string]string `msg:"meta,omitempty"` // arbitrary map of metadata MetaStruct metaStructMap `msg:"meta_struct,omitempty"` // arbitrary map of metadata with structured values Metrics map[string]float64 `msg:"metrics,omitempty"` // arbitrary map of numeric metrics - tags *spanTags `msg:"-"` // metadata and numeric metrics tags' optimized storage SpanID uint64 `msg:"span_id"` // identifier of this span TraceID uint64 `msg:"trace_id"` // lower 64-bits of the root span identifier ParentID uint64 `msg:"parent_id"` // identifier of the span's direct parent @@ -123,9 +122,6 @@ func (s *span) SetTag(key string, value interface{}) { if s.finished { return } - if s.tags == nil { - s.tags = &spanTags{} - } switch key { case ext.Error: s.setTagError(value, errorConfig{ @@ -390,9 +386,10 @@ func takeStacktrace(n, skip uint) string { // setMeta sets a string tag. This method is not safe for concurrent use. func (s *span) setMeta(key, v string) { - if s.tags == nil { - s.tags = &spanTags{} + if s.Meta == nil { + s.Meta = make(map[string]string, 1) } + delete(s.Metrics, key) switch key { case ext.SpanName: s.Name = v @@ -403,7 +400,7 @@ func (s *span) setMeta(key, v string) { case ext.SpanType: s.Type = v default: - s.tags.AppendMeta(key, v) + s.Meta[key] = v } } @@ -443,9 +440,10 @@ func (s *span) setTagBool(key string, v bool) { // setMetric sets a numeric tag, in our case called a metric. This method // is not safe for concurrent use. func (s *span) setMetric(key string, v float64) { - if s.tags == nil { - s.tags = &spanTags{} + if s.Metrics == nil { + s.Metrics = make(map[string]float64, 1) } + delete(s.Meta, key) switch key { case ext.ManualKeep: if v == float64(samplernames.AppSec) { @@ -456,7 +454,7 @@ func (s *span) setMetric(key string, v float64) { // We have it here for backward compatibility. s.setSamplingPriorityLocked(int(v), samplernames.Manual) default: - s.tags.AppendMetric(key, v) + s.Metrics[key] = v } } diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index d5f6c5ab7c..86452edc78 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -14,7 +14,6 @@ import ( "sync/atomic" "testing" "time" - "unsafe" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal" @@ -1033,9 +1032,7 @@ func TestSetUserPropagatedUserID(t *testing.T) { func BenchmarkSetTagMetric(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1046,9 +1043,7 @@ func BenchmarkSetTagMetric(b *testing.B) { func BenchmarkSetTagString(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1060,9 +1055,7 @@ func BenchmarkSetTagStringPtr(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") v := makePointer("some text") - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1074,9 +1067,7 @@ func BenchmarkSetTagStringer(b *testing.B) { span := newBasicSpan("bench.span") keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "") value := &stringer{} - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + b.ResetTimer() for i := 0; i < b.N; i++ { k := string(keys[i%len(keys)]) @@ -1087,9 +1078,7 @@ func BenchmarkSetTagStringer(b *testing.B) { func BenchmarkSetTagField(b *testing.B) { span := newBasicSpan("bench.span") keys := []string{ext.ServiceName, ext.ResourceName, ext.SpanType} - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + b.ResetTimer() for i := 0; i < b.N; i++ { k := keys[i%len(keys)] @@ -1155,9 +1144,7 @@ func BenchmarkSpanFinish(b *testing.B) { func BenchmarkConcurrentSpanSetTag(b *testing.B) { span := newBasicSpan("root") defer span.Finish() - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } + wg := sync.WaitGroup{} wg.Add(b.N) b.ResetTimer() diff --git a/ddtrace/tracer/spantags.go b/ddtrace/tracer/spantags.go deleted file mode 100644 index 5d26c652ff..0000000000 --- a/ddtrace/tracer/spantags.go +++ /dev/null @@ -1,151 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2024 Datadog, Inc. - -package tracer - -import ( - "sync" - "unsafe" -) - -type meta struct { - value string -} - -type metric struct { - magicNumber int64 - value float64 -} - -// tag is a key-value pair with a sibling pointer to the next tag. -// This is the foundational block of the linked list that spanTags -// relies on. -// -// It's a generic type but the allowed types must have a size of 16 bytes. -// This is because the linked list uses unsafe.Pointer to link the tags, -// and we need to introduce a magic number for any value that is not string -// to identify it from the memory pointer -type tag[T metric | meta] struct { - key string - value T - sibling unsafe.Pointer -} - -// tagPool is a linked list of tags that are not in use. -// It's used to recycle tags and avoid unnecessary allocations. -// -// It's a better suit than sync.Pool because the tags can be linked -// together and we can avoid the overhead of sync.Pool's more complex logic. -type tagPool struct { - mu sync.Mutex - tail unsafe.Pointer -} - -// Get returns a tag's unsafe.Pointer from the pool. -func (tp *tagPool) Get() unsafe.Pointer { - tp.mu.Lock() - defer tp.mu.Unlock() - - if tp.tail == nil { - return unsafe.Pointer(&tag[meta]{}) - } - ptr := tp.tail - tt := (*tag[meta])(ptr) - tp.tail = tt.sibling - tt.sibling = nil - return ptr -} - -// Put puts a tag's unsafe.Pointer back into the pool. -func (tp *tagPool) Put(ptr unsafe.Pointer) { - tp.mu.Lock() - defer tp.mu.Unlock() - - tt := (*tag[meta])(ptr) - tt.sibling = tp.tail - tp.tail = ptr -} - -var tagsPool = &tagPool{} - -// getTagFromPool returns a tag from the pool, casting it to the desired type. -func getTagFromPool[T meta | metric]() *tag[T] { - ptr := tagsPool.Get() - return (*tag[T])(ptr) -} - -// putTagToPool puts a tag back into the pool. -func putTagToPool[T meta | metric](tt *tag[T]) { - var zero T - tt.key = "" - tt.value = zero - tt.sibling = nil - tagsPool.Put(unsafe.Pointer(tt)) -} - -// spanTags is an append-only linked list of tags. -// Its usage assumes the following: -// - it works under a locked span. -// - pool is pre-allocated to a sensible size, taking into account that the tracer may be used concurrently. -type spanTags struct { - head unsafe.Pointer - tail unsafe.Pointer -} - -// Head returns the first tag in the linked list. -func (st *spanTags) Head() *tag[meta] { - // This is safe because we know that all tags have the same memory layout. - return (*tag[meta])(st.head) -} - -// Tail returns the last tag in the linked list. -func (st *spanTags) Tail() *tag[meta] { - // This is safe because we know that all tags have the same memory layout. - return (*tag[meta])(st.tail) -} - -// AppendMeta appends a string tag to the linked list. -func (st *spanTags) AppendMeta(key string, value string) { - tt := getTagFromPool[meta]() - tt.key = key - tt.value.value = value - ptr := unsafe.Pointer(tt) - st.updateTail(ptr) -} - -// AppendMetric appends a float64 tag to the linked list. -func (st *spanTags) AppendMetric(key string, value float64) { - tt := getTagFromPool[metric]() - tt.key = key - tt.value.value = value - ptr := unsafe.Pointer(tt) - st.updateTail(ptr) -} - -func (st *spanTags) updateTail(ptr unsafe.Pointer) { - if st.head == nil { - st.head = ptr - st.tail = ptr - return - } - tail := (*tag[meta])(st.tail) - tail.sibling = ptr - st.tail = ptr -} - -// Reset releases all tags in the linked list back to the pool. -func (st *spanTags) Reset() { - tt := st.Head() - for { - nt := tt.sibling - putTagToPool(tt) - if nt == nil { - break - } - tt = (*tag[meta])(nt) - } - st.head = nil - st.tail = nil -} diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go deleted file mode 100644 index c567eeabbe..0000000000 --- a/ddtrace/tracer/spantags_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2024 Datadog, Inc. - -package tracer - -import ( - "testing" - "unsafe" -) - -func TestSpanTagsSet(t *testing.T) { - var st spanTags - st.AppendMeta("key1", "value1") - st.AppendMetric("key2", 0.1) - st.AppendMetric("key3", 1) - st.AppendMeta("key2", "value2") - head := st.Head() - if head == nil { - t.Fatal() - } - if head.key != "key1" { - t.Fatal() - } - if head.value.value != "value1" { - t.Fatal() - } - tail := st.Tail() - if tail == nil { - t.Fatal() - } - if tail.key != "key2" { - t.Fatal() - } - if tail.value.value != "value2" { - t.Fatal() - } -} - -func TestSpanTagsReset(t *testing.T) { - var ( - st spanTags - elems = 100 - ) - for i := 0; i < elems; i++ { - st.AppendMeta("key", "value") - } - tags := make([]*tag[meta], elems) - tt := st.Head() - for i := range tags { - tags[i] = tt - tt = (*tag[meta])(tt.sibling) - } - st.Reset() - head := st.Head() - if head != nil { - t.Fatal("head not nil") - } - tail := st.Tail() - if tail != nil { - t.Fatal("tail not nil") - } - for i, tag := range tags { - if tag.key != "" { - t.Fatalf("key not empty at %d", i) - } - if tag.value.value != "" { - t.Fatalf("value not nil at %d", i) - } - } -} - -func BenchmarkSpanTagsSet(b *testing.B) { - var st spanTags - b.ResetTimer() - for i := 0; i < b.N; i++ { - st.AppendMeta("key", "value") - st.Reset() - } -} - -func BenchmarkSpanTagsSetPreallocated(b *testing.B) { - var st spanTags - for i := 0; i < b.N; i++ { - tagsPool.Put(unsafe.Pointer(&tag[meta]{})) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - st.AppendMeta("key", "value") - } -} From 51c0489cc933c598558e20c0f8238d2c6beeece4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Tue, 6 Aug 2024 12:43:33 +0200 Subject: [PATCH 09/14] ddtrace/tracer: implement distribution-based random number generator to test realistic scenarios --- ddtrace/tracer/spantags_test.go | 101 ++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ddtrace/tracer/spantags_test.go diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go new file mode 100644 index 0000000000..9720b56357 --- /dev/null +++ b/ddtrace/tracer/spantags_test.go @@ -0,0 +1,101 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package tracer + +import ( + "math/rand" + "strconv" + "testing" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" +) + +func BenchmarkSpanLifecycle(b *testing.B) { + r := rand.New(rand.NewSource(0)) + distribution := newDistributionRand( + b, + // The probabilities represent the distribution of the number of tags + // that are set on a span as observed in our production intake at the time + // of writing this benchmark. + []float64{0.01, 0.09, 0.4, 0.25, 0.15, 0.05, 0.04, 0.01}, + []float64{8.75, 14.59, 22.8, 31.2, 39.1, 43.5, 54.3, 70.0}, + ) + b.Run("baseline", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + span := StartSpan("benchmark") + span.Finish() + } + }) + b.Run("with unknown tags", func(b *testing.B) { + // precompute the tags + tags := make([]string, 70) + for i := 0; i < len(tags); i++ { + tags[i] = strconv.Itoa(i) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + span := StartSpan("benchmark") + nTags := int(distribution.generate(r)) + for j := 0; j < nTags; j++ { + span.SetTag(tags[j], "tag") + } + span.Finish() + } + }) + b.Run("with known tags", func(b *testing.B) { + // precompute the tags + tags := make([]string, 70) + for i := 0; i < len(tags); i++ { + tags[i] = strconv.Itoa(i) + } + tags[0] = ext.Environment + b.ResetTimer() + for i := 0; i < b.N; i++ { + span := StartSpan("benchmark") + nTags := int(distribution.generate(r)) + for j := 0; j < nTags; j++ { + span.SetTag(tags[j], "tag") + } + span.Finish() + } + }) +} + +// distributionRand is a helper for generating random numbers following +// a given probability distribution. It implements the inverse transform +// sampling method. +type distributionRand struct { + b *testing.B + cdf []float64 + values []float64 +} + +func newDistributionRand(b *testing.B, probabilities []float64, values []float64) *distributionRand { + b.Helper() + cdf := make([]float64, len(probabilities)) + sum := 0.0 + for i, p := range probabilities { + sum += p + cdf[i] = sum + } + return &distributionRand{ + b: b, + cdf: cdf, + values: values, + } +} + +func (d *distributionRand) generate(r *rand.Rand) float64 { + d.b.Helper() + u := r.Float64() + for i, c := range d.cdf { + if u <= c { + return d.values[i] + } + } + return d.values[len(d.values)-1] +} From dc77c97019edd51d7f228149974e0c29373d01c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Tue, 6 Aug 2024 15:00:43 +0200 Subject: [PATCH 10/14] ddtrace/tracer: preallocate meta map --- ddtrace/tracer/span.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 323003e23a..85939a5b43 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -387,7 +387,7 @@ func takeStacktrace(n, skip uint) string { // setMeta sets a string tag. This method is not safe for concurrent use. func (s *span) setMeta(key, v string) { if s.Meta == nil { - s.Meta = make(map[string]string, 1) + s.Meta = make(map[string]string, 5) } delete(s.Metrics, key) switch key { From ed88dac3836def3ae6d80056cbf0ee77b8bd228b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 7 Aug 2024 15:12:36 +0200 Subject: [PATCH 11/14] ddtrace/tracer: use a shared function to create span.Meta map in unit tests and production code --- ddtrace/tracer/span.go | 4 ++++ ddtrace/tracer/span_test.go | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 85939a5b43..4236521435 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -787,3 +787,7 @@ const ( keyUserScope = "usr.scope" keyUserSessionID = "usr.session_id" ) + +func defaultMetaMap() map[string]string { + return make(map[string]string, 5) +} diff --git a/ddtrace/tracer/span_test.go b/ddtrace/tracer/span_test.go index 5e7d16a7b6..227f659a00 100644 --- a/ddtrace/tracer/span_test.go +++ b/ddtrace/tracer/span_test.go @@ -34,7 +34,7 @@ func newSpan(name, service, resource string, spanID, traceID, parentID uint64) * Name: name, Service: service, Resource: resource, - Meta: map[string]string{}, + Meta: defaultMetaMap(), Metrics: map[string]float64{}, SpanID: spanID, TraceID: traceID, @@ -1147,12 +1147,19 @@ func BenchmarkConcurrentSpanSetTag(b *testing.B) { wg := sync.WaitGroup{} wg.Add(b.N) - b.ResetTimer() + + // Preallocate goroutines to avoid benchmarking goroutine creation + pole := make(chan struct{}) for i := 0; i < b.N; i++ { go func() { + // Wait for all goroutines to start + <-pole span.SetTag("key", "value") wg.Done() }() } + + b.ResetTimer() + close(pole) wg.Wait() } From 8595e2cbdebd26284bf68b98d4bc18f33cd5f373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 7 Aug 2024 15:13:18 +0200 Subject: [PATCH 12/14] ddtrace/tracer: introduce pooled span.Meta maps --- ddtrace/tracer/span.go | 2 +- ddtrace/tracer/spantags.go | 43 ++++++++++++++++++++++++++ ddtrace/tracer/spantags_test.go | 55 +++++++++++++++++++-------------- ddtrace/tracer/tracer.go | 2 ++ 4 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 ddtrace/tracer/spantags.go diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 4236521435..5db953655f 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -387,7 +387,7 @@ func takeStacktrace(n, skip uint) string { // setMeta sets a string tag. This method is not safe for concurrent use. func (s *span) setMeta(key, v string) { if s.Meta == nil { - s.Meta = make(map[string]string, 5) + s.Meta = mmp.Get() } delete(s.Metrics, key) switch key { diff --git a/ddtrace/tracer/spantags.go b/ddtrace/tracer/spantags.go new file mode 100644 index 0000000000..bd7def0d5b --- /dev/null +++ b/ddtrace/tracer/spantags.go @@ -0,0 +1,43 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024 Datadog, Inc. + +package tracer + +import "sync" + +type metaMapPool struct { + *sync.Pool +} + +var mmp = &metaMapPool{ + Pool: &sync.Pool{ + New: func() any { + return defaultMetaMap() + }, + }, +} + +func (p *metaMapPool) Get() map[string]string { + return p.Pool.Get().(map[string]string) +} + +func (p *metaMapPool) Put(m map[string]string) { + if m == nil { + return + } + clear(m) + p.Pool.Put(m) +} + +func releaseSpanMaps(spans []*span) { + for i := range spans { + releaseSpanMap(spans[i]) + } +} + +func releaseSpanMap(s *span) { + mmp.Put(s.Meta) + s.Meta = nil +} diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go index 9720b56357..ec7251ad2b 100644 --- a/ddtrace/tracer/spantags_test.go +++ b/ddtrace/tracer/spantags_test.go @@ -9,8 +9,6 @@ import ( "math/rand" "strconv" "testing" - - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" ) func BenchmarkSpanLifecycle(b *testing.B) { @@ -23,44 +21,55 @@ func BenchmarkSpanLifecycle(b *testing.B) { []float64{0.01, 0.09, 0.4, 0.25, 0.15, 0.05, 0.04, 0.01}, []float64{8.75, 14.59, 22.8, 31.2, 39.1, 43.5, 54.3, 70.0}, ) - b.Run("baseline", func(b *testing.B) { + b.Run("baseline/set tag", func(b *testing.B) { + span := &span{} + span.setMeta("key", "value") + if span.Meta == nil { + b.Fatal("expected span.Meta to be non-nil") + } b.ResetTimer() for i := 0; i < b.N; i++ { - span := StartSpan("benchmark") - span.Finish() + span.setMeta("key", "value") } }) - b.Run("with unknown tags", func(b *testing.B) { - // precompute the tags - tags := make([]string, 70) - for i := 0; i < len(tags); i++ { - tags[i] = strconv.Itoa(i) + b.Run("baseline/acquire, set tag, and release", func(b *testing.B) { + // preallocate the spans + spans := make([]*span, b.N) + for i := 0; i < b.N; i++ { + spans[i] = &span{} } b.ResetTimer() + b.ReportMetric(1.0, "tags/op") for i := 0; i < b.N; i++ { - span := StartSpan("benchmark") - nTags := int(distribution.generate(r)) - for j := 0; j < nTags; j++ { - span.SetTag(tags[j], "tag") - } - span.Finish() + spans[i].setMeta("key", "value") + releaseSpanMap(spans[i]) } }) - b.Run("with known tags", func(b *testing.B) { + b.Run("with tags", func(b *testing.B) { // precompute the tags tags := make([]string, 70) for i := 0; i < len(tags); i++ { tags[i] = strconv.Itoa(i) } - tags[0] = ext.Environment + // preallocate the spans and number of tags + spans := make([]struct { + span *span + n int + }, b.N) + totalSpanTags := 0 + for i := 0; i < b.N; i++ { + spans[i].span = &span{} + spans[i].n = int(distribution.generate(r)) + totalSpanTags += spans[i].n + } b.ResetTimer() + b.ReportMetric(float64(totalSpanTags/b.N), "tags/op") for i := 0; i < b.N; i++ { - span := StartSpan("benchmark") - nTags := int(distribution.generate(r)) - for j := 0; j < nTags; j++ { - span.SetTag(tags[j], "tag") + s := spans[i].span + for j := 0; j < spans[i].n; j++ { + s.setMeta(tags[j], "value") } - span.Finish() + releaseSpanMap(s) } }) } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index af794c7857..3bcb762cfb 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -369,6 +369,7 @@ func (t *tracer) worker(tick <-chan time.Time) { t.sampleChunk(trace) if len(trace.spans) != 0 { t.traceWriter.add(trace.spans) + releaseSpanMaps(trace.spans) } case <-tick: t.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:scheduled"}, 1) @@ -394,6 +395,7 @@ func (t *tracer) worker(tick <-chan time.Time) { t.sampleChunk(trace) if len(trace.spans) != 0 { t.traceWriter.add(trace.spans) + releaseSpanMaps(trace.spans) } default: break loop From 30d6cd855669ac546f845fdb3ee0fa6989c67158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 7 Aug 2024 15:37:21 +0200 Subject: [PATCH 13/14] ddtrace/tracer: remove meta maps pool Releasing the map after encoding the span for submission doesn't play well with our current codebase --- ddtrace/tracer/span.go | 2 +- ddtrace/tracer/spantags.go | 37 --------------------------------- ddtrace/tracer/spantags_test.go | 9 +++----- ddtrace/tracer/tracer.go | 2 -- 4 files changed, 4 insertions(+), 46 deletions(-) diff --git a/ddtrace/tracer/span.go b/ddtrace/tracer/span.go index 5db953655f..b1f93f3e4e 100644 --- a/ddtrace/tracer/span.go +++ b/ddtrace/tracer/span.go @@ -387,7 +387,7 @@ func takeStacktrace(n, skip uint) string { // setMeta sets a string tag. This method is not safe for concurrent use. func (s *span) setMeta(key, v string) { if s.Meta == nil { - s.Meta = mmp.Get() + s.Meta = defaultMetaMap() } delete(s.Metrics, key) switch key { diff --git a/ddtrace/tracer/spantags.go b/ddtrace/tracer/spantags.go index bd7def0d5b..33a3c0f8b6 100644 --- a/ddtrace/tracer/spantags.go +++ b/ddtrace/tracer/spantags.go @@ -4,40 +4,3 @@ // Copyright 2024 Datadog, Inc. package tracer - -import "sync" - -type metaMapPool struct { - *sync.Pool -} - -var mmp = &metaMapPool{ - Pool: &sync.Pool{ - New: func() any { - return defaultMetaMap() - }, - }, -} - -func (p *metaMapPool) Get() map[string]string { - return p.Pool.Get().(map[string]string) -} - -func (p *metaMapPool) Put(m map[string]string) { - if m == nil { - return - } - clear(m) - p.Pool.Put(m) -} - -func releaseSpanMaps(spans []*span) { - for i := range spans { - releaseSpanMap(spans[i]) - } -} - -func releaseSpanMap(s *span) { - mmp.Put(s.Meta) - s.Meta = nil -} diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go index ec7251ad2b..3a01d3ca10 100644 --- a/ddtrace/tracer/spantags_test.go +++ b/ddtrace/tracer/spantags_test.go @@ -22,8 +22,7 @@ func BenchmarkSpanLifecycle(b *testing.B) { []float64{8.75, 14.59, 22.8, 31.2, 39.1, 43.5, 54.3, 70.0}, ) b.Run("baseline/set tag", func(b *testing.B) { - span := &span{} - span.setMeta("key", "value") + span := newBasicSpan("benchmark") if span.Meta == nil { b.Fatal("expected span.Meta to be non-nil") } @@ -36,13 +35,12 @@ func BenchmarkSpanLifecycle(b *testing.B) { // preallocate the spans spans := make([]*span, b.N) for i := 0; i < b.N; i++ { - spans[i] = &span{} + spans[i] = newBasicSpan("benchmark") } b.ResetTimer() b.ReportMetric(1.0, "tags/op") for i := 0; i < b.N; i++ { spans[i].setMeta("key", "value") - releaseSpanMap(spans[i]) } }) b.Run("with tags", func(b *testing.B) { @@ -58,7 +56,7 @@ func BenchmarkSpanLifecycle(b *testing.B) { }, b.N) totalSpanTags := 0 for i := 0; i < b.N; i++ { - spans[i].span = &span{} + spans[i].span = newBasicSpan("benchmark") spans[i].n = int(distribution.generate(r)) totalSpanTags += spans[i].n } @@ -69,7 +67,6 @@ func BenchmarkSpanLifecycle(b *testing.B) { for j := 0; j < spans[i].n; j++ { s.setMeta(tags[j], "value") } - releaseSpanMap(s) } }) } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 3bcb762cfb..af794c7857 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -369,7 +369,6 @@ func (t *tracer) worker(tick <-chan time.Time) { t.sampleChunk(trace) if len(trace.spans) != 0 { t.traceWriter.add(trace.spans) - releaseSpanMaps(trace.spans) } case <-tick: t.statsd.Incr("datadog.tracer.flush_triggered", []string{"reason:scheduled"}, 1) @@ -395,7 +394,6 @@ func (t *tracer) worker(tick <-chan time.Time) { t.sampleChunk(trace) if len(trace.spans) != 0 { t.traceWriter.add(trace.spans) - releaseSpanMaps(trace.spans) } default: break loop From 372234c84ab69922c3ed4e30ea75fb18999ebbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Casta=C3=B1=C3=A9?= Date: Wed, 7 Aug 2024 17:25:58 +0200 Subject: [PATCH 14/14] ddtrace/tracer: complete BenchmarkSpanSetMeta proving that allocating the Meta map at the right size impacts performance --- ddtrace/tracer/spantags_test.go | 74 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/ddtrace/tracer/spantags_test.go b/ddtrace/tracer/spantags_test.go index 3a01d3ca10..fe7fcd875d 100644 --- a/ddtrace/tracer/spantags_test.go +++ b/ddtrace/tracer/spantags_test.go @@ -6,12 +6,13 @@ package tracer import ( + "fmt" "math/rand" "strconv" "testing" ) -func BenchmarkSpanLifecycle(b *testing.B) { +func BenchmarkSpanSetMeta(b *testing.B) { r := rand.New(rand.NewSource(0)) distribution := newDistributionRand( b, @@ -21,54 +22,49 @@ func BenchmarkSpanLifecycle(b *testing.B) { []float64{0.01, 0.09, 0.4, 0.25, 0.15, 0.05, 0.04, 0.01}, []float64{8.75, 14.59, 22.8, 31.2, 39.1, 43.5, 54.3, 70.0}, ) - b.Run("baseline/set tag", func(b *testing.B) { + b.Run("baseline", func(b *testing.B) { span := newBasicSpan("benchmark") if span.Meta == nil { b.Fatal("expected span.Meta to be non-nil") } b.ResetTimer() - for i := 0; i < b.N; i++ { - span.setMeta("key", "value") - } - }) - b.Run("baseline/acquire, set tag, and release", func(b *testing.B) { - // preallocate the spans - spans := make([]*span, b.N) - for i := 0; i < b.N; i++ { - spans[i] = newBasicSpan("benchmark") - } - b.ResetTimer() b.ReportMetric(1.0, "tags/op") for i := 0; i < b.N; i++ { - spans[i].setMeta("key", "value") + span.setMeta("key", "value") } }) - b.Run("with tags", func(b *testing.B) { - // precompute the tags - tags := make([]string, 70) - for i := 0; i < len(tags); i++ { - tags[i] = strconv.Itoa(i) - } - // preallocate the spans and number of tags - spans := make([]struct { - span *span - n int - }, b.N) - totalSpanTags := 0 - for i := 0; i < b.N; i++ { - spans[i].span = newBasicSpan("benchmark") - spans[i].n = int(distribution.generate(r)) - totalSpanTags += spans[i].n - } - b.ResetTimer() - b.ReportMetric(float64(totalSpanTags/b.N), "tags/op") - for i := 0; i < b.N; i++ { - s := spans[i].span - for j := 0; j < spans[i].n; j++ { - s.setMeta(tags[j], "value") + for v := range distribution.values { + metaSize := int(distribution.values[v]) + name := fmt.Sprintf("random number of tags (meta size=%d)", metaSize) + b.Run(name, func(b *testing.B) { + // precompute the tags + tags := make([]string, 70) + for i := 0; i < len(tags); i++ { + tags[i] = strconv.Itoa(i) } - } - }) + // preallocate the spans and number of tags + spans := make([]struct { + span *span + n int + }, b.N) + totalSpanTags := 0 + for i := 0; i < b.N; i++ { + spans[i].span = &span{ + Meta: make(map[string]string, metaSize), + } + spans[i].n = int(distribution.generate(r)) + totalSpanTags += spans[i].n + } + b.ResetTimer() + b.ReportMetric(float64(totalSpanTags/b.N), "tags/op") + for i := 0; i < b.N; i++ { + s, nTags := spans[i].span, spans[i].n + for j := 0; j < nTags; j++ { + s.setMeta(tags[j], "value") + } + } + }) + } } // distributionRand is a helper for generating random numbers following