|
6 | 6 | package server
|
7 | 7 |
|
8 | 8 | import (
|
| 9 | + "runtime" |
| 10 | + "unsafe" |
| 11 | + |
9 | 12 | "github.com/DataDog/datadog-agent/pkg/config/utils"
|
10 | 13 | "github.com/DataDog/datadog-agent/pkg/telemetry"
|
11 |
| - "github.com/DataDog/datadog-agent/pkg/util/log" |
12 | 14 | )
|
13 | 15 |
|
14 | 16 | var (
|
15 | 17 | // There are multiple instances of the interner, one per worker. Counters are normally fine,
|
16 | 18 | // gauges require special care to make sense. We don't need to clean up when an instance is
|
17 | 19 | // dropped, because it only happens on agent shutdown.
|
18 |
| - tlmSIResets = telemetry.NewSimpleCounter("dogstatsd", "string_interner_resets", |
19 |
| - "Amount of resets of the string interner used in dogstatsd") |
20 |
| - tlmSIRSize = telemetry.NewSimpleGauge("dogstatsd", "string_interner_entries", |
21 |
| - "Number of entries in the string interner") |
22 |
| - tlmSIRBytes = telemetry.NewSimpleGauge("dogstatsd", "string_interner_bytes", |
23 |
| - "Number of bytes stored in the string interner") |
24 | 20 | tlmSIRHits = telemetry.NewSimpleCounter("dogstatsd", "string_interner_hits",
|
25 | 21 | "Number of times string interner returned an existing string")
|
26 | 22 | tlmSIRMiss = telemetry.NewSimpleCounter("dogstatsd", "string_interner_miss",
|
27 | 23 | "Number of times string interner created a new string object")
|
28 | 24 | tlmSIRNew = telemetry.NewSimpleCounter("dogstatsd", "string_interner_new",
|
29 | 25 | "Number of times string interner was created")
|
30 |
| - tlmSIRStrBytes = telemetry.NewSimpleHistogram("dogstatsd", "string_interner_str_bytes", |
31 |
| - "Number of times string with specific length were added", |
32 |
| - []float64{1, 2, 4, 8, 16, 32, 64, 128}) |
33 | 26 | )
|
34 | 27 |
|
35 |
| -// stringInterner is a string cache providing a longer life for strings, |
36 |
| -// helping to avoid GC runs because they're re-used many times instead of |
37 |
| -// created every time. |
| 28 | +// A StringValue pointer is the handle to the underlying string value. |
| 29 | +// See Get how Value pointers may be used. |
| 30 | +type StringValue struct { |
| 31 | + _ [0]func() // prevent people from accidentally using value type as comparable |
| 32 | + cmpVal string |
| 33 | + resurrected bool |
| 34 | +} |
| 35 | + |
| 36 | +// Get the underlying string value |
| 37 | +func (v *StringValue) Get() string { |
| 38 | + return v.cmpVal |
| 39 | +} |
| 40 | + |
| 41 | +// stringInterner interns strings while allowing them to be cleaned up by the GC. |
| 42 | +// It can handle both string and []byte types without allocation. |
38 | 43 | type stringInterner struct {
|
39 |
| - strings map[string]string |
40 |
| - maxSize int |
41 |
| - curBytes int |
42 | 44 | tlmEnabled bool
|
| 45 | + valMap map[string]uintptr |
43 | 46 | }
|
44 | 47 |
|
45 |
| -func newStringInterner(maxSize int) *stringInterner { |
| 48 | +// newStringInterner creates a new StringInterner |
| 49 | +func newStringInterner() *stringInterner { |
46 | 50 | i := &stringInterner{
|
47 |
| - strings: make(map[string]string), |
48 |
| - maxSize: maxSize, |
| 51 | + valMap: make(map[string]uintptr), |
49 | 52 | tlmEnabled: utils.IsTelemetryEnabled(),
|
50 | 53 | }
|
| 54 | + |
51 | 55 | if i.tlmEnabled {
|
52 | 56 | tlmSIRNew.Inc()
|
53 | 57 | }
|
54 | 58 | return i
|
55 | 59 | }
|
56 | 60 |
|
57 |
| -// LoadOrStore always returns the string from the cache, adding it into the |
58 |
| -// cache if needed. |
59 |
| -// If we need to store a new entry and the cache is at its maximum capacity, |
60 |
| -// it is reset. |
61 |
| -func (i *stringInterner) LoadOrStore(key []byte) string { |
62 |
| - // here is the string interner trick: the map lookup using |
63 |
| - // string(key) doesn't actually allocate a string, but is |
64 |
| - // returning the string value -> no new heap allocation |
65 |
| - // for this string. |
66 |
| - // See https://github.com/golang/go/commit/f5f5a8b6209f84961687d993b93ea0d397f5d5bf |
67 |
| - if s, found := i.strings[string(key)]; found { |
68 |
| - if i.tlmEnabled { |
| 61 | +// Get returns a pointer representing the []byte k |
| 62 | +// |
| 63 | +// The returned pointer will be the same for Get(v) and Get(v2) |
| 64 | +// if and only if v == v2. The returned pointer will also be the same |
| 65 | +// for a string with same contents as the byte slice. |
| 66 | +// |
| 67 | +//go:nocheckptr |
| 68 | +func (s *stringInterner) LoadOrStore(k []byte) *StringValue { |
| 69 | + var v *StringValue |
| 70 | + // the compiler will optimize the following map lookup to not alloc a string |
| 71 | + if addr, ok := s.valMap[string(k)]; ok { |
| 72 | + //goland:noinspection GoVetUnsafePointer |
| 73 | + v = (*StringValue)((unsafe.Pointer)(addr)) |
| 74 | + v.resurrected = true |
| 75 | + if s.tlmEnabled { |
69 | 76 | tlmSIRHits.Inc()
|
70 | 77 | }
|
71 |
| - return s |
| 78 | + return v |
72 | 79 | }
|
73 |
| - if len(i.strings) >= i.maxSize { |
74 |
| - if i.tlmEnabled { |
75 |
| - tlmSIResets.Inc() |
76 |
| - tlmSIRBytes.Sub(float64(i.curBytes)) |
77 |
| - tlmSIRSize.Sub(float64(len(i.strings))) |
78 |
| - i.curBytes = 0 |
79 |
| - } |
80 | 80 |
|
81 |
| - i.strings = make(map[string]string) |
82 |
| - log.Debug("clearing the string interner cache") |
83 |
| - |
84 |
| - } |
85 |
| - |
86 |
| - s := string(key) |
87 |
| - i.strings[s] = s |
| 81 | + v = &StringValue{cmpVal: string(k)} |
| 82 | + runtime.SetFinalizer(v, s.finalize) |
| 83 | + s.valMap[string(k)] = uintptr(unsafe.Pointer(v)) |
| 84 | + tlmSIRMiss.Inc() |
| 85 | + return v |
| 86 | +} |
88 | 87 |
|
89 |
| - if i.tlmEnabled { |
90 |
| - tlmSIRMiss.Inc() |
91 |
| - tlmSIRSize.Inc() |
92 |
| - tlmSIRBytes.Add(float64(len(s))) |
93 |
| - tlmSIRStrBytes.Observe(float64(len(s))) |
94 |
| - i.curBytes += len(s) |
| 88 | +func (s *stringInterner) finalize(v *StringValue) { |
| 89 | + if v.resurrected { |
| 90 | + // We lost the race. Somebody resurrected it while we |
| 91 | + // were about to finalize it. Try again next round. |
| 92 | + v.resurrected = false |
| 93 | + runtime.SetFinalizer(v, s.finalize) |
| 94 | + return |
95 | 95 | }
|
96 |
| - |
97 |
| - return s |
| 96 | + delete(s.valMap, v.Get()) |
98 | 97 | }
|
0 commit comments