From b0c1f3d819401841cebc06f560d09607cf5ece93 Mon Sep 17 00:00:00 2001
From: Roman Khavronenko <roman@victoriametrics.com>
Date: Tue, 14 May 2024 14:43:39 +0200
Subject: [PATCH] app/vmalert/rule: reduce number of allocations for
 getStaleSeries fn (#6269)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Allocations are reduced by re-using the byte buffer when converting
labels to string keys.
```
name               old allocs/op  new allocs/op  delta
GetStaleSeries-10       703 ± 0%       203 ± 0%   ~     (p=1.000 n=1+1)
```

Signed-off-by: hagen1778 <roman@victoriametrics.com>
---
 app/vmalert/rule/group.go             | 34 ++++++++++++++-----------
 app/vmalert/rule/group_timing_test.go | 36 +++++++++++++++++++++++++++
 app/vmalert/rule/test_helpers.go      |  2 +-
 3 files changed, 57 insertions(+), 15 deletions(-)
 create mode 100644 app/vmalert/rule/group_timing_test.go

diff --git a/app/vmalert/rule/group.go b/app/vmalert/rule/group.go
index f4e6cd0ddb..520039d71f 100644
--- a/app/vmalert/rule/group.go
+++ b/app/vmalert/rule/group.go
@@ -9,10 +9,11 @@ import (
 	"hash/fnv"
 	"net/url"
 	"strconv"
-	"strings"
 	"sync"
 	"time"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
+
 	"github.com/cheggaaa/pb/v3"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
@@ -724,13 +725,19 @@ func (e *executor) exec(ctx context.Context, r Rule, ts time.Time, resolveDurati
 	return errGr.Err()
 }
 
+var bbPool bytesutil.ByteBufferPool
+
 // getStaleSeries checks whether there are stale series from previously sent ones.
 func (e *executor) getStaleSeries(r Rule, tss []prompbmarshal.TimeSeries, timestamp time.Time) []prompbmarshal.TimeSeries {
+	bb := bbPool.Get()
+	defer bbPool.Put(bb)
+
 	ruleLabels := make(map[string][]prompbmarshal.Label, len(tss))
 	for _, ts := range tss {
-		// convert labels to strings so we can compare with previously sent series
-		key := labelsToString(ts.Labels)
-		ruleLabels[key] = ts.Labels
+		// convert labels to strings, so we can compare with previously sent series
+		bb.B = labelsToString(bb.B, ts.Labels)
+		ruleLabels[string(bb.B)] = ts.Labels
+		bb.Reset()
 	}
 
 	rID := r.ID()
@@ -776,21 +783,20 @@ func (e *executor) purgeStaleSeries(activeRules []Rule) {
 	e.previouslySentSeriesToRWMu.Unlock()
 }
 
-func labelsToString(labels []prompbmarshal.Label) string {
-	var b strings.Builder
-	b.WriteRune('{')
+func labelsToString(dst []byte, labels []prompbmarshal.Label) []byte {
+	dst = append(dst, '{')
 	for i, label := range labels {
 		if len(label.Name) == 0 {
-			b.WriteString("__name__")
+			dst = append(dst, "__name__"...)
 		} else {
-			b.WriteString(label.Name)
+			dst = append(dst, label.Name...)
 		}
-		b.WriteRune('=')
-		b.WriteString(strconv.Quote(label.Value))
+		dst = append(dst, '=')
+		dst = strconv.AppendQuote(dst, label.Value)
 		if i < len(labels)-1 {
-			b.WriteRune(',')
+			dst = append(dst, ',')
 		}
 	}
-	b.WriteRune('}')
-	return b.String()
+	dst = append(dst, '}')
+	return dst
 }
diff --git a/app/vmalert/rule/group_timing_test.go b/app/vmalert/rule/group_timing_test.go
new file mode 100644
index 0000000000..c233ed34da
--- /dev/null
+++ b/app/vmalert/rule/group_timing_test.go
@@ -0,0 +1,36 @@
+package rule
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+)
+
+func BenchmarkGetStaleSeries(b *testing.B) {
+	ts := time.Now()
+	n := 100
+	payload := make([]prompbmarshal.TimeSeries, n)
+	for i := 0; i < n; i++ {
+		s := fmt.Sprintf("%d", i)
+		labels := toPromLabels(b,
+			"__name__", "foo", ""+
+				"instance", s,
+			"job", s,
+			"state", s,
+		)
+		payload = append(payload, newTimeSeriesPB([]float64{1}, []int64{ts.Unix()}, labels))
+	}
+
+	e := &executor{
+		previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
+	}
+	ar := &AlertingRule{RuleID: 1}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		e.getStaleSeries(ar, payload, ts)
+	}
+}
diff --git a/app/vmalert/rule/test_helpers.go b/app/vmalert/rule/test_helpers.go
index dcf23dc768..9373e2b814 100644
--- a/app/vmalert/rule/test_helpers.go
+++ b/app/vmalert/rule/test_helpers.go
@@ -95,7 +95,7 @@ func metricWithLabels(t *testing.T, labels ...string) datasource.Metric {
 	return m
 }
 
-func toPromLabels(t *testing.T, labels ...string) []prompbmarshal.Label {
+func toPromLabels(t testing.TB, labels ...string) []prompbmarshal.Label {
 	t.Helper()
 	if len(labels) == 0 || len(labels)%2 != 0 {
 		t.Fatalf("expected to get even number of labels")