Skip to content

Commit

Permalink
fix: reduce memalloc if field names pre-prefixed
Browse files Browse the repository at this point in the history
There is a remarkable amount of memory allocation occurring under load
to perform the field name "app." prefixing by the Beeline with "app.".
This change skips the memory allocations needed for string concat and
usage if the "app." prefix is already present on the field name
provided.
  • Loading branch information
robbkidd committed Nov 28, 2023
1 parent e4fea20 commit bd740f4
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 10 deletions.
30 changes: 22 additions & 8 deletions beeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,21 +272,23 @@ func Close() {
// a Handler, feel free to call AddField freely within your code. Pass it the
// context from the request (`r.Context()`) and the key and value you wish to
// add.This function is good for span-level data, eg timers or the arguments to
// a specific function call, etc. Fields added here are prefixed with `app.`
// a specific function call, etc.
//
// Field keys added will be prefixed with `app.` if the key does not already have
// that prefix.
//
// Errors are treated as a special case for convenience: if `val` is of type
// `error` then the key is set to the error's message in the span.
func AddField(ctx context.Context, key string, val interface{}) {
span := trace.GetSpanFromContext(ctx)
if span != nil {
if val != nil {
namespacedKey := "app." + key // Avoid excess parsing/allocation work
if valErr, ok := val.(error); ok {
// treat errors specially because it's a pain to have to
// remember to stringify them
span.AddField(namespacedKey, valErr.Error())
span.AddField(getNamespacedKey(key), valErr.Error())
} else {
span.AddField(namespacedKey, val)
span.AddField(getNamespacedKey(key), val)
}
}
}
Expand All @@ -297,14 +299,26 @@ func AddField(ctx context.Context, key string, val interface{}) {
// Additionally, these fields are packaged up and passed along to downstream
// processes if they are also using a beeline. This function is good for adding
// context that is better scoped to the request than this specific unit of work,
// eg user IDs, globally relevant feature flags, errors, etc. Fields added here
// are prefixed with `app.`
// eg user IDs, globally relevant feature flags, errors, etc.
//
// Field keys added will be prefixed with `app.` if the key does not already have
// that prefix.
func AddFieldToTrace(ctx context.Context, key string, val interface{}) {
namespacedKey := "app." + key // Avoid excess parsing/allocation work
tr := trace.GetTraceFromContext(ctx)
if tr != nil {
tr.AddField(namespacedKey, val)
tr.AddField(getNamespacedKey(key), val)
}
}

// getNamespacedKey ensures a key name is prefixed with "app." if the prefix
// isn't already present. If the key is already namespaced, this reduces the
// number of memory allocations needed to add a field to a span.
func getNamespacedKey(key string) string {
const prefix = "app."
if strings.HasPrefix(key, prefix) {
return key
}
return prefix + key
}

// StartSpan lets you start a new span as a child of an already instrumented
Expand Down
43 changes: 41 additions & 2 deletions beeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"testing"

"github.com/honeycombio/beeline-go/trace"
"github.com/honeycombio/libhoney-go/transmission"

libhoney "github.com/honeycombio/libhoney-go"
Expand Down Expand Up @@ -105,8 +106,46 @@ func BenchmarkBeelineAddField(b *testing.B) {
setupLibhoney(b)

ctx, _ := StartSpan(context.Background(), "parent")
for n := 0; n < b.N; n++ {
AddField(ctx, "foo", 1)

b.Run("oldAddField/whatever", func(b *testing.B) {
for n := 0; n < b.N; n++ {
oldAddField(ctx, "foo", 1)
}
})
b.Run("AddField/no-prefix", func(b *testing.B) {
for n := 0; n < b.N; n++ {
AddField(ctx, "foo", 1)
}
})
b.Run("AddField/half-prefixed", func(b *testing.B) {
for n := 0; n < b.N; n++ {
if n%2 == 0 {
AddField(ctx, "app.foo", 1)
} else {
AddField(ctx, "foo", 1)
}
}
})
b.Run("AddField/all-prefixed", func(b *testing.B) {
for n := 0; n < b.N; n++ {
AddField(ctx, "app.foo", 1)
}
})
}

func oldAddField(ctx context.Context, key string, val interface{}) {
span := trace.GetSpanFromContext(ctx)
if span != nil {
if val != nil {
namespacedKey := "app." + key
if valErr, ok := val.(error); ok {
// treat errors specially because it's a pain to have to
// remember to stringify them
span.AddField(namespacedKey, valErr.Error())
} else {
span.AddField(namespacedKey, val)
}
}
}
}

Expand Down

0 comments on commit bd740f4

Please sign in to comment.