Skip to content

Conversation

@martin-sucha
Copy link
Contributor

Currently the Client allocates a new context every time a counter/gauge/set is updated.

I use a small number of tags in my application multiple times, so I would like to cache the context to avoid an allocation.

This commit implements a new Scope structure that caches the context string and allows the application to update counters without an allocation.

I run the following benchmarks two times with -count=5:

func BenchmarkStatsdCounter(b *testing.B) {
	s, err := statsd.New("localhost:8125", statsd.WithNamespace("my_namespace"),
		statsd.WithClientSideAggregation(), statsd.WithExtendedClientSideAggregation())
	if err != nil {
		b.Fatal(err)
	}
	b.ReportAllocs()
	labels := []string{"label1:value1", "label2:value2"}
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			s.Count("my_metric", 1, labels, 1)
		}
	})
}

func BenchmarkStatsdCounterScope(b *testing.B) {
	s, err := statsd.New("localhost:8125", statsd.WithNamespace("my_namespace"),
		statsd.WithClientSideAggregation(), statsd.WithExtendedClientSideAggregation())
	if err != nil {
		b.Fatal(err)
	}
	b.ReportAllocs()
	labels := []string{"label1:value1", "label2:value2"}
	mc := s.Scope("my_metric", labels)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			mc.Count(1, 1)
		}
	})
}

The benchstat results:

goos: linux
goarch: amd64
pkg: gitlab.skypicker.com/go/pkg/monitoring/metricsv2
cpu: 12th Gen Intel(R) Core(TM) i7-1260P
                      │  bench.txt   │
                      │    sec/op    │
StatsdCounter           56.25n ±  5%
StatsdCounter-2         90.99n ±  5%
StatsdCounter-4         101.2n ± 40%
StatsdCounter-8         139.9n ±  4%
StatsdCounter-16        123.6n ±  4%
StatsdCounterScope      30.40n ±  5%
StatsdCounterScope-2    107.2n ±  7%
StatsdCounterScope-4    104.5n ±  7%
StatsdCounterScope-8    126.7n ±  5%
StatsdCounterScope-16   96.54n ± 25%
geomean                 90.60n

                      │  bench.txt   │
                      │     B/op     │
StatsdCounter           48.00 ± 0%
StatsdCounter-2         48.00 ± 0%
StatsdCounter-4         48.00 ± 0%
StatsdCounter-8         48.00 ± 0%
StatsdCounter-16        48.00 ± 0%
StatsdCounterScope      0.000 ± 0%
StatsdCounterScope-2    0.000 ± 0%
StatsdCounterScope-4    0.000 ± 0%
StatsdCounterScope-8    0.000 ± 0%
StatsdCounterScope-16   0.000 ± 0%
geomean                            ¹
¹ summaries must be >0 to compute geomean

                      │  bench.txt   │
                      │  allocs/op   │
StatsdCounter           1.000 ± 0%
StatsdCounter-2         1.000 ± 0%
StatsdCounter-4         1.000 ± 0%
StatsdCounter-8         1.000 ± 0%
StatsdCounter-16        1.000 ± 0%
StatsdCounterScope      0.000 ± 0%
StatsdCounterScope-2    0.000 ± 0%
StatsdCounterScope-4    0.000 ± 0%
StatsdCounterScope-8    0.000 ± 0%
StatsdCounterScope-16   0.000 ± 0%
geomean                            ¹
¹ summaries must be >0 to compute geomean

Currently the Client allocates a new context every time a
counter/gauge/set is updated.

I use a small number of tags in my application multiple times,
so I would like to cache the context to avoid an allocation.

This commit implements a new Scope structure that caches the
context string and allows the application to update counters
without an allocation.

I run the following benchmarks two times with -count=5:

```go
func BenchmarkStatsdCounter(b *testing.B) {
	s, err := statsd.New("localhost:8125", statsd.WithNamespace("my_namespace"),
		statsd.WithClientSideAggregation(), statsd.WithExtendedClientSideAggregation())
	if err != nil {
		b.Fatal(err)
	}
	b.ReportAllocs()
	labels := []string{"label1:value1", "label2:value2"}
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			s.Count("my_metric", 1, labels, 1)
		}
	})
}

func BenchmarkStatsdCounterScope(b *testing.B) {
	s, err := statsd.New("localhost:8125", statsd.WithNamespace("my_namespace"),
		statsd.WithClientSideAggregation(), statsd.WithExtendedClientSideAggregation())
	if err != nil {
		b.Fatal(err)
	}
	b.ReportAllocs()
	labels := []string{"label1:value1", "label2:value2"}
	mc := s.Scope("my_metric", labels)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			mc.Count(1, 1)
		}
	})
}
```

The benchstat results:

```
goos: linux
goarch: amd64
pkg: gitlab.skypicker.com/go/pkg/monitoring/metricsv2
cpu: 12th Gen Intel(R) Core(TM) i7-1260P
                      │  bench.txt   │
                      │    sec/op    │
StatsdCounter           56.25n ±  5%
StatsdCounter-2         90.99n ±  5%
StatsdCounter-4         101.2n ± 40%
StatsdCounter-8         139.9n ±  4%
StatsdCounter-16        123.6n ±  4%
StatsdCounterScope      30.40n ±  5%
StatsdCounterScope-2    107.2n ±  7%
StatsdCounterScope-4    104.5n ±  7%
StatsdCounterScope-8    126.7n ±  5%
StatsdCounterScope-16   96.54n ± 25%
geomean                 90.60n

                      │  bench.txt   │
                      │     B/op     │
StatsdCounter           48.00 ± 0%
StatsdCounter-2         48.00 ± 0%
StatsdCounter-4         48.00 ± 0%
StatsdCounter-8         48.00 ± 0%
StatsdCounter-16        48.00 ± 0%
StatsdCounterScope      0.000 ± 0%
StatsdCounterScope-2    0.000 ± 0%
StatsdCounterScope-4    0.000 ± 0%
StatsdCounterScope-8    0.000 ± 0%
StatsdCounterScope-16   0.000 ± 0%
geomean                            ¹
¹ summaries must be >0 to compute geomean

                      │  bench.txt   │
                      │  allocs/op   │
StatsdCounter           1.000 ± 0%
StatsdCounter-2         1.000 ± 0%
StatsdCounter-4         1.000 ± 0%
StatsdCounter-8         1.000 ± 0%
StatsdCounter-16        1.000 ± 0%
StatsdCounterScope      0.000 ± 0%
StatsdCounterScope-2    0.000 ± 0%
StatsdCounterScope-4    0.000 ± 0%
StatsdCounterScope-8    0.000 ± 0%
StatsdCounterScope-16   0.000 ± 0%
geomean                            ¹
¹ summaries must be >0 to compute geomean
```
@martin-sucha martin-sucha requested a review from a team as a code owner May 16, 2025 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant