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

Paul King updated GROOVY-12227:
-------------------------------
    Description: 
Packed closures (GROOVY-12151, GEP-27) cannot be used in a GraalVM native 
image. A class compiled with {{groovy.target.closure.pack=true}} links its 
dispatch tables on first closure creation, and that linkage fails inside an 
image — in two layers.

Without agent-recorded metadata, the bootstrap's method lookup fails:

{noformat}
java.lang.BootstrapMethodError: java.lang.NoSuchMethodException:
    no such method: B.$packedDispatch$(int,Object[])Object/invokeStatic
{noformat}

With agent-recorded metadata the lookup succeeds, and the real wall appears — 
the bootstrap spins hidden classes at run time, which an image forbids:

{noformat}
Exception in thread "main" java.lang.BootstrapMethodError: 
java.lang.InternalError:
com.oracle.svm.core.jdk.UnsupportedFeatureError: Classes cannot be defined at 
runtime
by default when using ahead-of-time Native Image compilation. Tried to define 
class:

    B$$LambdadLbm0tKUrZ8kVMiSpulnyK
{noformat}

The failure mode is the problem as much as the failure: packing compiles 
cleanly, the image builds cleanly, and the process then dies on the first 
closure created. There is no diagnostic pointing at the flag that caused it.

h2. Reproducing

{code:java}
import groovy.transform.CompileStatic

@CompileStatic
class B {
    static int work(int n) {
        def xs = (1..n).toList()
        int total = 0
        total += xs.collect { int v -> v * 2 }.size()
        total += xs.findAll { int v -> v % 2 == 0 }.size()
        total += (xs.inject(0) { int a, int b -> a + b } as int)
        total
    }
    static void main(String[] args) { println "result=${work(20)}" }
}
{code}

{noformat}
JAVA_OPTS="-Dgroovy.target.indy=false -Dgroovy.target.closure.pack=true" \
    groovyc -cp groovy-callsite.jar B.groovy       # emits a single class: 
B.class

java -agentlib:native-image-agent=config-output-dir=cfg -cp 
"groovy.jar:groovy-callsite.jar:." B

native-image --no-fallback -H:ConfigurationFileDirectories=cfg \
    
-H:IncludeResources='META-INF/dgminfo|META-INF/groovy/.*|META-INF/services/.*' \
    -cp "groovy.jar:groovy-callsite.jar:." B
./b   # fails as above
{noformat}

Reproduced with GraalVM CE 25.2.4 (native-image 25.0.4 — the first release 
whose Groovy substitution guard is fixed for Groovy 5+, see oracle/graal#13096; 
earlier releases fail before reaching this).

Note that indy must already be off, with {{groovy-callsite}} on the *compile* 
classpath: Groovy's own {{IndyInterface}} dispatch uses mutable call sites, 
which native image does not support, and it fails upstream of anything here. So 
this ticket only affects users who have already crossed that larger hurdle.

h2. Root cause

{{GeneratedDispatcher.bootstrap}} does two things an image cannot support:

# {{caller.findStatic(host, "$packedDispatch$", ...)}} — a *runtime* 
method-handle lookup, which under native image needs per-user-class reflection 
metadata. Groovy's own jar cannot ship that, so every user would need an agent 
run.
# Three programmatic 
{{LambdaMetafactory.metafactory(...).getTarget().invokeExact()}} calls, each 
defining a hidden class at run time. Bytecode-level LMF invokedynamic is 
pre-processed at image build time and works fine; *programmatic* metafactory 
calls inside a bootstrap are runtime class definition, which is forbidden.

The surrounding design is already image-friendly: the result is wrapped in a 
{{ConstantCallSite}} and never re-linked.

h2. Proposed fix (prototyped)

Two layered changes. JVM semantics are unchanged.

*1. Constant bootstrap arguments (ClosureWriter).* Emit the three dispatch 
tables as {{CONSTANT_MethodHandle}} bootstrap arguments, as javac does for 
LMF's {{implMethod}}:

