Labels: Add DropMetricName function, used in PromQL (#13446)

This function is called very frequently when executing PromQL functions,
and we can do it much more efficiently inside Labels.

In the common case that `__name__` comes first in the labels, we simply
re-point to start at the next label, which is nearly free.

`DropMetricName` is now so cheap I removed the cache - benchmarks show
everything still goes faster.

Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
pull/13462/head
Bryan Boreham 2024-01-25 10:48:49 +00:00 committed by GitHub
parent 3f30ad3cc2
commit 74b73d1e2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 58 additions and 37 deletions

View File

@ -342,6 +342,19 @@ func (ls Labels) Validate(f func(l Label) error) error {
return nil
}
// DropMetricName returns Labels with "__name__" removed.
func (ls Labels) DropMetricName() Labels {
for i, l := range ls {
if l.Name == MetricName {
if i == 0 { // Make common case fast with no allocations.
return ls[1:]
}
return append(ls[:i], ls[i+1:]...)
}
}
return ls
}
// InternStrings calls intern on every string value inside ls, replacing them with what it returns.
func (ls *Labels) InternStrings(intern func(string) string) {
for i, l := range *ls {

View File

@ -429,6 +429,27 @@ func (ls Labels) Validate(f func(l Label) error) error {
return nil
}
// DropMetricName returns Labels with "__name__" removed.
func (ls Labels) DropMetricName() Labels {
for i := 0; i < len(ls.data); {
lName, i2 := decodeString(ls.data, i)
size, i2 := decodeSize(ls.data, i2)
i2 += size
if lName == MetricName {
if i == 0 { // Make common case fast with no allocations.
ls.data = ls.data[i2:]
} else {
ls.data = ls.data[:i] + ls.data[i2:]
}
break
} else if lName[0] > MetricName[0] { // Stop looking if we've gone past.
break
}
i = i2
}
return ls
}
// InternStrings calls intern on every string value inside ls, replacing them with what it returns.
func (ls *Labels) InternStrings(intern func(string) string) {
ls.data = intern(ls.data)

View File

@ -447,6 +447,12 @@ func TestLabels_Get(t *testing.T) {
require.Equal(t, "222", FromStrings("aaaa", "111", "bbb", "222").Get("bbb"))
}
func TestLabels_DropMetricName(t *testing.T) {
require.True(t, Equal(FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").DropMetricName()))
require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "aaa", "111").DropMetricName()))
require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222").DropMetricName()))
}
// BenchmarkLabels_Get was written to check whether a binary search can improve the performance vs the linear search implementation
// The results have shown that binary search would only be better when searching last labels in scenarios with more than 10 labels.
// In the following list, `old` is the linear search while `new` is the binary search implementation (without calling sort.Search, which performs even worse here)

View File

@ -1076,7 +1076,7 @@ type EvalNodeHelper struct {
Out Vector
// Caches.
// DropMetricName and label_*.
// label_*.
Dmn map[uint64]labels.Labels
// funcHistogramQuantile for classic histograms.
signatureToMetricWithBuckets map[string]*metricWithBuckets
@ -1101,21 +1101,6 @@ func (enh *EvalNodeHelper) resetBuilder(lbls labels.Labels) {
}
}
// DropMetricName is a cached version of DropMetricName.
func (enh *EvalNodeHelper) DropMetricName(l labels.Labels) labels.Labels {
if enh.Dmn == nil {
enh.Dmn = make(map[uint64]labels.Labels, len(enh.Out))
}
h := l.Hash()
ret, ok := enh.Dmn[h]
if ok {
return ret
}
ret = dropMetricName(l)
enh.Dmn[h] = ret
return ret
}
// rangeEval evaluates the given expressions, and then for each step calls
// the given funcCall with the values computed for each expression at that
// step. The return value is the combination into time series of all the
@ -1492,7 +1477,7 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio
// vector functions, the only change needed is to drop the
// metric name in the output.
if e.Func.Name != "last_over_time" {
metric = dropMetricName(metric)
metric = metric.DropMetricName()
}
ss := Series{
Metric: metric,
@ -1624,7 +1609,7 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, annotations.Annotatio
mat := val.(Matrix)
if e.Op == parser.SUB {
for i := range mat {
mat[i].Metric = dropMetricName(mat[i].Metric)
mat[i].Metric = mat[i].Metric.DropMetricName()
for j := range mat[i].Floats {
mat[i].Floats[j].F = -mat[i].Floats[j].F
}
@ -2370,7 +2355,7 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
}
metric := resultMetric(ls.Metric, rs.Metric, op, matching, enh)
if returnBool {
metric = enh.DropMetricName(metric)
metric = metric.DropMetricName()
}
insertedSigs, exists := matchedSigs[sig]
if matching.Card == parser.CardOneToOne {
@ -2492,7 +2477,7 @@ func (ev *evaluator) VectorscalarBinop(op parser.ItemType, lhs Vector, rhs Scala
lhsSample.F = float
lhsSample.H = histogram
if shouldDropMetricName(op) || returnBool {
lhsSample.Metric = enh.DropMetricName(lhsSample.Metric)
lhsSample.Metric = lhsSample.Metric.DropMetricName()
}
enh.Out = append(enh.Out, lhsSample)
}
@ -2500,10 +2485,6 @@ func (ev *evaluator) VectorscalarBinop(op parser.ItemType, lhs Vector, rhs Scala
return enh.Out
}
func dropMetricName(l labels.Labels) labels.Labels {
return labels.NewBuilder(l).Del(labels.MetricName).Labels()
}
// scalarBinop evaluates a binary operation between two Scalars.
func scalarBinop(op parser.ItemType, lhs, rhs float64) float64 {
switch op {

View File

@ -445,7 +445,7 @@ func funcClamp(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper
}
for _, el := range vec {
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: math.Max(min, math.Min(max, el.F)),
})
}
@ -458,7 +458,7 @@ func funcClampMax(vals []parser.Value, args parser.Expressions, enh *EvalNodeHel
max := vals[1].(Vector)[0].F
for _, el := range vec {
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: math.Min(max, el.F),
})
}
@ -471,7 +471,7 @@ func funcClampMin(vals []parser.Value, args parser.Expressions, enh *EvalNodeHel
min := vals[1].(Vector)[0].F
for _, el := range vec {
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: math.Max(min, el.F),
})
}
@ -493,7 +493,7 @@ func funcRound(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper
for _, el := range vec {
f := math.Floor(el.F*toNearestInverse+0.5) / toNearestInverse
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: f,
})
}
@ -797,7 +797,7 @@ func simpleFunc(vals []parser.Value, enh *EvalNodeHelper, f func(float64) float6
for _, el := range vals[0].(Vector) {
if el.H == nil { // Process only float samples.
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: f(el.F),
})
}
@ -943,7 +943,7 @@ func funcTimestamp(vals []parser.Value, args parser.Expressions, enh *EvalNodeHe
vec := vals[0].(Vector)
for _, el := range vec {
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: float64(el.T) / 1000,
})
}
@ -1057,7 +1057,7 @@ func funcHistogramCount(vals []parser.Value, args parser.Expressions, enh *EvalN
continue
}
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: sample.H.Count,
})
}
@ -1074,7 +1074,7 @@ func funcHistogramSum(vals []parser.Value, args parser.Expressions, enh *EvalNod
continue
}
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: sample.H.Sum,
})
}
@ -1107,7 +1107,7 @@ func funcHistogramStdDev(vals []parser.Value, args parser.Expressions, enh *Eval
variance += cVariance
variance /= sample.H.Count
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: math.Sqrt(variance),
})
}
@ -1140,7 +1140,7 @@ func funcHistogramStdVar(vals []parser.Value, args parser.Expressions, enh *Eval
variance += cVariance
variance /= sample.H.Count
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: variance,
})
}
@ -1159,7 +1159,7 @@ func funcHistogramFraction(vals []parser.Value, args parser.Expressions, enh *Ev
continue
}
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: histogramFraction(lower, upper, sample.H),
})
}
@ -1230,7 +1230,7 @@ func funcHistogramQuantile(vals []parser.Value, args parser.Expressions, enh *Ev
}
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(sample.Metric),
Metric: sample.Metric.DropMetricName(),
F: histogramQuantile(q, sample.H),
})
}
@ -1440,7 +1440,7 @@ func dateWrapper(vals []parser.Value, enh *EvalNodeHelper, f func(time.Time) flo
for _, el := range vals[0].(Vector) {
t := time.Unix(int64(el.F), 0).UTC()
enh.Out = append(enh.Out, Sample{
Metric: enh.DropMetricName(el.Metric),
Metric: el.Metric.DropMetricName(),
F: f(t),
})
}