[ 
https://issues.apache.org/jira/browse/GROOVY-12234?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12234:
-------------------------------
    Description: 
Dynamic Groovy 5 cannot run in a GraalVM native image. Since indy became the 
sole compilation target (Groovy 3+ default, Groovy 5+ only option without the 
opt-in {{groovy-callsite}} module), every dynamic method call, property access, 
and operator links through {{IndyInterface.bootstrap}} — and that linkage dies 
inside an image. The current workaround is to compile the application *and 
every Groovy dependency* with {{-Dgroovy.target.indy=false}} plus 
{{groovy-callsite}}, which is impossible for jars you do not build — including 
the Groovy distribution's own Groovy-compiled modules (e.g. 
{{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash 
the moment a dependency crosses one dynamic site:

{noformat}
java.lang.BootstrapMethodError: ...
com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, 
MethodHandle)
{noformat}

A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the wall 
is narrow. Everything Groovy's indy runtime composes works natively — 
{{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}}, 
{{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint 
creation and guarding, {{MutableCallSite}} creation and dispatch, 
{{ConstantCallSite}}. Exactly two primitives fail, and they are the same 
operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}} 
and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps 
dispatching through the stale target, so catch-and-continue is semantically 
wrong — mutation cannot be papered over; it has to be designed out.

h2. Key insight

In Groovy's indy design those two primitives only ever *install* or *invalidate 
caches*. The dispatch semantics live entirely in method selection 
({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never 
retargets is slower, never wrong. The runtime already carries most of the 
data-side machinery an AOT mode needs: the per-site LRU PIC, {{defaultTarget}} 
(the cache-consulting path), and the reflective cold tier (GROOVY-12137).

h2. The fix: AOT link mode

Under AOT — 
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}}, 
or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is 
testable on a plain JVM — the bootstrap links each site *once, permanently*:

* The site becomes a {{ConstantCallSite}} whose target is one bound handle into 
{{aotDispatch}}: PIC lookup (a new allocation-free 
{{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in 
ordinary compiled Java. The {{CacheableCallSite}} is never installed as the 
call site — it is the state carrier (PIC, fallback target) — and its 
{{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM under 
the knob.
* Cache freshness moves from SwitchPoint guards (which can never fire natively) 
to a global invalidation stamp ({{AotDispatch}}): every invalidation path — all 
three {{SwitchPoint.invalidateAll}} call sites now funnel through 
{{AotDispatch.invalidateAll}} — advances the stamp; each cached 
{{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC hit is a 
miss and re-selects. Coarser than GROOVY-12191's scoped invalidation, which is 
safe: over-invalidation re-selects, staleness is impossible.
* The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion 
to full method-handle chains is gated off, since chains run in the native MH 
interpreter while reflective dispatch uses AOT-compiled invocation stubs.

The JVM path is unchanged: the mode is decided per link and never cached in 
statics (native image may initialize classes at build time, where the imagecode 
property reports {{buildtime}} — caching would bake the wrong answer into the 
image heap); hot paths read a site-local flag captured at link time; the stamp 
is written but never read outside AOT mode.

*GraalVM bug found en route:* the native-image runtime invokedynamic path 
invokes a bootstrap method *without running its declaring class's <clinit>* — 
{{IndyInterface.bootstrap}} executed against null static finals. An ordinary 
cross-class {{getstatic}} carries the missing initialization barrier (and 
{{Class.forName(initialize=true)}} does not recover), so every BSM entry calls 
{{ensureInitialized()}}, which triggers initialization through a helper class's 
field read. Minimal repro available; to be filed upstream alongside the narrow 
retargeting ask (runtime-created sites are never constant-folded into AOT code, 
so {{setTarget}} on them is morally a volatile store).

h2. What this enables

*Every already-published indy-compiled jar becomes native-capable without 
recompilation* — the BSM reference in existing class files is 
{{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the 
runtime side. That includes the distribution's own modules: the 
{{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.

h2. Verification

||check||result||
|20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic 
dispatch through one site, ExpandoMetaClass change observed by an already-hot 
site, per-instance metaclass, category enter/leave with both transitions, 
operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in a 
native image built from stock-indy class files with only agent-recorded 
metadata* — no extra flags|
|category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0 failures|
|zero retargets in AOT mode|enforced by the fail-fast {{setTarget}}, exercised 
across the gauntlet under the knob|
|real-world: the fully dynamic 
[GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server, 
unmodified, stock indy compilation|builds and serves all four tools natively, 
including a live HTTPS fetch through dynamically-compiled 
{{groovy-http-builder}}|

Measured against the same server hand-converted to {{@CompileStatic}} + 
indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):

||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
|ready after spawn|434 ms|23 ms|24–27 ms|
|get_policy|115 ms|6 ms|8–14 ms|
|search_policies|79 ms|8 ms|242 ms|
|refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
|binary size| |66.0 MB|55.8 MB|

The dynamic image is the first of the two with all four tools working: the 
static conversion's network path still crossed indy sites in the distribution 
jars.

h2. Performance model and known limitation

Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns, 
but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry 
interpreter cost, independent of chain depth, adapter count, or {{invokeExact}} 
vs {{invoke}}. Each dynamic call site's invokedynamic hop into its 
runtime-linked target pays it once per call, so a workload crossing N dynamic 
sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops per action ≈ 
4 ms (invisible for CLI/script/MCP workloads); hot loops crossing tens of 
thousands of sites show it plainly (search_policies above: ~60k ops ≈ 242 ms, 
matching the model).

This floor is structural (the indy-to-runtime-target boundary) and not fixable 
from Groovy's side; the escapes are {{@CompileStatic}} for hot code (statically 
compiled code emits no per-operation indy sites and is unaffected), a 
build-time rewrite of indy sites to {{invokestatic aotDispatch}} (removes the 
runtime-MH boundary entirely; the shallow dispatcher is deliberately shaped as 
its target), and upstream GraalVM work on compiling runtime-linked code 
(Ristretto/Project Crema).

h2. Scope and follow-ups (not this ticket)

* Experimental, spike-quality; proposed behind its current automatic detection 
with the diagnostic knob for CI.
* Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until 
GraalVM's dynamic class loading matures; runtime proxy generation for abstract 
classes likewise.
* Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter 
already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}} 
site-crossing counter so users can measure which zone they are in; the de-indy 
build-step rewrite tool; the two upstream GraalVM filings; shipping Groovy's 
own reachability metadata so the agent step shrinks.

Prototype branch with three commits available; happy to split delivery into the 
invalidation funnel, the link mode, and the dispatcher if that eases review.


  was:
Dynamic Groovy cannot run in a GraalVM native image. Since indy became the sole 
compilation target (Groovy 3+ default, Groovy 5+ only option without the opt-in 
{{groovy-callsite}} module), every dynamic method call, property access, and 
operator links through {{IndyInterface.bootstrap}} — and that linkage dies 
inside an image. The current workaround is to compile the application *and 
every Groovy dependency* with {{-Dgroovy.target.indy=false}} plus 
{{groovy-callsite}}, which is impossible for jars you do not build — including 
the Groovy distribution's own Groovy-compiled modules (e.g. 
{{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash 
the moment a dependency crosses one dynamic site:

{noformat}
java.lang.BootstrapMethodError: ...
com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, 
MethodHandle)
{noformat}

A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the wall 
is narrow. Everything Groovy's indy runtime composes works natively — 
{{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}}, 
{{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint 
creation and guarding, {{MutableCallSite}} creation and dispatch, 
{{ConstantCallSite}}. Exactly two primitives fail, and they are the same 
operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}} 
and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps 
dispatching through the stale target, so catch-and-continue is semantically 
wrong — mutation cannot be papered over; it has to be designed out.

h2. Key insight

In Groovy's indy design those two primitives only ever *install* or *invalidate 
caches*. The dispatch semantics live entirely in method selection 
({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never 
retargets is slower, never wrong. The runtime already carries most of the 
data-side machinery an AOT mode needs: the per-site LRU PIC, {{defaultTarget}} 
(the cache-consulting path), and the reflective cold tier (GROOVY-12137).

h2. The fix: AOT link mode

Under AOT — 
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}}, 
or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is 
testable on a plain JVM — the bootstrap links each site *once, permanently*:

* The site becomes a {{ConstantCallSite}} whose target is one bound handle into 
{{aotDispatch}}: PIC lookup (a new allocation-free 
{{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in 
ordinary compiled Java. The {{CacheableCallSite}} is never installed as the 
call site — it is the state carrier (PIC, fallback target) — and its 
{{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM under 
the knob.
* Cache freshness moves from SwitchPoint guards (which can never fire natively) 
to a global invalidation stamp ({{AotDispatch}}): every invalidation path — all 
three {{SwitchPoint.invalidateAll}} call sites now funnel through 
{{AotDispatch.invalidateAll}} — advances the stamp; each cached 
{{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC hit is a 
miss and re-selects. Coarser than GROOVY-12191's scoped invalidation, which is 
safe: over-invalidation re-selects, staleness is impossible.
* The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion 
to full method-handle chains is gated off, since chains run in the native MH 
interpreter while reflective dispatch uses AOT-compiled invocation stubs.

The JVM path is unchanged: the mode is decided per link and never cached in 
statics (native image may initialize classes at build time, where the imagecode 
property reports {{buildtime}} — caching would bake the wrong answer into the 
image heap); hot paths read a site-local flag captured at link time; the stamp 
is written but never read outside AOT mode.

*GraalVM bug found en route:* the native-image runtime invokedynamic path 
invokes a bootstrap method *without running its declaring class's <clinit>* — 
{{IndyInterface.bootstrap}} executed against null static finals. An ordinary 
cross-class {{getstatic}} carries the missing initialization barrier (and 
{{Class.forName(initialize=true)}} does not recover), so every BSM entry calls 
{{ensureInitialized()}}, which triggers initialization through a helper class's 
field read. Minimal repro available; to be filed upstream alongside the narrow 
retargeting ask (runtime-created sites are never constant-folded into AOT code, 
so {{setTarget}} on them is morally a volatile store).

h2. What this enables

*Every already-published indy-compiled jar becomes native-capable without 
recompilation* — the BSM reference in existing class files is 
{{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the 
runtime side. That includes the distribution's own modules: the 
{{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.

h2. Verification

||check||result||
|20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic 
dispatch through one site, ExpandoMetaClass change observed by an already-hot 
site, per-instance metaclass, category enter/leave with both transitions, 
operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in a 
native image built from stock-indy class files with only agent-recorded 
metadata* — no extra flags|
|category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0 failures|
|zero retargets in AOT mode|enforced by the fail-fast {{setTarget}}, exercised 
across the gauntlet under the knob|
|real-world: the fully dynamic 
[GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server, 
unmodified, stock indy compilation|builds and serves all four tools natively, 
including a live HTTPS fetch through dynamically-compiled 
{{groovy-http-builder}}|

Measured against the same server hand-converted to {{@CompileStatic}} + 
indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):

||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
|ready after spawn|434 ms|23 ms|24–27 ms|
|get_policy|115 ms|6 ms|8–14 ms|
|search_policies|79 ms|8 ms|242 ms|
|refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
|binary size| |66.0 MB|55.8 MB|

The dynamic image is the first of the two with all four tools working: the 
static conversion's network path still crossed indy sites in the distribution 
jars.

h2. Performance model and known limitation

Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns, 
but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry 
interpreter cost, independent of chain depth, adapter count, or {{invokeExact}} 
vs {{invoke}}. Each dynamic call site's invokedynamic hop into its 
runtime-linked target pays it once per call, so a workload crossing N dynamic 
sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops per action ≈ 
4 ms (invisible for CLI/script/MCP workloads); hot loops crossing tens of 
thousands of sites show it plainly (search_policies above: ~60k ops ≈ 242 ms, 
matching the model).

This floor is structural (the indy-to-runtime-target boundary) and not fixable 
from Groovy's side; the escapes are {{@CompileStatic}} for hot code (statically 
compiled code emits no per-operation indy sites and is unaffected), a 
build-time rewrite of indy sites to {{invokestatic aotDispatch}} (removes the 
runtime-MH boundary entirely; the shallow dispatcher is deliberately shaped as 
its target), and upstream GraalVM work on compiling runtime-linked code 
(Ristretto/Project Crema).

h2. Scope and follow-ups (not this ticket)

* Experimental, spike-quality; proposed behind its current automatic detection 
with the diagnostic knob for CI.
* Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until 
GraalVM's dynamic class loading matures; runtime proxy generation for abstract 
classes likewise.
* Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter 
already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}} 
site-crossing counter so users can measure which zone they are in; the de-indy 
build-step rewrite tool; the two upstream GraalVM filings; shipping Groovy's 
own reachability metadata so the agent step shrinks.

Prototype branch with three commits available; happy to split delivery into the 
invalidation funnel, the link mode, and the dispatcher if that eases review.



> Indy: AOT link mode so dynamic Groovy dispatch works in GraalVM native images
> -----------------------------------------------------------------------------
>
>                 Key: GROOVY-12234
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12234
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> Dynamic Groovy 5 cannot run in a GraalVM native image. Since indy became the 
> sole compilation target (Groovy 3+ default, Groovy 5+ only option without the 
> opt-in {{groovy-callsite}} module), every dynamic method call, property 
> access, and operator links through {{IndyInterface.bootstrap}} — and that 
> linkage dies inside an image. The current workaround is to compile the 
> application *and every Groovy dependency* with {{-Dgroovy.target.indy=false}} 
> plus {{groovy-callsite}}, which is impossible for jars you do not build — 
> including the Groovy distribution's own Groovy-compiled modules (e.g. 
> {{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash 
> the moment a dependency crosses one dynamic site:
> {noformat}
> java.lang.BootstrapMethodError: ...
> com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
> java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite, 
> MethodHandle)
> {noformat}
> A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the 
> wall is narrow. Everything Groovy's indy runtime composes works natively — 
> {{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}}, 
> {{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint 
> creation and guarding, {{MutableCallSite}} creation and dispatch, 
> {{ConstantCallSite}}. Exactly two primitives fail, and they are the same 
> operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}} 
> and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps 
> dispatching through the stale target, so catch-and-continue is semantically 
> wrong — mutation cannot be papered over; it has to be designed out.
> h2. Key insight
> In Groovy's indy design those two primitives only ever *install* or 
> *invalidate caches*. The dispatch semantics live entirely in method selection 
> ({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never 
> retargets is slower, never wrong. The runtime already carries most of the 
> data-side machinery an AOT mode needs: the per-site LRU PIC, 
> {{defaultTarget}} (the cache-consulting path), and the reflective cold tier 
> (GROOVY-12137).
> h2. The fix: AOT link mode
> Under AOT — 
> {{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}},
>  or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is 
> testable on a plain JVM — the bootstrap links each site *once, permanently*:
> * The site becomes a {{ConstantCallSite}} whose target is one bound handle 
> into {{aotDispatch}}: PIC lookup (a new allocation-free 
> {{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in 
> ordinary compiled Java. The {{CacheableCallSite}} is never installed as the 
> call site — it is the state carrier (PIC, fallback target) — and its 
> {{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM 
> under the knob.
> * Cache freshness moves from SwitchPoint guards (which can never fire 
> natively) to a global invalidation stamp ({{AotDispatch}}): every 
> invalidation path — all three {{SwitchPoint.invalidateAll}} call sites now 
> funnel through {{AotDispatch.invalidateAll}} — advances the stamp; each 
> cached {{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC 
> hit is a miss and re-selects. Coarser than GROOVY-12191's scoped 
> invalidation, which is safe: over-invalidation re-selects, staleness is 
> impossible.
> * The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion 
> to full method-handle chains is gated off, since chains run in the native MH 
> interpreter while reflective dispatch uses AOT-compiled invocation stubs.
> The JVM path is unchanged: the mode is decided per link and never cached in 
> statics (native image may initialize classes at build time, where the 
> imagecode property reports {{buildtime}} — caching would bake the wrong 
> answer into the image heap); hot paths read a site-local flag captured at 
> link time; the stamp is written but never read outside AOT mode.
> *GraalVM bug found en route:* the native-image runtime invokedynamic path 
> invokes a bootstrap method *without running its declaring class's <clinit>* — 
> {{IndyInterface.bootstrap}} executed against null static finals. An ordinary 
> cross-class {{getstatic}} carries the missing initialization barrier (and 
> {{Class.forName(initialize=true)}} does not recover), so every BSM entry 
> calls {{ensureInitialized()}}, which triggers initialization through a helper 
> class's field read. Minimal repro available; to be filed upstream alongside 
> the narrow retargeting ask (runtime-created sites are never constant-folded 
> into AOT code, so {{setTarget}} on them is morally a volatile store).
> h2. What this enables
> *Every already-published indy-compiled jar becomes native-capable without 
> recompilation* — the BSM reference in existing class files is 
> {{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the 
> runtime side. That includes the distribution's own modules: the 
> {{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.
> h2. Verification
> ||check||result||
> |20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic 
> dispatch through one site, ExpandoMetaClass change observed by an already-hot 
> site, per-instance metaclass, category enter/leave with both transitions, 
> operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in 
> a native image built from stock-indy class files with only agent-recorded 
> metadata* — no extra flags|
> |category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0 
> failures|
> |zero retargets in AOT mode|enforced by the fail-fast {{setTarget}}, 
> exercised across the gauntlet under the knob|
> |real-world: the fully dynamic 
> [GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server, 
> unmodified, stock indy compilation|builds and serves all four tools natively, 
> including a live HTTPS fetch through dynamically-compiled 
> {{groovy-http-builder}}|
> Measured against the same server hand-converted to {{@CompileStatic}} + 
> indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):
> ||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
> |ready after spawn|434 ms|23 ms|24–27 ms|
> |get_policy|115 ms|6 ms|8–14 ms|
> |search_policies|79 ms|8 ms|242 ms|
> |refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
> |binary size| |66.0 MB|55.8 MB|
> The dynamic image is the first of the two with all four tools working: the 
> static conversion's network path still crossed indy sites in the distribution 
> jars.
> h2. Performance model and known limitation
> Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns, 
> but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry 
> interpreter cost, independent of chain depth, adapter count, or 
> {{invokeExact}} vs {{invoke}}. Each dynamic call site's invokedynamic hop 
> into its runtime-linked target pays it once per call, so a workload crossing 
> N dynamic sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops 
> per action ≈ 4 ms (invisible for CLI/script/MCP workloads); hot loops 
> crossing tens of thousands of sites show it plainly (search_policies above: 
> ~60k ops ≈ 242 ms, matching the model).
> This floor is structural (the indy-to-runtime-target boundary) and not 
> fixable from Groovy's side; the escapes are {{@CompileStatic}} for hot code 
> (statically compiled code emits no per-operation indy sites and is 
> unaffected), a build-time rewrite of indy sites to {{invokestatic 
> aotDispatch}} (removes the runtime-MH boundary entirely; the shallow 
> dispatcher is deliberately shaped as its target), and upstream GraalVM work 
> on compiling runtime-linked code (Ristretto/Project Crema).
> h2. Scope and follow-ups (not this ticket)
> * Experimental, spike-quality; proposed behind its current automatic 
> detection with the diagnostic knob for CI.
> * Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until 
> GraalVM's dynamic class loading matures; runtime proxy generation for 
> abstract classes likewise.
> * Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter 
> already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}} 
> site-crossing counter so users can measure which zone they are in; the 
> de-indy build-step rewrite tool; the two upstream GraalVM filings; shipping 
> Groovy's own reachability metadata so the agent step shrinks.
> Prototype branch with three commits available; happy to split delivery into 
> the invalidation funnel, the link mode, and the dispatcher if that eases 
> review.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to