Skip to content

GROOVY-12263: Invoke cached Closure doCall targets via MethodHandle - #2790

Open
daniellansun wants to merge 3 commits into
masterfrom
GROOVY-12263
Open

GROOVY-12263: Invoke cached Closure doCall targets via MethodHandle#2790
daniellansun wants to merge 3 commits into
masterfrom
GROOVY-12263

Conversation

@daniellansun

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/GROOVY-12263

Performance Verification Report

Subject. 1c3820bff71419b5c40e73142bbf3e82210ea317Invoke cached Closure doCall targets via MethodHandle

Baseline. 9bb195dee52e82518bb5f4e5cda9ddbf08c16d39 — immediate parent of the subject. The only production delta is src/main/java/groovy/lang/Closure.java. The accompanying unit-test file is not on the JMH hot path.

Verdict. The change delivers a statistically significant, host-stable, and path-specific improvement on the Java/GDK Closure.call entry that the commit claims to accelerate.

On the four GDK iteration benches that actually execute DefaultGroovyMethodsClosure.call(Object)call(Object...), wall-clock time falls by 13–21% (geometric mean 1.168×, about 4–6 ns per callback). Groovy invokedynamic call sites that already bind straight to doCall are unchanged (geomean 0.990×). A MethodClosure negative control is unchanged (0.994×). Host-calibration rulers sit at 0.998×, so the GDK movement is not a host-speed artifact.


1. What was compared

git diff --stat 9bb195dee5 1c3820bff7:

File Role
src/main/java/groovy/lang/Closure.java Production: cached doCall / call targets are invoked via MethodHandle.invokeExact instead of Method.invoke.
src/test/groovy/groovy/lang/ClosureCallHandleTest.groovy Tests only. Not loaded by the JMH measurement loops.

No other module, Gradle flag, or benchmark source differs between the two worktrees.

Isolated bytecode check (same class extracted from each JMH fat JAR):

Artifact Closure.class SHA-256 invokeExact count Method.invoke count
Baseline JAR 7426d0a0…5461d609 0 2
Target JAR 2475a3f1…3e76a198 6 1

The six invokeExact sites are the specialized 0–4-arity cases plus the defensive spreader. The remaining Method.invoke is the documented fallback when unreflect fails. The two JARs therefore implement the two dispatch strategies under test and nothing else.


2. Measurement protocol

The experiment is a paired, same-host, same-JVM, sequential comparison of two isolated builds. It is deliberately not a comparison against the 90-day gh-pages history: that history is dominated by runner-hardware noise (see subprojects/performance/README.adoc).

2.1 Isolation

  • Separate git worktrees at the two SHAs (/tmp/groovy-base-9bb195d, /tmp/groovy-mh-1c3820b).
  • Each worktree built with ./gradlew :performance:jmhJar --offline.
  • JMH executed against that worktree’s own fat JAR (performance-6.0.0-SNAPSHOT-jmh.jar).
  • Runs were serial, not concurrent, so they did not contend for the six vCPUs.

2.2 JMH configuration

These flags override the @Fork(2) annotations on the benchmark classes.

Parameter Value Rationale
Benchmarks org.apache.groovy.perf.ClosureBench (19 methods) + org.apache.groovy.perf.HostCalibrationBench (3 rulers) The project’s own closure suite plus the core-hz hardware rulers
Mode / unit AverageTime, as declared by the benches (ms/op for ClosureBench, us/op for rulers) Lower is better
Forks 4 independent JVMs Between-fork variance is visible; one noisy fork cannot dominate
Warmup 4 × 2 s Past the C1/C2 transition on these loops
Measurement 5 × 2 s 20 samples per bench (4 × 5)
Heap -Xms2g -Xmx2g -XX:+AlwaysPreTouch Removes heap-resize and first-touch noise
Confidence JMH default 99.9% CI Primary significance criterion: non-overlapping CIs
Order Baseline first (23:49–00:19), target second (00:19–00:48), same host Calibration rulers quantify any thermal or neighbor drift

