From 9cd711ed3eb5bdbcc60a79f7640b63f458d8ff62 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Mon, 13 May 2019 11:03:33 -0400 Subject: [PATCH 1/5] Add benchmark for tag parsing Signed-off-by: Clayton O'Neill --- exporter_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/exporter_test.go b/exporter_test.go index 160d053..2a5021f 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -527,3 +527,21 @@ func BenchmarkEscapeMetricName(b *testing.B) { }) } } + +func BenchmarkParseDogStatsDTagsToLabels(b *testing.B) { + scenarios := map[string]string{ + "1 tag w/hash": "#test:tag", + "1 tag w/o hash": "test:tag", + "2 tags, mixed hashes": "tag1:test,#tag2:test", + "3 long tags": "tag1:reallylongtagthisisreallylong,tag2:anotherreallylongtag,tag3:thisisyetanotherextraordinarilylongtag", + "a-z tags": "a:0,b:1,c:2,d:3,e:4,f:5,g:6,h:7,i:8,j:9,k:0,l:1,m:2,n:3,o:4,p:5,q:6,r:7,s:8,t:9,u:0,v:1,w:2,x:3,y:4,z:5", + } + + for name, tags := range scenarios { + b.Run(name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + parseDogStatsDTagsToLabels(tags) + } + }) + } +} From 052beaa3ac519d4568a818d048aa6ca906418d33 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Mon, 13 May 2019 11:18:27 -0400 Subject: [PATCH 2/5] Replace TrimPrefix w/special purpose implementation On the a-z benchmark, this brings the ns/op down from 5658 to 5533. Signed-off-by: Clayton O'Neill --- exporter.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/exporter.go b/exporter.go index d62a4bb..529df10 100644 --- a/exporter.go +++ b/exporter.go @@ -589,9 +589,18 @@ func parseDogStatsDTagsToLabels(component string) map[string]string { tagsReceived.Inc() tags := strings.Split(component, ",") for _, t := range tags { - t = strings.TrimPrefix(t, "#") - kv := strings.SplitN(t, ":", 2) + // Bail early if the tag is empty + if len(t) == 0 { + tagErrors.Inc() + log.Debugf("Empty tag found in '%s'", component) + continue + } + // Skip hash if found. + if t[0] == '#' { + t = t[1:] + } + kv := strings.SplitN(t, ":", 2) if len(kv) < 2 || len(kv[1]) == 0 { tagErrors.Inc() log.Debugf("Malformed or empty DogStatsD tag %s in component %s", t, component) From a441eac07a1208c1780909be8b197bab69e4ccb0 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Mon, 13 May 2019 11:32:28 -0400 Subject: [PATCH 3/5] Replace SplitN w/special purpose implementation This improves performance per call in the a-z case from around 5533 ns/op to 3169ns/op. It also drops allocations per call from 57 to 31 in the a-z case. Signed-off-by: Clayton O'Neill --- exporter.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/exporter.go b/exporter.go index 529df10..ee14893 100644 --- a/exporter.go +++ b/exporter.go @@ -600,14 +600,25 @@ func parseDogStatsDTagsToLabels(component string) map[string]string { t = t[1:] } - kv := strings.SplitN(t, ":", 2) - if len(kv) < 2 || len(kv[1]) == 0 { + // find the first comma and split the tag into key and value. + var k, v string + for i, c := range t { + if c == ':' { + k = t[0:i] + v = t[(i + 1):] + break + } + } + + // If either of them is empty, then either the k or v is empty, or we + // didn't find a colon, either way, throw an error and skip ahead. + if len(k) == 0 || len(v) == 0 { tagErrors.Inc() log.Debugf("Malformed or empty DogStatsD tag %s in component %s", t, component) continue } - labels[escapeMetricName(kv[0])] = kv[1] + labels[escapeMetricName(k)] = v } return labels } From e3cdd85a092046a2799b0d9447beb207401df95e Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Mon, 13 May 2019 11:42:33 -0400 Subject: [PATCH 4/5] Extract individual tag parsing into new function Signed-off-by: Clayton O'Neill --- exporter.go | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/exporter.go b/exporter.go index ee14893..830e25d 100644 --- a/exporter.go +++ b/exporter.go @@ -584,32 +584,34 @@ func buildEvent(statType, metric string, value float64, relative bool, labels ma } } +func parseDogStatsDTagToKeyValue(tag string) (k, v string) { + // Bail early if the tag is empty + if len(tag) == 0 { + return + } + // Skip hash if found. + if tag[0] == '#' { + tag = tag[1:] + } + + // find the first comma and split the tag into key and value. + for i, c := range tag { + if c == ':' { + k = tag[0:i] + v = tag[(i + 1):] + break + } + } + + return +} + func parseDogStatsDTagsToLabels(component string) map[string]string { labels := map[string]string{} tagsReceived.Inc() tags := strings.Split(component, ",") for _, t := range tags { - // Bail early if the tag is empty - if len(t) == 0 { - tagErrors.Inc() - log.Debugf("Empty tag found in '%s'", component) - continue - } - // Skip hash if found. - if t[0] == '#' { - t = t[1:] - } - - // find the first comma and split the tag into key and value. - var k, v string - for i, c := range t { - if c == ':' { - k = t[0:i] - v = t[(i + 1):] - break - } - } - + k, v := parseDogStatsDTagToKeyValue(t) // If either of them is empty, then either the k or v is empty, or we // didn't find a colon, either way, throw an error and skip ahead. if len(k) == 0 || len(v) == 0 { From a4faae262b0367781f97117de9f1d9161ede1ef3 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Mon, 13 May 2019 12:10:17 -0400 Subject: [PATCH 5/5] Replace Split with special purpose implementation This improves performance from 3169ns/op to 2836 ns/op and drops one allocation per op. Signed-off-by: Clayton O'Neill --- exporter.go | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/exporter.go b/exporter.go index 830e25d..f2f2d02 100644 --- a/exporter.go +++ b/exporter.go @@ -584,9 +584,11 @@ func buildEvent(statType, metric string, value float64, relative bool, labels ma } } -func parseDogStatsDTagToKeyValue(tag string) (k, v string) { +func handleDogStatsDTagToKeyValue(labels map[string]string, component, tag string) { // Bail early if the tag is empty if len(tag) == 0 { + tagErrors.Inc() + log.Debugf("Malformed or empty DogStatsD tag %s in component %s", tag, component) return } // Skip hash if found. @@ -595,6 +597,7 @@ func parseDogStatsDTagToKeyValue(tag string) (k, v string) { } // find the first comma and split the tag into key and value. + var k, v string for i, c := range tag { if c == ':' { k = tag[0:i] @@ -602,6 +605,15 @@ func parseDogStatsDTagToKeyValue(tag string) (k, v string) { break } } + // If either of them is empty, then either the k or v is empty, or we + // didn't find a colon, either way, throw an error and skip ahead. + if len(k) == 0 || len(v) == 0 { + tagErrors.Inc() + log.Debugf("Malformed or empty DogStatsD tag %s in component %s", tag, component) + return + } + + labels[escapeMetricName(k)] = v return } @@ -609,19 +621,22 @@ func parseDogStatsDTagToKeyValue(tag string) (k, v string) { func parseDogStatsDTagsToLabels(component string) map[string]string { labels := map[string]string{} tagsReceived.Inc() - tags := strings.Split(component, ",") - for _, t := range tags { - k, v := parseDogStatsDTagToKeyValue(t) - // If either of them is empty, then either the k or v is empty, or we - // didn't find a colon, either way, throw an error and skip ahead. - if len(k) == 0 || len(v) == 0 { - tagErrors.Inc() - log.Debugf("Malformed or empty DogStatsD tag %s in component %s", t, component) - continue - } - labels[escapeMetricName(k)] = v + lastTagEndIndex := 0 + for i, c := range component { + if c == ',' { + tag := component[lastTagEndIndex:i] + lastTagEndIndex = i + 1 + handleDogStatsDTagToKeyValue(labels, component, tag) + } } + + // If we're not off the end of the string, add the last tag + if lastTagEndIndex < len(component) { + tag := component[lastTagEndIndex:] + handleDogStatsDTagToKeyValue(labels, component, tag) + } + return labels } @@ -703,7 +718,7 @@ samples: multiplyEvents = int(1 / samplingFactor) } case '#': - labels = parseDogStatsDTagsToLabels(component) + labels = parseDogStatsDTagsToLabels(component[1:]) default: log.Debugf("Invalid sampling factor or tag section %s on line %s", components[2], line) sampleErrors.WithLabelValues("invalid_sample_factor").Inc()