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

Paul King edited comment on GROOVY-12281 at 8/19/26 4:14 AM:
-------------------------------------------------------------

Investigation complete; closing with *no change to the default*, with every 
option carrying a measured or code-grounded verdict. Full record (methods, 
numbers, file:line evidence) in the assessment document on branch 
{{groovy12281}}; summary:

*Recompute-based options (SoftReference values, including the pristine-gated 
variant) — structurally illegal, not merely risky:*
# DGM and extension-module method arrays are written _once_ into each 
{{ClassInfo}} by {{CachedClass.updateSetNewMopMethods}} during registry 
initialization; no re-registration path exists, so a recomputed {{ClassInfo}} 
for {{String}} yields a MetaClass without DGM methods. This also nullifies 
pristine-gating: platform classes are exactly the never-pristine ones, so the 
immortal keys the mitigation targets would all be excluded.
# Classic call sites capture the {{ClassInfo}} _instance_ and guard on its 
version; a dropped-and-recreated instance leaves stale guards accepting forever 
— silently wrong dispatch, blind to metaclasses installed on the fresh instance.

*Hybrid per-key store (platform keys → weak map, user keys → ClassValue) — 
declined on measurement:* prototyped ({{-Dgroovy.use.classvalue=hybrid}}, kept 
on the branch as the measured artifact) and benchmarked; it tracks the _map_, 
not ClassValue, on macro dispatch, because dynamic code cannot avoid platform 
receivers (strings, boxed numbers, DGM targets) and those are precisely the 
keys it maps.

*Map-based default (flip {{groovy.use.classvalue}} default) — declined on 
measurement:* the compiler-performance harness gate _passed_ (+1.0%, within 
noise), but the runtime JMH classic-dispatch gate _failed_ under the pre-set 
"within noise → flip" rule: *+4.8% geomean* over 57 benchmarks, 12 
statistically significant regressions, worst band 1.20–1.38× on 
platform-receiver idioms ({{asToString}} 1.38×, elvis 1.32×, range iteration 
1.32×, safe navigation 1.31×, map property access 1.23×, {{in}} operator 1.20×).

*Outcome:* {{java.lang.ClassValue}} stays the default for its measured dispatch 
advantage; the container pin remains addressed by the two remedies GROOVY-12142 
ships — {{-Dgroovy.use.classvalue=false}} and explicit {{ClassInfo.remove()}} 
on undeploy. The practical yield of this investigation is that the escape 
hatch's cost is now _quantified_ rather than asserted (≈5% geomean on classic 
dispatch idioms, hot idioms to ~1.4×, compile within 1%), and the integration 
guide has been updated to say so, so affected deployments can decide with 
numbers.

Any future revisit has two hard constraints on record: no scheme may recompute 
{{ClassInfo}} (disqualifiers above), and per-key routing does not escape the 
map's dispatch cost. A genuinely new idea would need to change one of those 
facts first.

https://github.com/apache/groovy/blob/groovy12281/GROOVY-12281-assessment.html


was (Author: paulk):
Investigation complete; closing with *no change to the default*, with every 
option carrying a measured or code-grounded verdict. Full record (methods, 
numbers, file:line evidence) in the assessment document on branch 
{{groovy12281}}; summary:

*Recompute-based options (SoftReference values, including the pristine-gated 
variant) — structurally illegal, not merely risky:*
# DGM and extension-module method arrays are written _once_ into each 
{{ClassInfo}} by {{CachedClass.updateSetNewMopMethods}} during registry 
initialization; no re-registration path exists, so a recomputed {{ClassInfo}} 
for {{String}} yields a MetaClass without DGM methods. This also nullifies 
pristine-gating: platform classes are exactly the never-pristine ones, so the 
immortal keys the mitigation targets would all be excluded.
# Classic call sites capture the {{ClassInfo}} _instance_ and guard on its 
version; a dropped-and-recreated instance leaves stale guards accepting forever 
— silently wrong dispatch, blind to metaclasses installed on the fresh instance.