2.3 Host

Item Value
Host hera
CPU AMD EPYC 7763, 6 vCPUs, 1 thread/core
Memory 23 GiB
OS Linux 6.15.5 x86_64
JDK Amazon Corretto 25.0.2+10-LTS (25.0.2-amzn)
Frequency governor not exposed (cpufreq n/a)

2.4 How “faster” is defined

For AverageTime, speedup = baseline / target. Values greater than 1 mean the target is faster. A result is labeled faster or slower only when the two 99.9% CIs do not overlap; otherwise inconclusive.

A Welch two-sample t on the 20 raw iteration samples is reported as a secondary check (t, Welch–Satterthwaite df). Two-sided p-values are not tabulated: SciPy is not installed on this host, and every GDK t exceeds 11 on df > 22, which is p ≪ 0.001 under any reasonable tail model.


3. Path analysis: which benches must move

The production change lives only in Closure.call(Object...). A bench can improve only if its steady-state work actually enters that method.

Generated closures declare doCall(...) and do not override call(Object). Two distinct caller shapes then arise:

  1. Java / GDK entry. DefaultGroovyMethods.each / collect / findAll / inject compile as Java closure.call(item) (or call(acc, val)). That resolves to Closure.call(Object) / call(Object, Object), which wrap into call(Object...). This is the path named in the commit comment (the each / collect hot path). Primary treatment.

  2. Groovy invokedynamic entry. c(i) and c.call(i) in ClosureBench compile to the same indy site, invoke:(Lgroovy/lang/Closure;I). After warmup the site binds directly to doCall(Object) and never enters Closure.call(Object...). Must not move. Confirmed by disassembly of ClosureBench.closureCallMethod and closureReuse in the target JAR, and by the generated class ClosureBench$_closureCallMethod_closure14 exposing only doCall / doCall().

  3. Adapters that re-enter a generated closure. CurriedClosure and MethodClosure are explicitly CallOverride.NONE. ComposedClosure.doCall(Object[]) is array-typed and likewise uncached. Their outer call stays on the metaclass. The inner generated bodies, however, are invoked via call(...) after uncurry or composition, so they can pick up the MethodHandle path. These are secondary / indirect, not clean negatives.

  4. True negative control. list.&size is a MethodClosure: CallOverride.lookup returns NONE, and there is no generated doCall body to re-enter. Must not move.

  5. Hardware rulers. HostCalibrationBench.{cpuIntegerOps, memoryPointerChase, allocationChurn} are pure Java and Groovy-independent. Their geometric mean is the calibration factor. A factor near 1 means the two 29-minute windows ran at equivalent host speed.

                    Groovy indy  ──►  doCall            (ClosureBench.closureReuse / .closureCallMethod)
                                         ▲
Java/GDK call(Object)                    │
    └─► Closure.call(Object...) ──► Method.invoke   [baseline]
                                └─► invokeExact     [target]   ◄── this commit
                                         │
Curried / Composed outer ──► metaclass ──┘ (re-enters inner call(...))
MethodClosure            ──► metaclass, no generated doCall

4. Results

4.1 Hardware calibration (must be ~1.0×)

Ruler Baseline Target Speedup 99.9% CIs Verdict
cpuIntegerOps 407.502 ± 1.914 µs/op 405.945 ± 1.898 µs/op 1.004× overlap inconclusive
memoryPointerChase 1419.758 ± 52.048 µs/op 1451.855 ± 19.503 µs/op 0.978× overlap inconclusive
allocationChurn 96.799 ± 4.399 µs/op 95.504 ± 3.902 µs/op 1.014× overlap inconclusive
Geomean 0.998× no host drift

The second window is 0.2% slower on the geometric mean of the rulers — well inside JMH noise, and in the opposite direction of the GDK result. A 17% GDK movement cannot be attributed to the machine speeding up.

4.2 Primary treatment — GDK Java callbacks (must improve if the claim is true)