{noformat}
invokedynamic packedDispatchers()Object
    bsm  = IndyInterface.packedDispatchers(Lookup, String, MethodType,
                                           MethodHandle, MethodHandle, 
MethodHandle)
    args = [ MH(invokestatic host.$packedDispatch$), MH(...1$), MH(...2$) ]
{noformat}

Constant-pool method handles are resolved by the VM (and pre-resolved at image 
build time), so linking needs no {{findStatic}} and no metadata. Verified: the 
tracing agent records *zero* {{packedDispatch}} entries for the new bytecode. 
The existing 3-arg bootstrap is kept for class files emitted by earlier 6.0 
snapshots.

*2. Method-handle bundle fallback (GeneratedDispatcher).* On a regular JVM the 
{{LambdaMetafactory}} hidden-class adapters are kept — their interface call 
inlines under the JIT, which is the documented rationale. Where classes cannot 
be defined at run time, the tables are wrapped in method-handle-invoking 
adapters instead: ordinary Java lambdas of {{GeneratedDispatcher}} itself, 
whose bytecode-level LMF sites are AOT-compiled into the image, so nothing is 
defined at run time.

Detection is 
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}}, 
evaluated *per link* rather than cached in a static — under native image the 
class may be build-time initialized, where the property reports {{buildtime}}, 
and caching would bake the wrong answer into the image heap. A catch-based 
fallback around the metafactory calls covers AOT runtimes the property probe 
misses.

One subtlety: dispatch targets may throw checked exceptions the dispatcher 
interfaces do not declare, and the hidden-class path propagates them 
transparently. The wrapper path rethrows via {{UncheckedThrow}} to match; a 
dedicated test covers this.

h2. Opt-in / opt-out / permanent?

Permanent and automatic — no user-facing flag:
* JVM behaviour is byte-identical; the fallback engages only where the 
hidden-class path *cannot work*. A flag would choose between "works" and 
"crash".
* The gate already exists one level up: packing is itself opt-in 
({{groovy.target.closure.pack}} / {{@PackedClosures}}) and experimental under 
GEP-27.
* {{-Dgroovy.packed.dispatch.handles=true}} exists as a *diagnostic* knob only, 
forcing the wrapper path on a JVM so CI can assert parity without a native 
build.

h2. Correctness verification

||check||result||
|{{PackedDispatcherHandleBundleTest}} (new): parity across all three dispatch 
shapes and the GDK; undeclared checked-exception propagation|2/2 pass|
|existing packed/closure suites ({{PackedClosuresTransformTest}}, 
{{ClosurePackCapabilityTest}}, {{PackedClosureBoundariesTest}}, 
{{PackedClosureMetaClassTest}}, {{PackedClosureDebugMetadataTest}}, 
{{ClosureAndInnerClassNodeStructureTest}}, {{ClosureCallMopGuardTest}})|57 
tests green|
|repro, unpatched master, native|{{Classes cannot be defined at runtime}}|
|repro, patched, native|runs correctly, exit 0, no exceptions|
|agent metadata for the dispatcher|0 entries (was: per-class method 
registrations)|

h2. Performance

Measured on a 120-closure workload (121 classes unpacked, 1 packed) and a 
dispatch-heavy loop, GraalVM CE 25.2.4 / native-image 25.0.4. *The honest 
summary: this fix is cheap, but packing in a native image is a trade rather 
than a win.*

*Cost of the fix itself.* Isolated by building the same packed sources against 
unpatched master (which builds, then fails at run time) and against the patch:

||build||image size||delta||
|unpacked|29,138,784 bytes| |
|packed, unpatched master (does not run)|30,528,208 bytes|+1.39 MB|
|packed, patched (runs)|30,643,936 bytes|+1.51 MB|

So the fix adds *~113 KB (+0.4%)*. The larger +1.39 MB is pre-existing packing 
overhead, not introduced here: {{jdk.internal.classfile.impl}} (561 KiB) is 
pulled in by the {{LambdaMetafactory}} reference that master already had.

