GROOVY-12398: Improve lexer and parser performance - #2921
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2921 +/- ##
==================================================
+ Coverage 71.1883% 71.1934% +0.0051%
- Complexity 37708 37729 +21
==================================================
Files 1584 1584
Lines 136493 136500 +7
Branches 25358 25361 +3
==================================================
+ Hits 97167 97179 +12
+ Misses 30556 30551 -5
Partials 8770 8770
🚀 New features to boost your workflow:
|
JMH summary — classic (commit
|
| Group | Speedup | Calibrated | n |
|---|---|---|---|
| bench | 0.964 × | 1.002 × | 124 |
| core | 1.170 × | 1.102 × | 108 |
| grails | 1.336 × | 1.069 × | 80 |
No benchmark is ≥1.5× slower than its 90-day baseline.
⚠️ Runner speed differs ≥15% from the historical baseline hardware for: core-hz, grails-ez. Raw speedups are not meaningful for those parts — use the calibrated column.
Runner calibration (this run vs baseline hardware): bench 0.97× (27 rulers) · core-ag 0.98× (3 rulers) · core-hz 1.23× (3 rulers) · grails-ad 1.07× (3 rulers) · grails-ez 1.42× (3 rulers)
Baseline: dev/bench/jmh/<part>/classic/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data
JMH summary — indy (commit
|
| Group | Speedup | Calibrated | n |
|---|---|---|---|
| bench | 1.072 × | 1.092 × | 124 |
| core | 4.953 × | 4.914 × | 108 |
| grails | 2.770 × | 2.938 × | 80 |
No benchmark is ≥1.5× slower than its 90-day baseline.
Runner calibration (this run vs baseline hardware): bench 1.00× (27 rulers) · core-ag 1.04× (3 rulers) · core-hz 0.96× (3 rulers) · grails-ad 0.96× (3 rulers) · grails-ez 0.93× (3 rulers)
Baseline: dev/bench/jmh/<part>/indy/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data
|
Initial AI read (my initial leaning is master only to start with and potentially backport to 6 after some settling time):
|
|
Thanks Paul — that's a much closer reading than mine was, and I agree with the recommendation. Master only for now; I would not put this in the 6.0 RC line. The 1.6× figure is parse-only, and a grammar/ATN change is the wrong kind of surprise during RC. A backport after some settling time (and a corpus AST sweep if we do go there) sounds right. On the three deltas:
Happy to drop or reword any of that if you would rather keep the old supplementary-plane classification for this PR and treat the code-point fix separately. |
e27a4da to
4c89f67
Compare
Parser performance verification: GROOVY-12398Question. Does commit Answer. Yes. On this host, with a frozen corpus of 245 files (570 533 characters), the target parser used about 35% less wall time than the baseline (pooled mean 1.53×). Every post-warmup sample of the target was faster than every post-warmup sample of the baseline. Parse outcomes (SLL success, LL fallback, the one expected failure) are identical. This is a parse-only measurement. It does not claim the same factor for end-to-end 1. What was compared
The range contains two commits:
Each commit was built in its own Git worktree ( 2. MethodCorpus
The tree was frozen from the baseline commit and reused for every run, so a change in source text cannot be mistaken for a parser change. The same two directories at the target commit hash to the same SHA-256 (39 + 206 files). Input is therefore identical in both trees.
Parse pathMatches
ANTLR runtime: Timing protocol
Host
This is a shared developer VM. Variance across baseline JVMs is reported rather than hidden. 3. Correctness (same corpus, both parsers)
LL fallback (both parsers):
Hard failure (both parsers): The speedup is not from fewer LL retries. The SLL/LL split is unchanged. 4. ResultsTimes are seconds to parse the whole corpus after warmup. Lower is better. Per JVM (15 timed rounds each)
A2 is a noisy baseline JVM (mean 0.300 s vs ~0.243 s for A and A3). It is kept in the pool rather than discarded. Pooled post-warmup samples
Speedup
Throughput at the pooled means: 941 → 1 439 files/s, 2.19e6 → 3.35e6 chars/s. SeparationThe slowest target round (0.211886 s) is still faster than the fastest baseline round (0.218082 s). The two sample sets do not overlap. Cohen’s d on the pooled samples is 3.1 (large). That is a description of this data set, not a claim about every machine. Conservative cross-checkAgainst the fastest baseline JVM mean (A3, 0.242742 s) and the slowest target JVM mean (B, 0.177364 s), the factor is still 1.37×. That is the least flattering pairing of JVM means in this run. 5. What this does and does not showShown
Not shown
Noise Baseline JVM A2 was ~24% slower than A/A3. The machine is not a quiet benchmark box. Interleaving and pooling are how that is handled; dropping A2 would only make the target look better, so it was not dropped. 6. ConclusionRelative to That is sufficient evidence for a master merge from a performance standpoint. It is not by itself a case for slipping the same grammar/ATN rewrite into a 6.0 RC without a further compile-level corpus check. Appendix: how to reproduce# worktrees
git worktree add --detach /tmp/parse-verify/wt-baseline 4aa7b94b83bd29814b1449cde1b239bdbe8c0ff0
git worktree add --detach /tmp/parse-verify/wt-target 4c89f6774d5cbb431940b0ca9d1581a9434b0d1e
( cd /tmp/parse-verify/wt-baseline && ./gradlew :jar --offline -q )
( cd /tmp/parse-verify/wt-target && ./gradlew :jar --offline -q )
# freeze corpus from baseline
mkdir -p /tmp/parse-verify/corpus
cp -a /tmp/parse-verify/wt-baseline/src/main/groovy /tmp/parse-verify/corpus/main-groovy
cp -a /tmp/parse-verify/wt-baseline/src/test-resources/core /tmp/parse-verify/corpus/test-resources-core
ANTLR=$HOME/.gradle/caches/modules-2/files-2.1/me.sunlan/antlr4-runtime/4.13.2.16/9c7caa836c70ef09ee7b70f77c11b803f75e19c0/antlr4-runtime-4.13.2.16.jar
# ParseBench.java as used for this report (5 warmup, 15 rounds, SLL then LL)
java -Xms1g -Xmx1g -XX:+AlwaysPreTouch -Dbench.label=... \
-cp ".:<raw-jar>:$ANTLR" ParseBench \
/tmp/parse-verify/corpus/main-groovy \
/tmp/parse-verify/corpus/test-resources-core |
This comment has been minimized.
This comment has been minimized.
Flatten number-literal fragments to character classes, split ASCII identifiers onto a predicate-free DFA path, replace regex-based lexer predicates with direct character tests, and gate annotation element values so AdaptivePredict does not also explore assignment expressions.
Decode surrogate pairs before isUpperCase so CapitalizedIdentifier is correct, report invalid octals at the token start, and cover the annotation pair-name set plus those edge cases with tests.
Compute FIRST(elementValuePairName) with LL1Analyzer at startup so identifier and keywords no longer need a parallel Java token list.
937cd49 to
b133ca5
Compare
The empty file was added by accident and fails Apache RAT (unapproved license). GROOVY-12400 already inlined that corpus into ParserNegativeSyntaxTest.
|



https://issues.apache.org/jira/browse/GROOVY-12398
Flatten number-literal fragments to character classes, split ASCII identifiers onto a predicate-free DFA path, replace regex-based lexer predicates with direct character tests, and gate annotation element values so AdaptivePredict does not also explore assignment expressions.
Local parse-only measurement (SLL then LL) of src/main/groovy + src/test-resources/core (245 files, approximately 570k characters):
Best run is about 1.6x faster. Mean improves more because prediction is more stable after DFA warmup.