Each of these methods performs 1 000 000 closure invocations per JMH op (ITERATIONS/10 outer loops × a 10-element list, or 2-arg inject over the same list). Scores are therefore also nanoseconds per callback, including iterator and DGM overhead.

Benchmark Baseline (ms/op) Target (ms/op) Speedup Δ per call 99.9% CIs Welch t (df) Verdict
eachWithClosure 34.438 ± 0.666 29.404 ± 0.385 1.171× −5.03 ns disjoint 25.41 (30.4) faster
collectWithClosure 36.576 ± 1.598 31.454 ± 0.481 1.163× −5.12 ns disjoint 11.92 (22.4) faster
findAllWithClosure 34.202 ± 0.986 30.313 ± 0.906 1.128× −3.89 ns disjoint 11.28 (37.7) faster
injectWithClosure 34.052 ± 1.019 28.095 ± 0.677 1.212× −5.96 ns disjoint 18.90 (33.0) faster
Geomean 1.168× ≈ −5.0 ns all four faster

Per-fork means (ms/op) — every fork of every GDK bench moves in the same direction; this is not a single lucky fork:

Bench Baseline forks Target forks
each 34.28, 34.05, 34.51, 34.92 29.12, 29.54, 29.54, 29.42
collect 36.04, 37.11, 35.90, 37.25 31.67, 31.37, 31.44, 31.34
findAll 34.46, 34.72, 33.66, 33.97 30.34, 30.65, 30.24, 30.02
inject 34.41, 34.46, 33.62, 33.73 27.47, 28.61, 28.18, 28.12

Relative CI half-width stays in the 1.3–4.4% band on both sides; the target is if anything tighter. findAll saves a little less (~3.9 ns) than each / collect / inject (~5–6 ns), which is consistent with BooleanClosureWrapper adding a fixed cost that the MethodHandle change does not touch.

Reading the 5 ns. A Groovy-indy doCall of { it * 2 } is ~2.2 ns in the same process (closureCallMethod). The GDK benches spend ~34 ns per callback, of which iterator + DGM + call(Object) array wrap + doCall body account for the rest. Removing Method.invoke’s reflective wrapper from that mix and replacing it with invokeExact is expected to save a handful of nanoseconds, not tens. The measured −5 ns/call matches that model. It is not a 17% reduction inside doCall itself; it is a 17% reduction in the GDK callback round-trip, which is exactly the surface the commit optimizes.

4.3 Groovy indy sites (must not improve)

Benchmark Baseline Target Speedup Verdict
closureCallMethod 2.202 ± 0.094 2.200 ± 0.073 1.001× inconclusive
closureReuse 2.220 ± 0.105 2.182 ± 0.059 1.017× inconclusive
closureWithCapture 2.601 ± 0.062 2.580 ± 0.067 1.008× inconclusive
closureModifyCapture 4.813 ± 0.168 4.862 ± 0.184 0.990× inconclusive
closureMultiParams 14.942 ± 0.316 14.855 ± 0.443 1.006× inconclusive
simpleClosureCreation 29.528 ± 0.691 29.288 ± 0.807 1.008× inconclusive
closureAsParameter 28.606 ± 2.925 29.563 ± 3.470 0.968× inconclusive
closureDelegation 50.361 ± 1.603 46.799 ± 2.949 1.076× inconclusive
nestedClosures 33.478 ± 5.876 39.171 ± 7.665 0.855× inconclusive
Geomean 0.990× flat

nestedClosures and closureDelegation have wide CIs (inner allocation and property-dispatch work dominate) and still overlap. The near-2.2 ns closureCallMethod / closureReuse pair is identical to 0.1%. This group is the specificity check: if the whole JVM had simply gotten faster, these would have moved with the GDK benches. They did not.

4.4 Adapters and the true negative control