*Image size.* Packing makes the image ~1.4 MB *bigger*, not smaller. The 
121-to-1 class collapse does not translate into image savings — native image 
already handles many small classes efficiently, and the {{PackedClosure}} 
runtime is a fixed cost. (An earlier draft of this ticket asserted the 
opposite; the measurements above correct it.)

*Steady-state dispatch.*

||workload||native||JVM||
|unpacked|20–21 ms|18–21 ms|
|packed|*35–41 ms (~2x slower)*|19–20 ms|
|packed, MH path forced on JVM|—|20.2–20.4 ms (~7% over hidden-class)|

The JVM is unaffected: the wrapper costs ~7% there and is not used anyway. In 
an image there is no JIT to fold the {{invokeExact}}, so the indirection shows 
up in full on this deliberately dispatch-heavy loop; ordinary code will see 
less.

*Startup*, where packing does win — fewer classes to initialize:

||workload||unpacked||packed||
|120 closures|21.9 ms|*11.6 ms*|
|6 closures|12.8 ms|12.5 ms|

h2. Follow-ups (not this ticket)

* *Drop the LMF reference on the native path* so 
{{jdk.internal.classfile.impl}} (561 KiB) falls out of the image, removing most 
of packing's size overhead.
* *A class-free dispatch that is not method-handle-based* — for example 
{{MethodHandles.tableSwitch}}, or a generated switch — to close the ~2x 
steady-state gap. That would turn native packing from a trade into a win.
* *Ship Groovy's own reachability metadata* (VMPluginFactory reflection, 
{{dgm$NNN}} classes, {{META-INF/dgminfo}}) in the jar, so the agent step 
disappears for everyone. App-independent and pre-existing; unrelated to packing.


  was:
Packed closures (GROOVY-12151, GEP-27) do not work under GraalVM native image. 
A class compiled with {{groovy.target.closure.pack=true}} links its dispatch 
tables on first closure creation, and that linkage fails inside a native image 
— in two layers.

Without agent-recorded metadata, the bootstrap's method lookup fails:

{noformat}
java.lang.BootstrapMethodError: java.lang.NoSuchMethodException:
    no such method: M.$packedDispatch$(int,Object[])Object/invokeStatic
{noformat}

With agent-recorded metadata the lookup succeeds, and the real wall appears — 
the bootstrap spins hidden classes at run time, which a native image forbids:

{noformat}
Exception in thread "main" java.lang.BootstrapMethodError: 
java.lang.InternalError:
com.oracle.svm.core.jdk.UnsupportedFeatureError: Classes cannot be defined at 
runtime
by default when using ahead-of-time Native Image compilation. Tried to define 
class:

    M$$LambdadLbm0tKUrZ8kVMiSpulnyK
{noformat}

This matters because packing is otherwise a natural fit for native images: it 
collapses per-closure inner classes into the hosting class (a 3-to-1 class 
reduction in the repro below), which directly reduces image size and 
reachability metadata, and its call site is a {{ConstantCallSite}} — no runtime 
re-linking, unlike the mutable-call-site dispatch of regular indy.

h2. Reproducing

{code:java}
import groovy.transform.CompileStatic
import java.util.function.BiFunction

@CompileStatic
class M {
    static void main(String[] args) {
        BiFunction<Integer, Integer, Integer> lam = (Integer a, Integer b) -> a 
+ b
        BiFunction<Integer, Integer, Integer> coe = { Integer a, Integer b -> a 
* b } as BiFunction<Integer, Integer, Integer>
        var doubled = [1, 2, 3].collect { int n -> n * 2 }
        println "lambda:  ${lam.apply(20, 22)}"
        println "coerced: ${coe.apply(6, 7)}"
        println "collect: ${doubled}"
    }
}
{code}

