Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ddtrace/tracer: optimize span tags storage in the hot path #2799

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6c5c797
ddtrace/tracer: dereference pointers to supported types in span.SetTag
darccio Jul 23, 2024
46d44dc
ddtrace/tracer: basic implementation for pooled linked span tag list
darccio Jul 25, 2024
cebf289
ddtrace/tracer: rename tag.go to spantags.go
darccio Jul 26, 2024
af4d586
ddtrace/tracer: embracing unsafe.Pointer and generic types
darccio Jul 26, 2024
2f610fd
ddtrace/tracer: add comments to clarify spanTags implementation
darccio Jul 26, 2024
e0f5da3
ddtrace/tracer: add benchmark for concurrently setting tags in a span
darccio Jul 31, 2024
399d412
ddtrace/tracer: preallocate pool in BenchmarkConcurrentSpanSetTag
darccio Jul 31, 2024
5860d9d
ddtrace/tracer: revert to map-based implementation
darccio Aug 6, 2024
3fd141c
Merge remote-tracking branch 'origin' into dario.castane/exp-optimize…
darccio Aug 6, 2024
51c0489
ddtrace/tracer: implement distribution-based random number generator …
darccio Aug 6, 2024
dc77c97
ddtrace/tracer: preallocate meta map
darccio Aug 6, 2024
ed88dac
ddtrace/tracer: use a shared function to create span.Meta map in unit…
darccio Aug 7, 2024
8595e2c
ddtrace/tracer: introduce pooled span.Meta maps
darccio Aug 7, 2024
d091651
Merge remote-tracking branch 'origin' into dario.castane/exp-optimize…
darccio Aug 7, 2024
30d6cd8
ddtrace/tracer: remove meta maps pool
darccio Aug 7, 2024
372234c
ddtrace/tracer: complete BenchmarkSpanSetMeta proving that allocating…
darccio Aug 7, 2024
51cf835
Merge branch 'main' into dario.castane/exp-optimize-settags
darccio Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ddtrace/tracer/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = defaultMetaMap()
}
delete(s.Metrics, key)
switch key {
Expand Down Expand Up @@ -766,3 +766,7 @@ const (
keyUserScope = "usr.scope"
keyUserSessionID = "usr.session_id"
)

func defaultMetaMap() map[string]string {
return make(map[string]string, 5)
}
41 changes: 39 additions & 2 deletions ddtrace/tracer/span_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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,
Expand Down Expand Up @@ -1012,7 +1012,7 @@ func BenchmarkSetTagString(b *testing.B) {

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")
}
}
Expand All @@ -1033,6 +1033,7 @@ func BenchmarkSetTagStringer(b *testing.B) {
span := newBasicSpan("bench.span")
keys := strings.Split("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "")
value := &stringer{}

b.ResetTimer()
for i := 0; i < b.N; i++ {
k := keys[i%len(keys)]
Expand Down Expand Up @@ -1092,3 +1093,39 @@ 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()
}
}

func BenchmarkConcurrentSpanSetTag(b *testing.B) {
span := newBasicSpan("root")
defer span.Finish()

wg := sync.WaitGroup{}
wg.Add(b.N)

// 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()
}
6 changes: 6 additions & 0 deletions ddtrace/tracer/spantags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 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
103 changes: 103 additions & 0 deletions ddtrace/tracer/spantags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 (
"fmt"
"math/rand"
"strconv"
"testing"
)

func BenchmarkSpanSetMeta(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) {
span := newBasicSpan("benchmark")
if span.Meta == nil {
b.Fatal("expected span.Meta to be non-nil")
}
b.ResetTimer()
b.ReportMetric(1.0, "tags/op")
for i := 0; i < b.N; i++ {
span.setMeta("key", "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
// 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]
}
Loading