Benchmark Path Baseline Target Speedup Verdict
methodReference MethodClosureNONE 87.927 ± 3.022 88.473 ± 3.352 0.994× inconclusive
curriedClosure outer NONE, inner re-enters call 24.920 ± 0.757 20.975 ± 0.936 1.188× faster
rightCurriedClosure same 25.006 ± 0.564 20.072 ± 0.690 1.246× faster
closureComposition doCall(Object[]) uncached; inners via call 39.456 ± 0.952 33.935 ± 1.119 1.163× faster

methodReference is the clean negative and does not move (t = −0.47). Curry, rcurry, and compose do move, in the same 16–25% band as the GDK group. That is expected once the inner generated closures are taken into account: CurriedClosure is excluded from the cache so that MetaClassImpl can uncurry and re-enter, and that re-entry is a call(...) on a generated closure. Treating them as proof of a general, unspecified speedup would be wrong; treating them as a contradiction would also be wrong. They are a consistent secondary effect.

4.5 Context benches (not a test of this commit)

Benchmark Baseline Target Speedup Verdict
closureTrampoline 44.738 ± 2.324 44.550 ± 2.327 1.004× inconclusive
closureSpread 1786.532 ± 64.833 1832.367 ± 94.159 0.975× inconclusive

Trampoline is dominated by TrampolineClosure machinery. Spread (sum3(*args)) is dominated by argument packing. CIs overlap.


5. Why this is the MethodHandle change, not a confound

Alternative explanation Why it is rejected
Host sped up between the two 29-minute windows Calibration geomean 0.998×; integer-ruler CIs overlap; the 0.4% cpuIntegerOps tick is smaller than, and opposite in direction to, a 17% GDK claim.
Different sources, flags, or dependency versions git diff is two files; both JARs built --offline from worktrees pinned at the two SHAs; Closure.class hashes and invokeExact counts match the intended implementations.
JIT / fork noise 4 forks; every GDK fork moves the same way; 20 samples; 99.9% CIs disjoint; Welch t ∈ [11, 25].
“Everything that uses a closure got faster” Indy doCall sites and MethodClosure did not move. The improvement is confined to callers that enter Closure.call(Object...).
The test-file change affected the benches ClosureCallHandleTest is not referenced from ClosureBench.
Packed closures hiding the path Packing is opt-in (@PackedClosures / groovy.target.closure.pack). ClosureBench is compiled without it; the generated classes extend Closure and implement GeneratedClosure with doCall only.

6. What this report does not claim

  • It does not claim that Groovy dynamic dispatch in general is 17% faster. Groovy-indy c(i) was already ~2.2 ns/call and stays there.
  • It does not claim a 17% reduction inside doCall. The ~5 ns is the reflective-invoke overhead disappearing from the Java call wrapper around doCall.
  • It does not compare against the gh-pages 90-day dashboard. That comparison is hardware-dominated; this one is a same-host A/B of two commits.
  • It does not measure cold start, allocation (-prof gc was not attached, to keep the timing run clean), or packed-closure dispatch (PackedClosure overrides call and never uses this cache).
  • Frequency-governor data is unavailable on this VM; calibration rulers are the substitute.

7. Conclusion

On a same-host, 4-fork, 99.9%-CI JMH comparison of the isolated parent and the MethodHandle commit:

  1. The advertised path is faster. GDK each / collect / findAll / inject improve by 1.13–1.21× (geomean 1.168×, −4 to −6 ns per callback). All four 99.9% CIs are disjoint; all four fork sets move in the same direction.
  2. The improvement is specific. Groovy-indy sites that bind to doCall (geomean 0.990×) and MethodClosure (0.994×) do not move.
  3. The host did not move. Calibration geomean 0.998×.
  4. Secondary adapters behave as the architecture predicts. Curry and compose improve because they re-enter generated call(...); they are not counterexamples.

The performance claim of 1c3820b is therefore confirmed for the Java/GDK Closure.call hot path, at a magnitude that matches the cost of Method.invoke being removed, and with no detectable regression on the paths the change is designed to leave alone.


Appendix A — Reproducing this run

