Skip to content

[PERF] PromQL: Move more work to preprocessing step #16896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

bboreham
Copy link
Member

@bboreham bboreham commented Jul 19, 2025

Instead of doing lots of unwrapping during the evaluation phase, take more care over what we wrap, and unwrap superfluous parens that the caller added, during the preprocessing step.

This means we only do it once, rather than on every step of a range query. Also the code gets a bit shorter.
Looking at the history, some of this came in #9591 as a fix for #9590, because the original version was too eager to unwrap. This version does not unwrap parens on binary expressions, where they are important for precedence.

Matrix selectors have a Timestamp which indicates they are time-invariant, so we don't need to wrap and then unwrap them when we come to use them.

Fix up tests that check very specific details which change due to this change of approach.

This should address #16880, although I didn't add any tests for that (yet).

Separated into multiple commits for ease of understanding.

There is one remaining call to unwrapStepInvariantExpr, in the special handling for timestep(). This seems like a bad smell, but I left it for another time.

bboreham added 4 commits July 19, 2025 16:32
This should mean we can stop unwrapping them later.

Fix up tests that check very specific details.

Signed-off-by: Bryan Boreham <[email protected]>
Matrix selectors have a Timestamp which indicates they are time-invariant,
so we don't need to wrap and then unwrap them when we come to use them.

Fix up tests that check this level of detail.

Signed-off-by: Bryan Boreham <[email protected]>
In aggregations and function calls. We no longer wrap the literal values
or matrix selectors that would appear here.

Signed-off-by: Bryan Boreham <[email protected]>
This means we only do it once, rather than on every step of a range
query. Also the code gets a bit shorter.

Signed-off-by: Bryan Boreham <[email protected]>

case *parser.MatrixSelector:
return preprocessExprHelper(n.VectorSelector, start, end)
// We don't need to wrap a MatrixSelector because functions over range vectors evaluate those directly,
// and they can't appear at top level in a range query.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thoughts, I think this second line of comment is unnecessary.
It was provoked by a test failing.
So long as all evaluations of MatrixSelector check the Timestamp field, we don't need a wrapper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this would change after #14095? post that PR, the MatrixSelector can appear in a range query and it will be evaluated in an eval call, so we'd want it to be wrapped in StepInvariantExpr if it is indeed step invariant
eg:

rate(my_metric[5m] @ start() != 0)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that perhaps isn't obvious about preprocessExprHelper is that it is coded to wrap the highest possible node in the tree with StepInvariantExpr. In your example the BinaryExpr node should get wrapped, and no wrapping is needed on the MatrixSelector.

Try it!

@@ -2304,20 +2304,16 @@ func TestPreprocessAndWrapWithStepInvariantExpr(t *testing.T) {
}{
{
input: "123.4567",
Copy link

@tcp13equals2 tcp13equals2 Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is still one more missing unwrap ...

This can be verified by adding in test cases for the following;

  • "()"
  • ("<string>")
  • ("")

Currently these would not be unwrapped to the NumberLiteral and StringLiteral.

I think in the PreprocessExpr() it is safe to do a unwrapParenExpr(&expr) before passing into the preprocessExprHelper and this will then allow the above test cases to pass.

The current flow of handling these cases in the *parser.ParenExpr in the preprocessExprHelper() does not actually strip the unnecessary parent expr.

Try adding these test cases in;

{ input: "(123.4567)", expected: &parser.NumberLiteral{ Val: 123.4567, PosRange: posrange.PositionRange{Start: 1, End: 9}, }, }, { input:("foo"), expected: &parser.StringLiteral{ Val: "foo", PosRange: posrange.PositionRange{Start: 1, End: 6}, }, }, { input: (""), expected: &parser.StringLiteral{ Val: "", PosRange: posrange.PositionRange{Start: 1, End: 3}, }, }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My PR does not aim to unwrap those expressions. They evaluate correctly, right? That's all we need.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

("") and ("") work. I double checked one of the originals range queries in unecessary () and it is also good.

() returns an error, but I am guessing that is not a valid query.

So all good!

@tcp13equals2
Copy link

hey @bboreham - I think there is one more case to be considered which can be exposed by adding another few test cases to the TestPreprocessAndWrapWithStepInvariantExpr() - see comment on the file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants