[ 
https://issues.apache.org/jira/browse/GROOVY-12234?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18101650#comment-18101650
 ] 

ASF GitHub Bot commented on GROOVY-12234:
-----------------------------------------

testlens-app[bot] commented on PR #2766:
URL: https://github.com/apache/groovy/pull/2766#issuecomment-5177054790

   ## ✅ All tests passed ✅
   
   🏷️ Commit: f0ecc57449d08b348bdeddc34bddade411f4662d
   ▶️ Tests:  108774 executed
   ⚪️ Checks: 23/23 completed
   
   ---
   _Learn more about TestLens at [testlens.app](https://testlens.app)._
   




> 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 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