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

feat: Improve speed and reduce allocations when adding fields #402

Closed
wants to merge 10 commits into from
52 changes: 50 additions & 2 deletions beeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/honeycombio/libhoney-go/transmission"
Expand Down Expand Up @@ -100,6 +101,11 @@ type Config struct {
PprofTagging bool
}

var (
cachedFieldNames = map[string]string{}
cachedFieldNamesLock = &sync.RWMutex{}
)

func IsClassicKey(config Config) bool {
// classic key has 32 characters
return len(config.WriteKey) == 32
Expand Down Expand Up @@ -280,7 +286,7 @@ 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
namespacedKey := getPrefixedFieldName(key)
if valErr, ok := val.(error); ok {
// treat errors specially because it's a pain to have to
// remember to stringify them
Expand All @@ -300,7 +306,7 @@ func AddField(ctx context.Context, key string, val interface{}) {
// eg user IDs, globally relevant feature flags, errors, etc. Fields added here
// are prefixed with `app.`
func AddFieldToTrace(ctx context.Context, key string, val interface{}) {
namespacedKey := "app." + key // Avoid excess parsing/allocation work
namespacedKey := getPrefixedFieldName(key)
tr := trace.GetTraceFromContext(ctx)
if tr != nil {
tr.AddField(namespacedKey, val)
Expand Down Expand Up @@ -354,3 +360,45 @@ func readResponses(responses chan transmission.Response) {
}
}
}

// getPrefixedFieldName returns the field name with the "app." if it's not already there.
// This uses a cache to avoid prefixing the same field name over and over again to save allocations.
// The cache will be reset if it grows too large to avoid unbounded memory growth.
func getPrefixedFieldName(key string) string {
const prefix = "app."

// return if the key already has the prefix
if strings.HasPrefix(key, prefix) {
return key
}

// check the cache using a read lock first
cachedFieldNamesLock.RLock()
val, ok := cachedFieldNames[key]
cachedFieldNamesLock.RUnlock()
if ok {
return val
}

// not in the cache, so get a write lock
cachedFieldNamesLock.Lock()
defer cachedFieldNamesLock.Unlock()

// check again in case it was added while we were waiting for the lock
val, ok = cachedFieldNames[key]
if ok {
return val
}

// before we add the key to the cache, reset the cache if it's getting too big.
// this can happen if lots of unique keys are being used and we don't want to
// grow the cache indefinitely
if len(cachedFieldNames) > 1000 {
cachedFieldNames = map[string]string{}
}

// add the prefixed key to the cache and return it
prefixedKey := prefix + key
cachedFieldNames[key] = prefixedKey
return prefixedKey
}
21 changes: 20 additions & 1 deletion beeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package beeline
import (
"context"
"fmt"
"strconv"
"testing"

"github.com/honeycombio/libhoney-go/transmission"
Expand Down Expand Up @@ -101,7 +102,16 @@ func BenchmarkCreateSpan(b *testing.B) {
}
}

func BenchmarkBeelineAddField(b *testing.B) {
func BenchmarkBeelineAddField_PrefixedKey(b *testing.B) {
setupLibhoney(b)

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

func BenchmarkBeelineAddField_ConsistentKey(b *testing.B) {
setupLibhoney(b)

ctx, _ := StartSpan(context.Background(), "parent")
Expand All @@ -110,6 +120,15 @@ func BenchmarkBeelineAddField(b *testing.B) {
}
}

func BenchmarkBeelineAddField_InconsistentKey(b *testing.B) {
setupLibhoney(b)

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

func setupLibhoney(t testing.TB) *transmission.MockSender {
mo := &transmission.MockSender{}
client, err := libhoney.NewClient(
Expand Down