git worktree add /tmp/groovy-base-9bb195d 9bb195dee52e82518bb5f4e5cda9ddbf08c16d39
git worktree add /tmp/groovy-mh-1c3820b   1c3820bff71419b5c40e73142bbf3e82210ea317

(cd /tmp/groovy-base-9bb195d && ./gradlew :performance:jmhJar --offline)
(cd /tmp/groovy-mh-1c3820b   && ./gradlew :performance:jmhJar --offline)

java -jar /tmp/groovy-base-9bb195d/subprojects/performance/build/libs/performance-6.0.0-SNAPSHOT-jmh.jar \
  org.apache.groovy.perf.ClosureBench org.apache.groovy.perf.HostCalibrationBench \
  -f 4 -wi 4 -i 5 -w 2s -r 2s -rf json -foe true \
  -jvmArgsAppend '-Xms2g -Xmx2g -XX:+AlwaysPreTouch' \
  -rff jmh-base.json -o jmh-base.txt

java -jar /tmp/groovy-mh-1c3820b/subprojects/performance/build/libs/performance-6.0.0-SNAPSHOT-jmh.jar \
  org.apache.groovy.perf.ClosureBench org.apache.groovy.perf.HostCalibrationBench \
  -f 4 -wi 4 -i 5 -w 2s -r 2s -rf json -foe true \
  -jvmArgsAppend '-Xms2g -Xmx2g -XX:+AlwaysPreTouch' \
  -rff jmh-target.json -o jmh-target.txt

Raw JMH JSON from this run: /tmp/grok-1000/jmh-base.json, /tmp/grok-1000/jmh-target.json.

Appendix B — Artifact hashes

Item Value
Baseline SHA 9bb195dee52e82518bb5f4e5cda9ddbf08c16d39
Target SHA 1c3820bff71419b5c40e73142bbf3e82210ea317
Baseline JMH JAR SHA-256 e321d268c7e099519e3f9e8b1b38d9bba5e11bf8151ee928f4a12156d4e40016
Target JMH JAR SHA-256 4cd0b63d1ca9a7f9cb383bae2b594adfc3cb079790260d3415aa2308d5f02b17
Baseline window 2026-08-15 23:49:53 – 2026-08-16 00:19:05 +09
Target window 2026-08-16 00:19:15 – 2026-08-16 00:48:23 +09

@codecov-commenter

codecov-commenter commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.13861% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.1436%. Comparing base (142130d) to head (9b75750).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
src/main/java/groovy/lang/Closure.java 86.1386% 8 Missing and 6 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2790        +/-   ##
==================================================
+ Coverage     70.1064%   70.1436%   +0.0371%     
- Complexity      35774      35808        +34     
==================================================
  Files            1561       1562         +1     
  Lines          132373     132484       +111     
  Branches        24332      24365        +33     
==================================================
+ Hits            92802      92929       +127     
+ Misses          31170      31154        -16     
  Partials         8401       8401                
Files with missing lines Coverage Δ
src/main/java/groovy/lang/Closure.java 84.7875% <86.1386%> (+5.1731%) ⬆️

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

JMH summary — classic (commit 76bba6a)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 0.989 × 1.015 × 99
core 1.141 × 1.032 × 83
grails 1.008 × 0.870 × 80

⚠️ 7 benchmarks at least 1.5× slower than the 90-day baseline:

  • org.apache.groovy.perf.grails.CategoryBench.categoryShadowingExistingMethod — 3.08× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.nestedCategories — 2.68× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.categoryInLoop — 2.66× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.categoryWithOutsideCalls — 2.61× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.multipleCategoriesSimultaneous — 2.44× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.nestedCategoryOuterWrapping — 2.37× slower (calibrated)
  • org.apache.groovy.perf.grails.CategoryBench.threeCategoriesSimultaneous — 1.95× slower (calibrated)

⚠️ Runner speed differs ≥15% from the historical baseline hardware for: grails-ad. Raw speedups are not meaningful for those parts — use the calibrated column.