{noformat}
JAVA_OPTS="-Dgroovy.target.indy=false -Dgroovy.target.lambda.hoist=true 
-Dgroovy.target.closure.pack=true" \
    groovyc -cp groovy-callsite.jar M.groovy      # emits a single class: 
M.class

java -agentlib:native-image-agent=config-output-dir=cfg -cp 
"groovy.jar:groovy-callsite.jar:." M

native-image --no-fallback -H:ConfigurationFileDirectories=cfg \
    
-H:IncludeResources='META-INF/dgminfo|META-INF/groovy/.*|META-INF/services/.*' \
    -cp "groovy.jar:groovy-callsite.jar:." M
./m   # fails as above
{noformat}

Reproduced with GraalVM CE 25.2.4 (native-image 25.0.4, the first release whose 
Groovy substitution guard is fixed for Groovy 5+ — see oracle/graal#13096).

h2. Root cause

{{GeneratedDispatcher.bootstrap}} does two things a native image cannot support:

# {{caller.findStatic(host, "$packedDispatch$", ...)}} — a *runtime* 
method-handle lookup, which under native image requires per-user-class 
reflection metadata (unknowable to Groovy's own jar, so every user would need 
an agent run).
# Three programmatic 
{{LambdaMetafactory.metafactory(...).getTarget().invokeExact()}} calls — each 
defines a hidden class at run time. Bytecode-level LMF invokedynamic is 
pre-processed at image build time and works fine; *programmatic* metafactory 
calls inside a bootstrap are runtime class definition, which is forbidden.

The surrounding design is already image-friendly: the result is wrapped in a 
{{ConstantCallSite}} and never re-linked.

h2. Proposed fix (prototyped)

Two layered changes; JVM semantics unchanged.

*1. Constant bootstrap arguments (ClosureWriter).* Emit the three dispatch 
tables as {{CONSTANT_MethodHandle}} bootstrap arguments, exactly as javac does 
for LMF's {{implMethod}}:

{noformat}
invokedynamic packedDispatchers()Object
    bsm  = IndyInterface.packedDispatchers(Lookup, String, MethodType,
                                           MethodHandle, MethodHandle, 
MethodHandle)
    args = [ MH(invokestatic host.$packedDispatch$), MH(...1$), MH(...2$) ]
{noformat}

Constant-pool method handles are resolved by the VM (and pre-resolved at image 
build time), so linking needs no {{findStatic}} and no metadata. Verified: the 
tracing agent records *zero* {{packedDispatch}} entries for the new bytecode. 
The existing 3-arg bootstrap remains for class files emitted by earlier 6.0 
snapshots.

*2. Method-handle bundle fallback (GeneratedDispatcher).* On a regular JVM the 
{{LambdaMetafactory}} hidden-class adapters are kept — their interface call 
inlines under the JIT, which is the documented rationale. Where classes cannot 
be defined at run time, the tables are wrapped in method-handle-invoking 
adapters instead: ordinary Java lambdas of {{GeneratedDispatcher}} itself, 
whose bytecode-level LMF sites are AOT-compiled into the image, so no class 
definition happens at run time. There is no JIT in such runtimes, so the 
inlining rationale does not apply there.

Detection is 
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}}, 
evaluated *per link* rather than cached in a static — under native image the 
class may be build-time initialized, where the property reports {{buildtime}}, 
and caching would bake the wrong answer into the image heap. A catch-based 
fallback around the metafactory calls covers AOT runtimes the property probe 
misses.

One subtlety: dispatch targets may throw checked exceptions the dispatcher 
interfaces do not declare, and the hidden-class path propagates them 
transparently. The wrapper path rethrows via {{UncheckedThrow}} to match, and 
this is covered by a dedicated test.

h2. Opt-in / opt-out / permanent?

Permanent and automatic — no user-facing flag:
* JVM behaviour is byte-identical; the fallback engages only where the 
hidden-class path *cannot work*. A flag would choose between "works" and 
"crash".
* The gate already exists one level up: packing is itself opt-in 
({{groovy.target.closure.pack}} / {{@PackedClosures}}) and experimental under 
GEP-27.
* {{-Dgroovy.packed.dispatch.handles=true}} exists as a *diagnostic* knob only, 
forcing the wrapper path on a JVM so CI can assert parity without a native 
build.

