Merge pull request #17067 from prometheus/faster-promql-printer

[PERF] PromQL: Speed up PromQL to string conversion
This commit is contained in:
Bryan Boreham 2025-09-04 13:41:21 +01:00 committed by GitHub
commit 1cd746ebfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 126 additions and 56 deletions

View file

@ -54,7 +54,7 @@ func (e *AggregateExpr) Pretty(level int) string {
return s
}
s += e.getAggOpStr()
s += e.ShortString()
s += "(\n"
if e.Op.IsAggregatorWithParam() {

View file

@ -53,49 +53,57 @@ func (node *EvalStmt) String() string {
}
func (es Expressions) String() (s string) {
if len(es) == 0 {
switch len(es) {
case 0:
return ""
case 1:
return es[0].String()
}
for _, e := range es {
s += e.String()
s += ", "
b := bytes.NewBuffer(make([]byte, 0, 1024))
b.WriteString(es[0].String())
for _, e := range es[1:] {
b.WriteString(", ")
b.WriteString(e.String())
}
return s[:len(s)-2]
return b.String()
}
func (node *AggregateExpr) String() string {
aggrString := node.getAggOpStr()
aggrString += "("
b := bytes.NewBuffer(make([]byte, 0, 1024))
node.writeAggOpStr(b)
b.WriteString("(")
if node.Op.IsAggregatorWithParam() {
aggrString += fmt.Sprintf("%s, ", node.Param)
b.WriteString(node.Param.String())
b.WriteString(", ")
}
aggrString += fmt.Sprintf("%s)", node.Expr)
b.WriteString(node.Expr.String())
b.WriteString(")")
return aggrString
return b.String()
}
func (node *AggregateExpr) ShortString() string {
aggrString := node.getAggOpStr()
return aggrString
b := bytes.NewBuffer(make([]byte, 0, 1024))
node.writeAggOpStr(b)
return b.String()
}
func (node *AggregateExpr) getAggOpStr() string {
aggrString := node.Op.String()
func (node *AggregateExpr) writeAggOpStr(b *bytes.Buffer) {
b.WriteString(node.Op.String())
switch {
case node.Without:
aggrString += fmt.Sprintf(" without (%s) ", joinLabels(node.Grouping))
b.WriteString(" without (")
writeLabels(b, node.Grouping)
b.WriteString(") ")
case len(node.Grouping) > 0:
aggrString += fmt.Sprintf(" by (%s) ", joinLabels(node.Grouping))
b.WriteString(" by (")
writeLabels(b, node.Grouping)
b.WriteString(") ")
}
return aggrString
}
func joinLabels(ss []string) string {
var bytea [1024]byte // On stack to avoid memory allocation while building the output.
b := bytes.NewBuffer(bytea[:0])
func writeLabels(b *bytes.Buffer, ss []string) {
for i, s := range ss {
if i > 0 {
b.WriteString(", ")
@ -106,7 +114,18 @@ func joinLabels(ss []string) string {
b.WriteString(s)
}
}
return b.String()
}
// writeStringsJoin is like strings.Join but appending to a bytes.Buffer.
func writeStringsJoin(b *bytes.Buffer, elems []string, sep string) {
if len(elems) == 0 {
return
}
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
}
func (node *BinaryExpr) returnBool() string {
@ -118,11 +137,11 @@ func (node *BinaryExpr) returnBool() string {
func (node *BinaryExpr) String() string {
matching := node.getMatchingStr()
return fmt.Sprintf("%s %s%s%s %s", node.LHS, node.Op, node.returnBool(), matching, node.RHS)
return node.LHS.String() + " " + node.Op.String() + node.returnBool() + matching + " " + node.RHS.String()
}
func (node *BinaryExpr) ShortString() string {
return fmt.Sprintf("%s%s%s", node.Op, node.returnBool(), node.getMatchingStr())
return node.Op.String() + node.returnBool() + node.getMatchingStr()
}
func (node *BinaryExpr) getMatchingStr() string {
@ -147,32 +166,54 @@ func (node *BinaryExpr) getMatchingStr() string {
}
func (node *DurationExpr) String() string {
var expr string
b := bytes.NewBuffer(make([]byte, 0, 1024))
node.writeTo(b)
return b.String()
}
func (node *DurationExpr) writeTo(b *bytes.Buffer) {
if node.Wrapped {
b.WriteByte('(')
}
switch {
case node.Op == STEP:
expr = "step()"
b.WriteString("step()")
case node.Op == MIN:
expr = fmt.Sprintf("min(%s, %s)", node.LHS, node.RHS)
b.WriteString("min(")
b.WriteString(node.LHS.String())
b.WriteString(", ")
b.WriteString(node.RHS.String())
b.WriteByte(')')
case node.Op == MAX:
expr = fmt.Sprintf("max(%s, %s)", node.LHS, node.RHS)
b.WriteString("max(")
b.WriteString(node.LHS.String())
b.WriteString(", ")
b.WriteString(node.RHS.String())
b.WriteByte(')')
case node.LHS == nil:
// This is a unary duration expression.
switch node.Op {
case SUB:
expr = fmt.Sprintf("%s%s", node.Op, node.RHS)
b.WriteString(node.Op.String())
b.WriteString(node.RHS.String())
case ADD:
expr = node.RHS.String()
b.WriteString(node.RHS.String())
default:
// This should never happen.
panic(fmt.Sprintf("unexpected unary duration expression: %s", node.Op))
}
default:
expr = fmt.Sprintf("%s %s %s", node.LHS, node.Op, node.RHS)
b.WriteString(node.LHS.String())
b.WriteByte(' ')
b.WriteString(node.Op.String())
b.WriteByte(' ')
b.WriteString(node.RHS.String())
}
if node.Wrapped {
return fmt.Sprintf("(%s)", expr)
b.WriteByte(')')
}
return expr
}
func (node *DurationExpr) ShortString() string {
@ -180,7 +221,7 @@ func (node *DurationExpr) ShortString() string {
}
func (node *Call) String() string {
return fmt.Sprintf("%s(%s)", node.Func.Name, node.Args)
return node.Func.Name + "(" + node.Args.String() + ")"
}
func (node *Call) ShortString() string {
@ -294,15 +335,15 @@ func (node *NumberLiteral) String() string {
}
func (node *ParenExpr) String() string {
return fmt.Sprintf("(%s)", node.Expr)
return "(" + node.Expr.String() + ")"
}
func (node *StringLiteral) String() string {
return fmt.Sprintf("%q", node.Val)
return strconv.Quote(node.Val)
}
func (node *UnaryExpr) String() string {
return fmt.Sprintf("%s%s", node.Op, node.Expr)
return node.Op.String() + node.Expr.String()
}
func (node *UnaryExpr) ShortString() string {
@ -321,28 +362,33 @@ func (node *VectorSelector) String() string {
}
labelStrings = append(labelStrings, matcher.String())
}
offset := ""
switch {
case node.OriginalOffsetExpr != nil:
offset = fmt.Sprintf(" offset %s", node.OriginalOffsetExpr)
case node.OriginalOffset > time.Duration(0):
offset = fmt.Sprintf(" offset %s", model.Duration(node.OriginalOffset))
case node.OriginalOffset < time.Duration(0):
offset = fmt.Sprintf(" offset -%s", model.Duration(-node.OriginalOffset))
b := bytes.NewBuffer(make([]byte, 0, 1024))
b.WriteString(node.Name)
if len(labelStrings) != 0 {
b.WriteByte('{')
sort.Strings(labelStrings)
writeStringsJoin(b, labelStrings, ",")
b.WriteByte('}')
}
at := ""
switch {
case node.Timestamp != nil:
at = fmt.Sprintf(" @ %.3f", float64(*node.Timestamp)/1000.0)
b.WriteString(" @ ")
b.Write(strconv.AppendFloat(b.AvailableBuffer(), float64(*node.Timestamp)/1000.0, 'f', 3, 64))
case node.StartOrEnd == START:
at = " @ start()"
b.WriteString(" @ start()")
case node.StartOrEnd == END:
at = " @ end()"
b.WriteString(" @ end()")
}
if len(labelStrings) == 0 {
return fmt.Sprintf("%s%s%s", node.Name, at, offset)
switch {
case node.OriginalOffsetExpr != nil:
b.WriteString(" offset ")
node.OriginalOffsetExpr.writeTo(b)
case node.OriginalOffset > time.Duration(0):
b.WriteString(" offset ")
b.WriteString(model.Duration(node.OriginalOffset).String())
case node.OriginalOffset < time.Duration(0):
b.WriteString(" offset -")
b.WriteString(model.Duration(-node.OriginalOffset).String())
}
sort.Strings(labelStrings)
return fmt.Sprintf("%s{%s}%s%s", node.Name, strings.Join(labelStrings, ","), at, offset)
return b.String()
}

View file

@ -217,6 +217,9 @@ func TestExprString(t *testing.T) {
{
in: "foo[200 - min(step() + 10s, -max(step() ^ 2, 3))]",
},
{
in: `predict_linear(foo[1h], 3000)`,
},
}
for _, test := range inputs {
@ -234,6 +237,27 @@ func TestExprString(t *testing.T) {
}
}
func BenchmarkExprString(b *testing.B) {
inputs := []string{
`sum by(code) (task:errors:rate10s{job="s"})`,
`max( 100 * (1 - avg by(instance) (irate(node_cpu_seconds_total{instance=~".*cust01.prd.*",mode="idle"}[86400s]))))`,
`http_requests_total{job="api-server", group="canary"} + rate(http_requests_total{job="api-server"}[10m]) * 5 * 60`,
`sum by (pod) ((kube_pod_container_status_restarts_total{namespace="mynamespace",cluster="mycluster"} - kube_pod_container_status_restarts_total{namespace="mynamespace}",cluster="mycluster}"} offset 10m) >= 1 and ignoring (reason) min_over_time(kube_pod_container_status_last_terminated_reason{namespace="mynamespace",cluster="mycluster",reason="OOMKilled"}[10m]) == 1)`,
`sum by (pod) ((kube_pod_container_status_restarts_total{cluster="mycluster",namespace="mynamespace"} - kube_pod_container_status_restarts_total{cluster="mycluster",namespace="mynamespace}"} offset 10m) >= 1 and ignoring (reason) min_over_time(kube_pod_container_status_last_terminated_reason{cluster="mycluster",namespace="mynamespace",reason="OOMKilled"}[10m]) == 1)`, // Sort matchers.
`label_replace(testmetric, "dst", "destination-value-$1", "src", "source-value-(.*)")`,
}
for _, test := range inputs {
b.Run(readable(test), func(b *testing.B) {
expr, err := ParseExpr(test)
require.NoError(b, err)
for i := 0; i < b.N; i++ {
_ = expr.String()
}
})
}
}
func TestVectorSelector_String(t *testing.T) {
for _, tc := range []struct {
name string