Skip to content

Commit 8139bd7

Browse files
committed
wip
1 parent 1eecf1b commit 8139bd7

26 files changed

Lines changed: 610 additions & 182 deletions

comp/dogstatsd/server/impl/batch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func (s *shardKeyGenerator) Generate(sample metrics.MetricSample, shards int) ui
8484
// TODO(remy): re-using this tagsBuffer later in the pipeline (by sharing
8585
// it in the sample?) would reduce CPU usage, avoiding to recompute
8686
// the tags hashes while generating the context key.
87+
s.tagsBuffer.AppendInterned(sample.ITags...)
8788
s.tagsBuffer.Append(sample.Tags...)
8889
h := s.keyGenerator.Generate(sample.Name, sample.Host, s.tagsBuffer)
8990
s.tagsBuffer.Reset()

comp/dogstatsd/server/impl/enrich.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
metricsevent "github.com/DataDog/datadog-agent/pkg/metrics/event"
1717
"github.com/DataDog/datadog-agent/pkg/metrics/servicecheck"
1818
taggertypes "github.com/DataDog/datadog-agent/pkg/tagger/types"
19+
"github.com/DataDog/datadog-agent/pkg/tagset"
1920
"github.com/DataDog/datadog-agent/pkg/util/infratags"
2021
utilstrings "github.com/DataDog/datadog-agent/pkg/util/strings"
2122
)
@@ -47,7 +48,7 @@ type enrichConfig struct {
4748
// (origins, cardinality), and the JMX check name extracted from dd.internal.jmx_check_name (empty
4849
// string if absent). The JMX check name is returned so callers can pass it directly to
4950
// AppendJMXDogstatsdInfraTags without re-scanning the tag slice.
50-
func extractTagsMetadata(tags []string, originFromUDS string, processID uint32, localData origindetection.LocalData, externalData origindetection.ExternalData, cardinality string, conf enrichConfig) ([]string, string, taggertypes.OriginInfo, metrics.MetricSource, string) {
51+
func extractTagsMetadata(tags []tagset.InternedTag, originFromUDS string, processID uint32, localData origindetection.LocalData, externalData origindetection.ExternalData, cardinality string, conf enrichConfig) ([]tagset.InternedTag, string, taggertypes.OriginInfo, metrics.MetricSource, string) {
5152
host := conf.defaultHostname
5253
metricSource := GetDefaultMetricSource()
5354
jmxCheckName := ""
@@ -63,7 +64,8 @@ func extractTagsMetadata(tags []string, originFromUDS string, processID uint32,
6364
origin.LocalData.ProcessID = processID
6465

6566
n := 0
66-
for _, tag := range tags {
67+
for _, itag := range tags {
68+
tag := itag.Value()
6769
if strings.HasPrefix(tag, hostTagPrefix) {
6870
host = tag[len(hostTagPrefix):]
6971
continue
@@ -78,7 +80,7 @@ func extractTagsMetadata(tags []string, originFromUDS string, processID uint32,
7880
metricSource = metrics.JMXCheckNameToMetricSource(jmxCheckName)
7981
continue
8082
}
81-
tags[n] = tag
83+
tags[n] = itag
8284
n++
8385
}
8486

@@ -148,10 +150,10 @@ func tsToFloatForSamples(ts time.Time) float64 {
148150
}
149151

150152
func enrichMetricSample(dest []metrics.MetricSample, ddSample dogstatsdMetricSample, origin string, processID uint32, listenerID string, conf enrichConfig, filterList *utilstrings.Matcher) []metrics.MetricSample {
151-
metricName := ddSample.name
153+
metricName := ddSample.name.Value()
152154
tags, hostnameFromTags, extractedOrigin, metricSource, jmxCheckName := extractTagsMetadata(ddSample.tags, origin, processID, ddSample.localData, ddSample.externalData, ddSample.cardinality, conf)
153155
if conf.infraTagger.IsCheckEligible(jmxCheckName) {
154-
tags = conf.infraTagger.AppendTags(tags)
156+
tags = conf.infraTagger.AppendInternedTags(tags)
155157
}
156158

157159
if !isExcluded(metricName, conf.metricPrefix, conf.metricPrefixBlacklist) {
@@ -180,7 +182,7 @@ func enrichMetricSample(dest []metrics.MetricSample, ddSample dogstatsdMetricSam
180182
metrics.MetricSample{
181183
Host: hostnameFromTags,
182184
Name: metricName,
183-
Tags: tags,
185+
ITags: tags,
184186
Mtype: mtype,
185187
Value: ddSample.values[idx],
186188
SampleRate: ddSample.sampleRate,
@@ -199,7 +201,7 @@ func enrichMetricSample(dest []metrics.MetricSample, ddSample dogstatsdMetricSam
199201
return append(dest, metrics.MetricSample{
200202
Host: hostnameFromTags,
201203
Name: metricName,
202-
Tags: tags,
204+
ITags: tags,
203205
Mtype: mtype,
204206
Value: ddSample.value,
205207
SampleRate: ddSample.sampleRate,
@@ -237,7 +239,8 @@ func enrichEventAlertType(dogstatsdAlertType alertType) metricsevent.AlertType {
237239
}
238240

239241
func enrichEvent(event dogstatsdEvent, origin string, processID uint32, conf enrichConfig) *metricsevent.Event {
240-
tags, hostnameFromTags, extractedOrigin, _, _ := extractTagsMetadata(event.tags, origin, processID, event.localData, event.externalData, event.cardinality, conf)
242+
itags, hostnameFromTags, extractedOrigin, _, _ := extractTagsMetadata(event.tags, origin, processID, event.localData, event.externalData, event.cardinality, conf)
243+
tags := tagset.Values(itags)
241244

242245
enrichedEvent := &metricsevent.Event{
243246
Title: event.title,
@@ -274,7 +277,8 @@ func enrichServiceCheckStatus(status serviceCheckStatus) servicecheck.ServiceChe
274277
}
275278

276279
func enrichServiceCheck(serviceCheck dogstatsdServiceCheck, origin string, processID uint32, conf enrichConfig) *servicecheck.ServiceCheck {
277-
tags, hostnameFromTags, extractedOrigin, _, _ := extractTagsMetadata(serviceCheck.tags, origin, processID, serviceCheck.localData, serviceCheck.externalData, serviceCheck.cardinality, conf)
280+
itags, hostnameFromTags, extractedOrigin, _, _ := extractTagsMetadata(serviceCheck.tags, origin, processID, serviceCheck.localData, serviceCheck.externalData, serviceCheck.cardinality, conf)
281+
tags := tagset.Values(itags)
278282

279283
enrichedServiceCheck := &servicecheck.ServiceCheck{
280284
CheckName: serviceCheck.name,

comp/dogstatsd/server/impl/enrich_bench_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/DataDog/datadog-agent/comp/core/tagger/origindetection"
1313
"github.com/DataDog/datadog-agent/pkg/metrics"
14+
"github.com/DataDog/datadog-agent/pkg/tagset"
1415
utilstrings "github.com/DataDog/datadog-agent/pkg/util/strings"
1516
)
1617

@@ -24,15 +25,15 @@ func buildTags(tagCount int) []string {
2425
}
2526

2627
// used to store the result and avoid optimizations
27-
var tags []string
28+
var tags []tagset.InternedTag
2829

2930
func BenchmarkExtractTagsMetadata(b *testing.B) {
3031
conf := enrichConfig{
3132
defaultHostname: "hostname",
3233
}
3334
for i := 20; i <= 200; i += 20 {
3435
b.Run(fmt.Sprintf("%d-tags", i), func(sb *testing.B) {
35-
baseTags := append([]string{hostTagPrefix + "foo", entityIDTagPrefix + "bar"}, buildTags(i/10)...)
36+
baseTags := tagset.InternAll(append([]string{hostTagPrefix + "foo", entityIDTagPrefix + "bar"}, buildTags(i/10)...))
3637
sb.ResetTimer()
3738

3839
for n := 0; n < sb.N; n++ {
@@ -46,7 +47,7 @@ func BenchmarkMetricsExclusion(b *testing.B) {
4647
conf := enrichConfig{}
4748

4849
sample := dogstatsdMetricSample{
49-
name: "datadog.agent.testing.metric.does_not_match",
50+
name: tagset.Intern("datadog.agent.testing.metric.does_not_match"),
5051
}
5152

5253
out := make([]metrics.MetricSample, 0, 10)

comp/dogstatsd/server/impl/enrich_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/DataDog/datadog-agent/pkg/metrics/event"
2222
"github.com/DataDog/datadog-agent/pkg/metrics/servicecheck"
2323
taggertypes "github.com/DataDog/datadog-agent/pkg/tagger/types"
24+
"github.com/DataDog/datadog-agent/pkg/tagset"
2425
"github.com/DataDog/datadog-agent/pkg/util/infratags"
2526
utilstrings "github.com/DataDog/datadog-agent/pkg/util/strings"
2627
)
@@ -50,7 +51,7 @@ func parseAndEnrichSingleMetricMessage(t *testing.T, message []byte, conf enrich
5051
if len(samples) != 1 {
5152
return metrics.MetricSample{}, errors.New("wrong number of metrics parsed")
5253
}
53-
return samples[0], nil
54+
return resolveSampleTags(samples)[0], nil
5455
}
5556

5657
func parseAndEnrichMultipleMetricMessage(t *testing.T, message []byte, conf enrichConfig) ([]metrics.MetricSample, error) {
@@ -63,7 +64,18 @@ func parseAndEnrichMultipleMetricMessage(t *testing.T, message []byte, conf enri
6364
}
6465

6566
samples := []metrics.MetricSample{}
66-
return enrichMetricSample(samples, parsed, "", 0, "", conf, nil), nil
67+
return resolveSampleTags(enrichMetricSample(samples, parsed, "", 0, "", conf, nil)), nil
68+
}
69+
70+
// resolveSampleTags materializes the interned tags the dogstatsd pipeline
71+
// produces into MetricSample.Tags, so that tests can keep asserting on plain
72+
// strings. In production this resolution happens further down, at the tag
73+
// accumulator.
74+
func resolveSampleTags(samples []metrics.MetricSample) []metrics.MetricSample {
75+
for i := range samples {
76+
samples[i].Tags = tagset.Values(samples[i].ITags)
77+
}
78+
return samples
6779
}
6880

6981
func parseAndEnrichServiceCheckMessage(t *testing.T, message []byte, conf enrichConfig) (*servicecheck.ServiceCheck, error) {
@@ -1500,8 +1512,8 @@ func TestEnrichTags(t *testing.T) {
15001512
tt.wantedOrigin.ProductOrigin = origindetection.ProductOriginDogStatsD
15011513

15021514
t.Run(tt.name, func(t *testing.T) {
1503-
tags, host, origin, metricSource, _ := extractTagsMetadata(tt.args.tags, tt.args.originFromUDS, 0, tt.args.localData, tt.args.externalData, tt.args.cardinality, tt.args.conf)
1504-
assert.Equal(t, tt.wantedTags, tags)
1515+
tags, host, origin, metricSource, _ := extractTagsMetadata(tagset.InternAll(tt.args.tags), tt.args.originFromUDS, 0, tt.args.localData, tt.args.externalData, tt.args.cardinality, tt.args.conf)
1516+
assert.Equal(t, tt.wantedTags, tagset.Values(tags))
15051517
assert.Equal(t, tt.wantedHost, host)
15061518
assert.Equal(t, tt.wantedOrigin, origin)
15071519
assert.Equal(t, tt.wantedMetricSource, metricSource)
@@ -1548,7 +1560,8 @@ func TestEnrichTagsWithJMXCheckName(t *testing.T) {
15481560
}
15491561
for _, tt := range tests {
15501562
t.Run(tt.name, func(t *testing.T) {
1551-
tags, _, _, metricSource, _ := extractTagsMetadata(tt.tags, "", 0, origindetection.LocalData{}, origindetection.ExternalData{}, "", enrichConfig{})
1563+
itags, _, _, metricSource, _ := extractTagsMetadata(tagset.InternAll(tt.tags), "", 0, origindetection.LocalData{}, origindetection.ExternalData{}, "", enrichConfig{})
1564+
tags := tagset.Values(itags)
15521565
assert.Equal(t, tt.wantedTags, tags)
15531566
assert.Equal(t, tt.wantedMetricSource, metricSource)
15541567
assert.NotContains(t, tags, tt.jmxCheckName)

comp/dogstatsd/server/impl/intern.go

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,40 @@ package serverimpl
77

88
import (
99
"fmt"
10+
11+
"github.com/DataDog/datadog-agent/pkg/tagset"
1012
)
1113

12-
// stringInterner is a string cache providing a longer life for strings,
13-
// helping to avoid GC runs because they're re-used many times instead of
14-
// created every time.
14+
// stringInterner hands out tagset.InternedTag values for strings read off the
15+
// wire, so that a tag or metric name seen many times is stored once.
1516
//
16-
// The current interning strategy is fairly simple, but can require manual
17-
// adjustments of the `maxSize` to improve performance, which is not ideal.
18-
19-
// However the current strategy works well enough, and there is an
20-
// accepted go proposal to offer an "interning" mechanism from the
21-
// go runtime directly.
22-
23-
// Once this is available, the interner design should be re-visited to
24-
// take advantage of the new "Unique" api that is proposed below.
25-
// ref: https://github.com/golang/go/issues/62483
17+
// Canonicalization itself is done by the standard library `unique` package: it
18+
// owns the single copy of each string and keeps it alive exactly as long as some
19+
// handle still refers to it. That removes the two problems the hand-rolled
20+
// interner had:
21+
//
22+
// - a reset dropped the canonical strings, so the same tag arriving after a
23+
// reset allocated a fresh copy and the agent held several copies of the same
24+
// tag at once. Handles issued before a reset stay valid and stay canonical.
25+
// - entries were retained forever (up to maxSize) even if no sample referenced
26+
// them anymore. `unique` reclaims a string once the last handle goes away.
27+
//
28+
// What is left here is a per-worker lookaside cache. It exists for two reasons:
29+
// looking up by []byte without allocating a string (which unique.Make cannot do),
30+
// and memoizing the tag hash so a given tag is hashed once rather than once per
31+
// sample carrying it. maxSize now only bounds this cache, not the strings.
2632
type stringInterner struct {
27-
strings map[string]string
33+
cache map[string]tagset.InternedTag
2834
maxSize int
2935
id string
3036

3137
telemetry *stringInternerInstanceTelemetry
3238
}
3339

3440
func newStringInterner(maxSize int, internerID int, siTelemetry *stringInternerTelemetry) *stringInterner {
35-
// telemetryOnce.Do(func() { initGlobalTelemetry(telemetrycomp) })
36-
3741
id := fmt.Sprintf("interner_%d", internerID)
3842
i := &stringInterner{
39-
strings: make(map[string]string),
43+
cache: make(map[string]tagset.InternedTag),
4044
id: id,
4145
maxSize: maxSize,
4246
telemetry: siTelemetry.PrepareForID(id),
@@ -45,31 +49,52 @@ func newStringInterner(maxSize int, internerID int, siTelemetry *stringInternerT
4549
return i
4650
}
4751

48-
// LoadOrStore always returns the string from the cache, adding it into the
49-
// cache if needed.
50-
// If we need to store a new entry and the cache is at its maximum capacity,
51-
// it is reset.
52-
func (i *stringInterner) LoadOrStore(key []byte) string {
52+
// LoadOrStore always returns a handle for the given key, interning it if this is
53+
// the first time the worker sees it.
54+
func (i *stringInterner) LoadOrStore(key []byte) tagset.InternedTag {
5355
// here is the string interner trick: the map lookup using
5456
// string(key) doesn't actually allocate a string, but is
5557
// returning the string value -> no new heap allocation
5658
// for this string.
5759
// See https://github.com/golang/go/commit/f5f5a8b6209f84961687d993b93ea0d397f5d5bf
58-
if s, found := i.strings[string(key)]; found {
60+
if t, found := i.cache[string(key)]; found {
61+
i.telemetry.Hit()
62+
return t
63+
}
64+
65+
if len(i.cache) >= i.maxSize {
66+
i.telemetry.Reset(len(i.cache))
67+
68+
i.cache = make(map[string]tagset.InternedTag)
69+
}
70+
71+
t := tagset.InternBytes(key)
72+
// Key the cache on the canonical string so the cache does not retain a second
73+
// copy of it.
74+
i.cache[t.Value()] = t
75+
76+
i.telemetry.Miss(len(key))
77+
78+
return t
79+
}
80+
81+
// LoadOrStoreString is LoadOrStore for a key the caller already holds as a string.
82+
func (i *stringInterner) LoadOrStoreString(key string) tagset.InternedTag {
83+
if t, found := i.cache[key]; found {
5984
i.telemetry.Hit()
60-
return s
85+
return t
6186
}
6287

63-
if len(i.strings) >= i.maxSize {
64-
i.telemetry.Reset(len(i.strings))
88+
if len(i.cache) >= i.maxSize {
89+
i.telemetry.Reset(len(i.cache))
6590

66-
i.strings = make(map[string]string)
91+
i.cache = make(map[string]tagset.InternedTag)
6792
}
6893

69-
s := string(key)
70-
i.strings[s] = s
94+
t := tagset.Intern(key)
95+
i.cache[t.Value()] = t
7196

72-
i.telemetry.Miss(len(s))
97+
i.telemetry.Miss(len(key))
7398

74-
return s
99+
return t
75100
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package serverimpl
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
"github.com/DataDog/datadog-agent/pkg/metrics"
15+
"github.com/DataDog/datadog-agent/pkg/tagset"
16+
)
17+
18+
// TestInternedTagsReachTheSample checks that tags parsed off the wire arrive at
19+
// the aggregator as handles rather than being copied into plain strings, and that
20+
// the handles are canonical: the same tag seen on two different messages resolves
21+
// to the same handle, so the aggregator holds one copy of the string.
22+
func TestInternedTagsReachTheSample(t *testing.T) {
23+
conf := enrichConfig{defaultHostname: "default-hostname"}
24+
25+
first, err := parseAndEnrichMultipleMetricMessageNoResolve(t,
26+
[]byte("daemon:666|g|#env:prod,service:api"), conf)
27+
require.NoError(t, err)
28+
require.Len(t, first, 1)
29+
30+
second, err := parseAndEnrichMultipleMetricMessageNoResolve(t,
31+
[]byte("other:1|c|#env:prod,service:api"), conf)
32+
require.NoError(t, err)
33+
require.Len(t, second, 1)
34+
35+
// The pipeline populates ITags, not Tags.
36+
require.Len(t, first[0].ITags, 2)
37+
assert.Nil(t, first[0].Tags, "the interned pipeline must not materialize Tags")
38+
39+
assert.Equal(t, []string{"env:prod", "service:api"}, tagset.Values(first[0].ITags))
40+
41+
// Handles are canonical across parsers and messages.
42+
assert.Equal(t, first[0].ITags, second[0].ITags,
43+
"identical tags on different messages must share handles")
44+
45+
// The memoized hash matches what the accumulator would compute for the string.
46+
for _, itag := range first[0].ITags {
47+
assert.Equal(t, tagset.Intern(itag.Value()).Hash(), itag.Hash())
48+
}
49+
}
50+
51+
// TestInternedTagsFeedAccumulator checks the aggregator side: GetTags must push
52+
// the handles into the hashing accumulator with their precomputed hashes, and the
53+
// resulting tag set must be identical to the one a plain-string sample produces.
54+
func TestInternedTagsFeedAccumulator(t *testing.T) {
55+
conf := enrichConfig{defaultHostname: "default-hostname"}
56+
57+
samples, err := parseAndEnrichMultipleMetricMessageNoResolve(t,
58+
[]byte("daemon:666|g|#env:prod,service:api"), conf)
59+
require.NoError(t, err)
60+
require.Len(t, samples, 1)
61+
62+
interned := tagset.NewHashingTagsAccumulator()
63+
interned.AppendInterned(samples[0].ITags...)
64+
65+
plain := tagset.NewHashingTagsAccumulatorWithTags([]string{"env:prod", "service:api"})
66+
67+
assert.Equal(t, plain.Get(), interned.Get())
68+
assert.Equal(t, plain.Hash(), interned.Hash(),
69+
"interned tags must hash identically to the same tags as strings")
70+
}
71+
72+
func parseAndEnrichMultipleMetricMessageNoResolve(t *testing.T, message []byte, conf enrichConfig) ([]metrics.MetricSample, error) {
73+
deps := newServerDeps(t)
74+
stringInternerTelemetry := newSiTelemetry(false, deps.Telemetry)
75+
parser := newParser(deps.Config, newFloat64ListPool(deps.Config, deps.Telemetry), 1, deps.WMeta, stringInternerTelemetry)
76+
parsed, err := parser.parseMetricSample(message)
77+
if err != nil {
78+
return nil, err
79+
}
80+
return enrichMetricSample(nil, parsed, "", 0, "", conf, nil), nil
81+
}

0 commit comments

Comments
 (0)