Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion statsd/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ func getContext(name string, tags []string, cardinality Cardinality) string {
return c
}

// stringBuilderPool pools strings.Builder objects to reduce allocations
var stringBuilderPool = sync.Pool{
New: func() interface{} { return &strings.Builder{} },
}

// getContextAndTags returns the context and tags for a metric name, tags, and cardinality.
//
// See getContext for usage for context
Expand All @@ -204,7 +209,12 @@ func getContextAndTags(name string, tags []string, cardinality Cardinality) (str
cardStringLen = len(cardString) + len(cardSeparatorSymbol)
}

var sb strings.Builder
sb := stringBuilderPool.Get().(*strings.Builder)
defer func() {
sb.Reset()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a concern about correctness of this approach.

AFAIR sb.String() returns a string backed directly by the builder's internal buffer, so it does not copy. The defer here resets and returns the builder to the pool before the caller ever uses the returned strings, meaning the next goroutine to get the builder will overwrite the buffer that the returned string values still point into. This is silent heap corruption under concurrent use and might not be caught by the race detector.

stringBuilderPool.Put(sb)
}()

sb.Grow(n)
sb.WriteString(name)
sb.WriteString(nameSeparatorSymbol)
Expand Down
44 changes: 44 additions & 0 deletions statsd/aggregator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,50 @@ func BenchmarkGetContextNoTags(b *testing.B) {
b.ReportAllocs()
}

func BenchmarkGetContextAndTags(b *testing.B) {
name := "test.metric"
tags := []string{"tag:tag", "foo:bar", "env:prod", "version:1.0"}
for i := 0; i < b.N; i++ {
getContextAndTags(name, tags, CardinalityLow)
}
b.ReportAllocs()
}

// getContextAndTagsOld is an older implementation without pooling for benchmarking comparison.
// Probably should delete this and the related benchmark if satisfied with results.
func getContextAndTagsOld(name string, tags []string) (string, string) {
if len(tags) == 0 {
return name, ""
}
n := len(name) + len(nameSeparatorSymbol) + len(tagSeparatorSymbol)*(len(tags)-1)
for _, s := range tags {
n += len(s)
}

var sb strings.Builder
sb.Grow(n)
sb.WriteString(name)
sb.WriteString(nameSeparatorSymbol)
sb.WriteString(tags[0])
for _, s := range tags[1:] {
sb.WriteString(tagSeparatorSymbol)
sb.WriteString(s)
}

s := sb.String()

return s, s[len(name)+len(nameSeparatorSymbol):]
}

func BenchmarkGetContextAndTagsOld(b *testing.B) {
name := "test.metric"
tags := []string{"tag:tag", "foo:bar", "env:prod", "version:1.0"}
for i := 0; i < b.N; i++ {
getContextAndTagsOld(name, tags)
}
b.ReportAllocs()
}

func TestAggregatorCardinalitySeparation(t *testing.T) {
a := newAggregator(nil, 0)
tags := []string{"env:prod", "service:api"}
Expand Down