Runner calibration (this run vs baseline hardware): bench 0.98× (26 rulers) · core-ag 1.14× (3 rulers) · core-hz 1.07× (3 rulers) · grails-ad 1.43× (3 rulers) · grails-ez 0.98× (3 rulers)

Baseline: dev/bench/jmh/<part>/classic/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

JMH summary — indy (commit 76bba6a)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 0.973 × 0.998 × 99
core 2.762 × 2.670 × 83
grails 5.276 × 3.942 × 80

No benchmark is ≥1.5× slower than its 90-day baseline.

⚠️ Runner speed differs ≥15% from the historical baseline hardware for: grails-ad, grails-ez. Raw speedups are not meaningful for those parts — use the calibrated column.

Runner calibration (this run vs baseline hardware): bench 0.97× (26 rulers) · core-ag 1.12× (3 rulers) · core-hz 0.93× (3 rulers) · grails-ad 1.26× (3 rulers) · grails-ez 1.41× (3 rulers)

Baseline: dev/bench/jmh/<part>/indy/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

Comment thread src/main/java/groovy/lang/Closure.java Outdated
case 4:
return handle.invokeExact((Object) self, arguments[0], arguments[1], arguments[2], arguments[3]);
default:
return handle.asSpreader(Object[].class, arguments.length).invokeExact((Object) self, arguments);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think you have no advantage of invokeExact in this case, unless you cache the handle resulting from asSpreader. If not cached I would use invokeWithArguments here instead. I doubt it is slower for this case, but it sure is better readable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — thank you.

invokeExact only helps when the call site sees a stable MethodType. A per-call asSpreader allocates a new handle, so that invokeExact was just a more expensive invoke, and less readable than invokeWithArguments.

We took the other half of the same advice: the spreader is now built once in CallOverride.unreflect and stored as (Object, Object[])Object (MethodType.genericMethodType(1, true)). invokeHandle's default is then:

return handle.invokeExact((Object) self, arguments);

That is a cached spreader, so invokeExact is the type-correct call. We did not use handle.invokeWithArguments(self, arguments): that is a real varargs method and would pack as length 2.

The specialised 0..4 invokeExact switch is unchanged. That remains the GDK each / collect / inject path.

if (byArity[arity] != null) {
handles[arity] = unreflect(byArity[arity]);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the cases over arity limit could be created here as well, but with taking Object[]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and that is now in place.

ARITY_LIMIT is only the specialised-invokeExact cutoff (0..4). doCall methods with arity >= 5 are no longer skipped. Each unambiguous, non-array target is stored in a small side table (SpreadSlot[]), keyed by exact parameter count, as a lookup-time asSpreader of type (Object, Object[])Object.

We kept that table off the handles[0..4] array on purpose:

  • mixing the two handle shapes in one array would WrongMethodTypeException if the wrong invokeExact form were used;
  • a single catch-all (Object, Object[]) slot cannot represent both a 5-arg and a 6-arg doCall (asSpreader is arity-specific);
  • array-typed / varargs doCall still belongs to the MOP (hasArray skip), as before.

Selection, GROOVY-12164 guards, ambiguity, MethodClosure / CurriedClosureNONE, and the mopUnperturbed gate are the same rules as for 0..4. High-arity is always a doCall body, so it does not use the GROOVY-11911 re-entry latch.

The 0..4 branch in call(Object...) (arity < ARITY_LIMIT) is unchanged, so the measured GDK path is not on this table.

Build asSpreader handles once at CallOverride lookup for arity >= 5,
and invokeExact the cached (Object, Object[])Object shape. Leave the
specialised 0-4 invokeExact switch on the GDK each/collect path unchanged.
@daniellansun
daniellansun requested a review from blackdrag August 16, 2026 03:46
@sonarqubecloud

Copy link
Copy Markdown

@testlens-app

testlens-app Bot commented Aug 16, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 9b75750
▶️ Tests: 110391 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app.

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