h2. Verification

||check||result||
|{{PackedDispatcherHandleBundleTest}} (new: parity across all three dispatch 
shapes + the GDK; undeclared checked-exception propagation)|2/2 pass|
|existing packed/closure suites ({{PackedClosuresTransformTest}}, 
{{ClosurePackCapabilityTest}}, {{PackedClosureBoundariesTest}}, 
{{PackedClosureMetaClassTest}}, {{PackedClosureDebugMetadataTest}}, 
{{ClosureAndInnerClassNodeStructureTest}}, {{ClosureCallMopGuardTest}})|57 
tests green|
|repro, unpatched master, native|{{Classes cannot be defined at runtime}}|
|repro, patched, native|runs correctly: {{lambda: 42 / coerced: 42 / collect: 
[2, 4, 6]}}, exit 0, no exceptions|
|agent metadata for the dispatcher|0 entries (was: per-class method 
registrations)|
|footprint|single emitted class; 30 MB image; ~12 ms total run time|

The remaining agent configuration in the repro covers the general Groovy 
runtime bootstrap (VMPluginFactory reflection, {{dgm$NNN}} classes, 
{{META-INF/dgminfo}}) — app-independent, pre-existing, and out of scope here; 
shipping that fixed set as reachability metadata in Groovy's own jar would be a 
natural follow-up ticket.




