Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

perf: trim per-parse allocations that do no work - #2874

Merged
manticore-projects merged 4 commits into
JSQLParser:masterfrom
knutwannheden:jsqlparser-trim-two-per-parse-allocations
Oct 31, 2026
Merged

manticore-projects merged 4 commits into
JSQLParser:masterfrom
knutwannheden:jsqlparser-trim-two-per-parse-allocations

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Parsing performance.sql, the JMH benchmark's input, allocates 7.17 MB per parse on master, and about 1 MB of that (15%) does no work. The profile taken for #2870 found the sources: collections created on every call of a production but filled only on a rare path, a regex compiled for every LIKE, and function names joined into a string just to compare them.

This PR is separate from #2870 so each PR's benchmark stays clean.

  • Condition() created a deque for leading NOT and ! prefixes on every call, though few conditions are negated. An empty ArrayDeque allocates its 17-slot array up front. The deque is now created at the first NOT or !, as fix: give every parsed object a node spanning its source #2870 already does for the token deque beside it.
  • PrimaryExpression() created two lists on every call, for chained signs such as - -1 and for JSON operators such as ->. Both are now created at their first element.
  • LikeExpression.KeyWord.from called String.replaceAll("\\s+", "_"), which compiles the pattern on every call. The pattern is now a static constant.
  • consumeXmlLeadingKeyword and FunctionArgumentList called Function.getName() for every function to check for XMLPARSE, XMLELEMENT or XMLFOREST, and getName() joins the name parts into a new string each time. Both now compare the single name part.

Behaviour is unchanged, so there are no new tests. ExpressionPrecedenceTest pins stacked prefixes (not !1 = 1 deparses as NOT ! 1 = 1), DuckDBTest the chained signs, JsonExpressionRenderingTest the JSON operators, XmlForestTest the XML keywords and LikeExpressionTest the multi-word SIMILAR TO.

./gradlew jmh -PjmhProfiler=gc, alternating master (a075737) and this branch on the same machine. gc.alloc.rate.norm is bytes per parse:

run master this branch 5.4 (master run / branch run)
1 time 39.07 ± 2.05 ms/op 38.19 ± 1.39 ms/op 49.52 / 48.21 ms/op
1 alloc 7,166,709 B/op 6,118,424 B/op (−14.6%) 7,767,862 / 7,751,289
2 time 39.56 ± 1.90 ms/op 38.55 ± 1.84 ms/op 48.71 / 49.66 ms/op
2 alloc 7,183,726 B/op 6,084,685 B/op (−15.3%) 7,784,537 / 7,767,974

The branch is 2.4% faster in both runs while the 5.4 control moves 2% either way, so the time difference is at the edge of what this benchmark resolves. The allocation difference is not in doubt.

The Condition() change edits the same lines as #2870, so whichever merges second has a small conflict to resolve. Both deques can then be created together at the first NOT or !.

`Condition()` created a deque for leading `NOT` and `!` prefixes on every
call, though few conditions have one. An empty `ArrayDeque` allocates its
17-slot array up front, so parsing `performance.sql` allocated 585 KB per
parse for deques that stayed empty. The deque is now created at the first
`NOT` or `!`.
`KeyWord.from` runs for every `LIKE`-family condition the parser builds,
and `String.replaceAll` compiled its `\s+` pattern on each call. Parsing
`performance.sql` allocated 201 KB per parse for those compilations. The
pattern is now a precompiled constant.
…t use

`PrimaryExpression()` created two lists on every call: one for chained
signs such as `- -1` and one for JSON operators such as `->`. Nearly
every primary expression has neither, so both lists usually stayed
empty. They are now created at their first element.
`consumeXmlLeadingKeyword` and `FunctionArgumentList` called
`Function.getName()` for every function to check for `XMLPARSE`,
`XMLELEMENT` or `XMLFOREST`, and `getName()` joins the name parts into
a new string on each call. Both checks now compare the single name part.
@knutwannheden knutwannheden changed the title perf: drop an empty deque per condition and a regex compile per LIKE perf: trim per-parse allocations that do no work Oct 29, 2026
@manticore-projects
manticore-projects merged commit 4b983b2 into JSQLParser:master Oct 31, 2026
10 checks passed
@manticore-projects

Copy link
Copy Markdown
Contributor

Good work, thank you much for tracing and improving this!

knutwannheden added a commit to knutwannheden/JSqlParser that referenced this pull request Oct 31, 2026
Conflicts in JSqlParserCC.jjt, resolved by keeping master's logic and
re-adding the node links on top:

- PARTITION BY (JSQLParser#2878): the four sites call ComplexExpressionList()
  directly and still link the PARTITION BY node.
- PrimaryExpression (JSQLParser#2874): innerSigns is null without an inner sign,
  so the loop that wraps and links signed values is guarded.
- Condition (JSQLParser#2874): one lazily created deque of NOT tokens carries both
  the token for the node and whether it was spelled "!".
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.

2 participants