Skip to content

Commit 071ccfd

Browse files
committed
WIP: internal/traceprof: make endpoint call counting efficient
Switch to a data structure designed for efficient concurrent updates for endpoint call counting. This is a prerequisite for enabling the feature by default. TODO: say more about the choice of library Benchmark results on my M1 Max Macbook Pro: goos: darwin goarch: arm64 pkg: gopkg.in/DataDog/dd-trace-go.v1/internal/traceprof cpu: Apple M1 Max │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ EndpointCounter/enabled=true-10 139.800n ± 11% 5.000n ± 18% -96.42% (p=0.000 n=10) EndpointCounter/enabled=false-10 0.2716n ± 5% 0.2682n ± 1% ~ (p=0.118 n=10) geomean 6.162n 1.158n -81.21% │ before.txt │ after.txt │ │ B/op │ B/op vs base │ EndpointCounter/enabled=true-10 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ EndpointCounter/enabled=false-10 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean │ before.txt │ after.txt │ │ allocs/op │ allocs/op vs base │ EndpointCounter/enabled=true-10 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ EndpointCounter/enabled=false-10 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean
1 parent 739be25 commit 071ccfd

File tree

10 files changed

+84
-18
lines changed

10 files changed

+84
-18
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ require (
8181
github.com/miekg/dns v1.1.55
8282
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
8383
github.com/opentracing/opentracing-go v1.2.0
84+
github.com/puzpuzpuz/xsync/v3 v3.5.0
8485
github.com/redis/go-redis/v9 v9.1.0
8586
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3
8687
github.com/segmentio/kafka-go v0.4.42

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
855855
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
856856
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
857857
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
858+
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
859+
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
858860
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
859861
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
860862
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=

internal/apps/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ require (
3535
github.com/outcaste-io/ristretto v0.2.3 // indirect
3636
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3737
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
38+
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
3839
github.com/ryanuber/go-glob v1.0.0 // indirect
3940
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
4041
github.com/shoenig/go-m1cpu v0.1.6 // indirect

internal/apps/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM
139139
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
140140
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
141141
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
142+
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
143+
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
142144
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
143145
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
144146
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY=

internal/exectracetest/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ require (
4646
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
4747
github.com/pkg/errors v0.9.1 // indirect
4848
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
49+
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
4950
github.com/ryanuber/go-glob v1.0.0 // indirect
5051
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
5152
github.com/shirou/gopsutil/v3 v3.24.4 // indirect

internal/exectracetest/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM
146146
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
147147
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
148148
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
149+
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
150+
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
149151
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
150152
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
151153
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY=

internal/orchestrion/_integration/go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ require (
243243
github.com/pkg/errors v0.9.1 // indirect
244244
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
245245
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
246+
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
246247
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
247248
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 // indirect
248249
github.com/rivo/uniseg v0.4.7 // indirect

internal/orchestrion/_integration/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,8 @@ github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJN
14891489
github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
14901490
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
14911491
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
1492+
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
1493+
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
14921494
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
14931495
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
14941496
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=

internal/traceprof/endpoint_counter.go

+53-18
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package traceprof
88
import (
99
"sync"
1010
"sync/atomic"
11+
12+
"github.com/puzpuzpuz/xsync/v3"
1113
)
1214

1315
// globalEndpointCounter is shared between the profiler and the tracer.
@@ -38,7 +40,11 @@ func GlobalEndpointCounter() *EndpointCounter {
3840
// NewEndpointCounter returns a new NewEndpointCounter that will track hit
3941
// counts for up to limit endpoints. A limit of <= 0 indicates no limit.
4042
func NewEndpointCounter(limit int) *EndpointCounter {
41-
return &EndpointCounter{enabled: 1, limit: limit, counts: map[string]uint64{}}
43+
return &EndpointCounter{
44+
enabled: 1,
45+
limit: limit,
46+
counts: xsync.NewMapOf[string, *xsync.Counter](),
47+
}
4248
}
4349

4450
// EndpointCounter counts hits per endpoint.
@@ -50,7 +56,7 @@ func NewEndpointCounter(limit int) *EndpointCounter {
5056
type EndpointCounter struct {
5157
enabled uint64
5258
mu sync.Mutex
53-
counts map[string]uint64
59+
counts *xsync.MapOf[string, *xsync.Counter]
5460
limit int
5561
}
5662

@@ -69,30 +75,59 @@ func (e *EndpointCounter) Inc(endpoint string) {
6975
return
7076
}
7177

72-
// Acquire lock until func returns
73-
e.mu.Lock()
74-
defer e.mu.Unlock()
75-
76-
// Don't add another endpoint to the map if the limit is reached. See
77-
// globalEndpointCounter comment.
78-
count, ok := e.counts[endpoint]
79-
if !ok && e.limit > 0 && len(e.counts) >= e.limit {
78+
count, ok := e.counts.Load(endpoint)
79+
if !ok {
80+
// If we haven't seen this endpoint yet, add it. Another
81+
// goroutine might be racing to add it, so use
82+
// LoadOrStore: we'll only store if this goroutine
83+
// "wins" the race to add it, and we'll have a small
84+
// wasted allocation if the goroutine "loses" the race.
85+
// In microbenchmarks this seems to be faster than a
86+
// single LoadOrCompute
87+
// TODO: our tests pass whether or not we re-set ok
88+
// here. re-setting seems right because we need to check
89+
// whether we hit the limit _if_ we added
90+
// Can we test more thoroughly?
91+
count, ok = e.counts.LoadOrStore(endpoint, xsync.NewCounter())
92+
}
93+
if !ok && e.limit > 0 && e.counts.Size() > e.limit {
94+
// If we went over the limit when we added the counter,
95+
// delete it.
96+
// TODO: this is racy: another goroutine might also add
97+
// a different endpoint and exceed the limit _after_
98+
// this one, yet we check the size first end delete our
99+
// endpoint _before_ the other goroutine.
100+
// Does it matter in practice?
101+
e.counts.Delete(endpoint)
80102
return
81103
}
82104
// Increment the endpoint count
83-
e.counts[endpoint] = count + 1
105+
count.Inc()
106+
return
84107
}
85108

86109
// GetAndReset returns the hit counts for all endpoints and resets their counts
87110
// back to 0.
88111
func (e *EndpointCounter) GetAndReset() map[string]uint64 {
89-
// Acquire lock until func returns
90-
e.mu.Lock()
91-
defer e.mu.Unlock()
92-
93-
// Return current counts and reset internal map.
94-
counts := e.counts
95-
e.counts = make(map[string]uint64)
112+
// Try to right-size the allocation
113+
counts := make(map[string]uint64, e.counts.Size())
114+
e.counts.Range(func(key string, _ *xsync.Counter) bool {
115+
// TODO: in https://github.com/felixge/countermap/blob/main/xsync_map_counter_map.go,
116+
// Felix reads the input value and then deletes the key.
117+
// A LoadAndDelete ensures we don't miss updates to the
118+
// count for the endpoint: either we get them here or in
119+
// the next cycle. We could also consider not deleting
120+
// the value, but instead reset it, if we aren't at the
121+
// size limit? Would be nice if xsync.Counter had a
122+
// Swap operation for that.
123+
v, ok := e.counts.LoadAndDelete(key)
124+
if ok {
125+
// ok should always be true unless we're calling
126+
// GetAndReset concurrently somewhere else...
127+
counts[key] = uint64(v.Value())
128+
}
129+
return true
130+
})
96131
return counts
97132
}
98133

internal/traceprof/endpoint_counter_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package traceprof
77

88
import (
99
"fmt"
10+
"sync"
1011
"testing"
1112

1213
"github.com/stretchr/testify/require"
@@ -27,6 +28,24 @@ func TestEndpointCounter(t *testing.T) {
2728
require.Equal(t, map[string]uint64{}, ec.GetAndReset())
2829
})
2930

31+
t.Run("fixed limit parallel", func(t *testing.T) {
32+
ec := NewEndpointCounter(10)
33+
var wg sync.WaitGroup
34+
for i := range 10 {
35+
wg.Add(1)
36+
go func() {
37+
defer wg.Done()
38+
for j := range 10 {
39+
// Non-overlapping keys
40+
// TODO: explain why (if?) this is a good thing to check
41+
ec.Inc(fmt.Sprintf("%d", i*10+j))
42+
}
43+
}()
44+
}
45+
wg.Wait()
46+
require.Len(t, ec.GetAndReset(), 10)
47+
})
48+
3049
t.Run("no limit", func(t *testing.T) {
3150
ec := NewEndpointCounter(-1)
3251
for i := 0; i < 100; i++ {

0 commit comments

Comments
 (0)