> GeneratedDispatcher: avoid runtime class definition so packed closures work 
> in native images 
> ---------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12227
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12227
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> Packed closures (GROOVY-12151, GEP-27) cannot be used in a GraalVM native 
> image. A class compiled with {{groovy.target.closure.pack=true}} links its 
> dispatch tables on first closure creation, and that linkage fails inside an 
> image — in two layers.
> Without agent-recorded metadata, the bootstrap's method lookup fails:
> {noformat}
> java.lang.BootstrapMethodError: java.lang.NoSuchMethodException:
>     no such method: B.$packedDispatch$(int,Object[])Object/invokeStatic
> {noformat}
> With agent-recorded metadata the lookup succeeds, and the real wall appears — 
> the bootstrap spins hidden classes at run time, which an image forbids:
> {noformat}
> Exception in thread "main" java.lang.BootstrapMethodError: 
> java.lang.InternalError:
> com.oracle.svm.core.jdk.UnsupportedFeatureError: Classes cannot be defined at 
> runtime
> by default when using ahead-of-time Native Image compilation. Tried to define 
> class:
>     B$$LambdadLbm0tKUrZ8kVMiSpulnyK
> {noformat}
> The failure mode is the problem as much as the failure: packing compiles 
> cleanly, the image builds cleanly, and the process then dies on the first 
> closure created. There is no diagnostic pointing at the flag that caused it.
> h2. Reproducing
> {code:java}
> import groovy.transform.CompileStatic
> @CompileStatic
> class B {
>     static int work(int n) {
>         def xs = (1..n).toList()
>         int total = 0
>         total += xs.collect { int v -> v * 2 }.size()
>         total += xs.findAll { int v -> v % 2 == 0 }.size()
>         total += (xs.inject(0) { int a, int b -> a + b } as int)
>         total
>     }
>     static void main(String[] args) { println "result=${work(20)}" }
> }
> {code}
> {noformat}
> JAVA_OPTS="-Dgroovy.target.indy=false -Dgroovy.target.closure.pack=true" \
>     groovyc -cp groovy-callsite.jar B.groovy       # emits a single class: 
> B.class
> java -agentlib:native-image-agent=config-output-dir=cfg -cp 
> "groovy.jar:groovy-callsite.jar:." B
> native-image --no-fallback -H:ConfigurationFileDirectories=cfg \
>     
> -H:IncludeResources='META-INF/dgminfo|META-INF/groovy/.*|META-INF/services/.*'
>  \
>     -cp "groovy.jar:groovy-callsite.jar:." B
> ./b   # fails as above
> {noformat}
> Reproduced with GraalVM CE 25.2.4 (native-image 25.0.4 — the first release 
> whose Groovy substitution guard is fixed for Groovy 5+, see 
> oracle/graal#13096; earlier releases fail before reaching this).
> Note that indy must already be off, with {{groovy-callsite}} on the *compile* 
> classpath: Groovy's own {{IndyInterface}} dispatch uses mutable call sites, 
> which native image does not support, and it fails upstream of anything here. 
> So this ticket only affects users who have already crossed that larger hurdle.
> h2. Root cause
> {{GeneratedDispatcher.bootstrap}} does two things an image cannot support:
> # {{caller.findStatic(host, "$packedDispatch$", ...)}} — a *runtime* 
> method-handle lookup, which under native image needs per-user-class 
> reflection metadata. Groovy's own jar cannot ship that, so every user would 
> need an agent run.
> # Three programmatic 
> {{LambdaMetafactory.metafactory(...).getTarget().invokeExact()}} calls, each 
> defining a hidden class at run time. Bytecode-level LMF invokedynamic is 
> pre-processed at image build time and works fine; *programmatic* metafactory 
> calls inside a bootstrap are runtime class definition, which is forbidden.
> The surrounding design is already image-friendly: the result is wrapped in a 
> {{ConstantCallSite}} and never re-linked.
> h2. Proposed fix (prototyped)
> Two layered changes. JVM semantics are unchanged.
> *1. Constant bootstrap arguments (ClosureWriter).* Emit the three dispatch 
> tables as {{CONSTANT_MethodHandle}} bootstrap arguments, as javac does for 
> LMF's {{implMethod}}:
> {noformat}
> invokedynamic packedDispatchers()Object
>     bsm  = IndyInterface.packedDispatchers(Lookup, String, MethodType,
>                                            MethodHandle, MethodHandle, 
> MethodHandle)
>     args = [ MH(invokestatic host.$packedDispatch$), MH(...1$), MH(...2$) ]
> {noformat}
> Constant-pool method handles are resolved by the VM (and pre-resolved at 
> image build time), so linking needs no {{findStatic}} and no metadata. 
> Verified: the tracing agent records *zero* {{packedDispatch}} entries for the 
> new bytecode. The existing 3-arg bootstrap is kept for class files emitted by 
> earlier 6.0 snapshots.
> *2. Method-handle bundle fallback (GeneratedDispatcher).* On a regular JVM 
> the {{LambdaMetafactory}} hidden-class adapters are kept — their interface 
> call inlines under the JIT, which is the documented rationale. Where classes 
> cannot be defined at run time, the tables are wrapped in 
> method-handle-invoking adapters instead: ordinary Java lambdas of 
> {{GeneratedDispatcher}} itself, whose bytecode-level LMF sites are 
> AOT-compiled into the image, so nothing is defined at run time.
> Detection is 
> {{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}},
>  evaluated *per link* rather than cached in a static — under native image the 
> class may be build-time initialized, where the property reports 
> {{buildtime}}, and caching would bake the wrong answer into the image heap. A 
> catch-based fallback around the metafactory calls covers AOT runtimes the 
> property probe misses.
> One subtlety: dispatch targets may throw checked exceptions the dispatcher 
> interfaces do not declare, and the hidden-class path propagates them 
> transparently. The wrapper path rethrows via {{UncheckedThrow}} to match; a 
> dedicated test covers this.
> h2. Opt-in / opt-out / permanent?
> Permanent and automatic — no user-facing flag:
> * JVM behaviour is byte-identical; the fallback engages only where the 
> hidden-class path *cannot work*. A flag would choose between "works" and 
> "crash".
> * The gate already exists one level up: packing is itself opt-in 
> ({{groovy.target.closure.pack}} / {{@PackedClosures}}) and experimental under 
> GEP-27.
> * {{-Dgroovy.packed.dispatch.handles=true}} exists as a *diagnostic* knob 
> only, forcing the wrapper path on a JVM so CI can assert parity without a 
> native build.
> h2. Correctness verification
> ||check||result||
> |{{PackedDispatcherHandleBundleTest}} (new): parity across all three dispatch 
> shapes and the GDK; undeclared checked-exception propagation|2/2 pass|
> |existing packed/closure suites ({{PackedClosuresTransformTest}}, 
> {{ClosurePackCapabilityTest}}, {{PackedClosureBoundariesTest}}, 
> {{PackedClosureMetaClassTest}}, {{PackedClosureDebugMetadataTest}}, 
> {{ClosureAndInnerClassNodeStructureTest}}, {{ClosureCallMopGuardTest}})|57 
> tests green|
> |repro, unpatched master, native|{{Classes cannot be defined at runtime}}|
> |repro, patched, native|runs correctly, exit 0, no exceptions|
> |agent metadata for the dispatcher|0 entries (was: per-class method 
> registrations)|
> h2. Performance
> Measured on a 120-closure workload (121 classes unpacked, 1 packed) and a 
> dispatch-heavy loop, GraalVM CE 25.2.4 / native-image 25.0.4. *The honest 
> summary: this fix is cheap, but packing in a native image is a trade rather 
> than a win.*
> *Cost of the fix itself.* Isolated by building the same packed sources 
> against unpatched master (which builds, then fails at run time) and against 
> the patch:
> ||build||image size||delta||
> |unpacked|29,138,784 bytes| |
> |packed, unpatched master (does not run)|30,528,208 bytes|+1.39 MB|
> |packed, patched (runs)|30,643,936 bytes|+1.51 MB|
> So the fix adds *~113 KB (+0.4%)*. The larger +1.39 MB is pre-existing 
> packing overhead, not introduced here: {{jdk.internal.classfile.impl}} (561 
> KiB) is pulled in by the {{LambdaMetafactory}} reference that master already 
> had.
> *Image size.* Packing makes the image ~1.4 MB *bigger*, not smaller. The 
> 121-to-1 class collapse does not translate into image savings — native image 
> already handles many small classes efficiently, and the {{PackedClosure}} 
> runtime is a fixed cost. (An earlier draft of this ticket asserted the 
> opposite; the measurements above correct it.)
> *Steady-state dispatch.*
> ||workload||native||JVM||
> |unpacked|20–21 ms|18–21 ms|
> |packed|*35–41 ms (~2x slower)*|19–20 ms|
> |packed, MH path forced on JVM|—|20.2–20.4 ms (~7% over hidden-class)|
> The JVM is unaffected: the wrapper costs ~7% there and is not used anyway. In 
> an image there is no JIT to fold the {{invokeExact}}, so the indirection 
> shows up in full on this deliberately dispatch-heavy loop; ordinary code will 
> see less.
> *Startup*, where packing does win — fewer classes to initialize:
> ||workload||unpacked||packed||
> |120 closures|21.9 ms|*11.6 ms*|
> |6 closures|12.8 ms|12.5 ms|
> h2. Follow-ups (not this ticket)
> * *Drop the LMF reference on the native path* so 
> {{jdk.internal.classfile.impl}} (561 KiB) falls out of the image, removing 
> most of packing's size overhead.
> * *A class-free dispatch that is not method-handle-based* — for example 
> {{MethodHandles.tableSwitch}}, or a generated switch — to close the ~2x 
> steady-state gap. That would turn native packing from a trade into a win.
> * *Ship Groovy's own reachability metadata* (VMPluginFactory reflection, 
> {{dgm$NNN}} classes, {{META-INF/dgminfo}}) in the jar, so the agent step 
> disappears for everyone. App-independent and pre-existing; unrelated to 
> packing.



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

Reply via email to