*Hybrid per-key store (platform keys → weak map, user keys → ClassValue) — 
declined on measurement:* prototyped ({{-Dgroovy.use.classvalue=hybrid}}, kept 
on the branch as the measured artifact) and benchmarked; it tracks the _map_, 
not ClassValue, on macro dispatch, because dynamic code cannot avoid platform 
receivers (strings, boxed numbers, DGM targets) and those are precisely the 
keys it maps.

*Map-based default (flip {{groovy.use.classvalue}} default) — declined on 
measurement:* the compiler-performance harness gate _passed_ (+1.0%, within 
noise), but the runtime JMH classic-dispatch gate _failed_ under the pre-set 
"within noise → flip" rule: *+4.8% geomean* over 57 benchmarks, 12 
statistically significant regressions, worst band 1.20–1.38× on 
platform-receiver idioms ({{asToString}} 1.38×, elvis 1.32×, range iteration 
1.32×, safe navigation 1.31×, map property access 1.23×, {{in}} operator 1.20×).

*Outcome:* {{java.lang.ClassValue}} stays the default for its measured dispatch 
advantage; the container pin remains addressed by the two remedies GROOVY-12142 
ships — {{-Dgroovy.use.classvalue=false}} and explicit {{ClassInfo.remove()}} 
on undeploy. The practical yield of this investigation is that the escape 
hatch's cost is now _quantified_ rather than asserted (≈5% geomean on classic 
dispatch idioms, hot idioms to ~1.4×, compile within 1%), and the integration 
guide has been updated to say so, so affected deployments can decide with 
numbers.

Any future revisit has two hard constraints on record: no scheme may recompute 
{{ClassInfo}} (disqualifiers above), and per-key routing does not escape the 
map's dispatch cost. A genuinely new idea would need to change one of those 
facts first.


> Investigate mitigating class-loader pinning by ClassInfo.globalClassValue 
> under the default ClassValue implementation
> ---------------------------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12281
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12281
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> Follow-up from review discussion on [PR 
> #2798|https://github.com/apache/groovy/pull/2798] (GROOVY-12142), preserving 
> the analysis from that review. A {{ClassValue}} realizes the chain _key class 
> → association → value → everything reachable from it_, and the association 
> lives as long as the key class 
> ([JDK-8136353|https://bugs.openjdk.org/browse/JDK-8136353], working as 
> intended). The {{ClassValue}} implementation class itself prevents no 
> unloading; what matters is the key's origin: a Groovy-loaded key class dies 
> with Groovy's loader (fine), but a JDK/platform key class is effectively 
> immortal, so any value reachable from it that was loaded by Groovy's loader 
> pins that loader forever. Indirection counts — a JDK-typed value (e.g. an 
> {{ArrayList}}) whose _elements_ are Groovy-loaded re-creates the pin one 
> level down.
> {{ClassInfo.globalClassValue}} is static with unknown keys, including 
> platform classes ({{String}} receives a {{ClassInfo}} in essentially every 
> Groovy program), so the default {{ClassValue}} path pins the loader; today's 
> only remedy is the global {{groovy.use.classvalue=false}} escape hatch, which 
> trades away the per-class fast path for all keys.
> The general mitigation — {{SoftReference}}-wrapped values with a 
> check-remove-recompute protocol — requires that recomputation be legal, and 
> for {{ClassInfo}} it is not in general: a {{ClassInfo}} can carry 
> non-recomputable state (modified metaclasses, category state), so a 
> softly-collected value could silently discard user metaclass customizations. 
> Approaches to investigate:
> * soft values while a {{ClassInfo}} is pristine, hardening the reference on 
> first mutation — only classes with customized metaclasses would then pin, a 
> far smaller set;
> * per-key policy: platform-loader keys get soft/map treatment, Groovy-loader 
> keys stay strong (the key-origin rule applied mechanically);
> * splitting {{ClassInfo}} into recomputable and stateful parts;
> * revisiting whether the map-based implementation should become the default, 
> with {{ClassValue}} as the opt-in fast path.
> Recompute cost and dispatch-path performance need measurement for any 
> candidate.



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

Reply via email to