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

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

paulk-asert commented on PR #2736:
URL: https://github.com/apache/groovy/pull/2736#issuecomment-5081857121

   AI read:
   
   > ## PR 2736 (GROOVY-12191) readiness assessment
   > 
   > **Bottom line:** The design is sound, the timing (early Groovy 6 cycle) is 
exactly right for a change this deep in the dispatch core, and the evidence 
base is unusually strong. But it isn't merge-ready today: it's one day old with 
zero human reviews, the SonarCloud gate is red (all trivially fixable), and 
there are three or four substantive questions — two of them 
backward-compatibility related — that deserve an answer on the PR first. I'd 
call it "approve direction, request changes on details."
   > 
   > ### What it does
   > 
   > Replaces the single process-wide `SwitchPoint` guarding all indy MOP call 
sites with a per-class SwitchPoint domain on `ClassInfo` (new 
`org.apache.groovy.runtime.indy` package: `IndyInvalidation` policy + 
`SwitchPointInvalidator` lifecycle). MetaClass changes for type T now retire 
only T's SwitchPoint plus loaded subtypes/implementors; category enter/leave 
and `VMPlugin.invalidateCallSites()` bulk-retire all class domains. Hot call 
sites still carry exactly one guard, so steady-state shape is unchanged.
   > 
   > ### Evidence quality — strong
   > 
   > - All CI test jobs green across JDK 17/21/25, three OSes, plus dist builds.
   > - CI JMH (calibrated): indy core **3.3×**, grails suite **4.5×**, bench 
suite parity; classic runtime parity (as expected — it's untouched).
   > - The **"Performance Alert" on the PR is a false alarm**: 
`dispatch_8_megamorphic_java` went from 1841 to 2857 ops/ms — that's 1.55× 
*faster*; the alert bot applied its smaller-is-better ratio to a throughput 
metric. Worth a dismissive comment on the PR so it doesn't spook anyone.
   > - Daniel's A/B verification report: ~146× on cross-type invalidation 
churn, ~76× on burst-then-steady, steady-state parity, 56/56 tests. Same-type 
churn and categories deliberately still pay full re-link cost (correctness 
preserved, not "falsely optimized").
   > - Test coverage is unusually thorough: hierarchy fan-out, interfaces, 
primitives, cleared ClassInfo weak refs, concurrent detach/get races, cold 
tier, child-process probes for the frozen log flags.
   > 
   > ### Backward compatibility — the detailed look you asked for
   > 
   > **Binary compatibility: clean.** `IndyInterface.switchPoint` is retained 
(adding `volatile` and `@Deprecated` doesn't affect linkage), all `ClassInfo` 
additions are additive and `@Internal`-annotated, and the build's compat checks 
passed. Non-indy/legacy callsite caching is unaffected because 
`ClassInfo.getVersion()` bump semantics are preserved on every path.
   > 
   > **Behavioral compatibility: three real changes, all release-notes 
material:**
   > 
   > 1. **`IndyInterface.switchPoint` no longer means what it meant.** It's no 
longer the guard on any call site, and — the sharper edge — it is *no longer 
rotated on per-class MetaClass changes*, only on category enter/leave and 
`invalidateCallSites()`. Any external code (it's `protected`, so subclasses of 
`IndyInterface` or anything generating code against it) that installed guards 
on this field will now silently miss MetaClass-change invalidations and 
dispatch stale. Exposure is probably tiny, but this must go in the Groovy 6 
upgrade notes, and the javadoc's claim that "external readers still see an 
invalidation" is only true for the category subset of events.
   > 
   > 2. **The legacy field rotation lost its lock.** The old code rotated 
`switchPoint` under `synchronized (IndyInterface.class)`; the new code does an 
unsynchronized read-replace-invalidate on the volatile. Two concurrent 
`invalidateSwitchPoints()` calls can orphan a live SwitchPoint that an external 
reader just grabbed — that reader then never observes any future invalidation. 
Since the field is being kept *solely* for external observers, I'd restore the 
synchronized block; it's a cold path and the fix is two lines. (This is also 
effectively what Sonar's S3077 finding is circling.)
   > 
   > 3. **`ClassInfo.incVersion()` (public API) no longer triggers a global 
flush.** It used to call `VMPluginFactory.getPlugin().invalidateCallSites()`; 
now it scopes to class+subtypes. External MOP tooling that called 
`incVersion()` as a "flush everything" hammer gets weaker semantics. Legitimate 
design, but it's a documented-behavior change on a public method — upgrade 
notes again.
   > 
   > **One genuine correctness hole to raise: array classes.** 
`cannotHaveLoadedSubtypes()` treats final classes as fan-out-free, and 
`Class.getModifiers()` reports arrays as final — but `Object[]` is 
assignable-from every object array type and is the MOP superclass of array 
metaclasses. So `registry.setMetaClass(Object[], emc)` (obscure, but legal) 
retires only `Object[]`'s domain and leaves a `String[]`-linked site stale, 
where the old global SwitchPoint caught it. Needs either a special case 
(`type.isArray()` → don't short-circuit) or a test proving it can't matter.
   > 
   > **Scalability trade-off worth acknowledging explicitly:** invalidation 
cost went from O(1) (rotate one SwitchPoint) to O(loaded ClassInfos) — every 
MetaClass change on a non-final type scans `getAllClassInfo()` with 
`isAssignableFrom`, and every category enter/leave detaches every loaded class 
domain. Batching via `invalidateAll` keeps it to one safepoint, and the grails 
suite shows the trade wins decisively in realistic workloads, but Grails-style 
startup (thousands of EMC mutations × tens of thousands of loaded classes) is 
now O(N×M) where it was O(N). Fine to accept for alpha; I'd note a follow-up 
idea (a subtype index on ClassInfo) in the JIRA rather than block on it.
   > 
   > **Also flagged for reviewer attention:** the `invokeColdReflective` 
rewrite is a behavior change beyond pure scoping — it now re-selects via 
`fallback(..., false)` and caches the full wrapper into the callsite to prevent 
cold-tier recursion. Reasonable, but it subtly changes cold-tier promotion and 
deserves its own review eyes (it's also where most of the uncovered lines in 
the codecov report live).
   > 
   > ### Housekeeping before merge
   > 
   > - **SonarCloud gate (red):** four findings, all ~10 minutes of work — 
empty constructor needs removal or comment (S1186), `@Deprecated` needs 
`since`/`forRemoval` (S6355), S3077 on the volatile field (best answered by 
restoring the synchronized rotation), S1133 informational.
   > - **JIRA has no Fix Version** — should get 6.0.0-beta-1.
   > - **No human review yet** — for a change that rewires `ClassInfo` and the 
indy guard chain, I'd want at least one other MOP-deep committer (Eric or 
Jochen) to sign off, and given it changes the documented semantics of 
`IndyInterface.switchPoint`, a short heads-up on dev@ would be in keeping with 
how past indy-internals changes were handled.
   > 
   > ### Groovy 6 fit
   > 
   > Right change, right release, right point in the cycle: it's exactly the 
kind of behavioral rework that must land in a major, and landing it pre-alpha 
maximizes soak time across the ecosystem (Grails, Spock, Geb all lean on 
MetaClass churn patterns this directly targets). Nothing here threatens the 
Groovy 6 schedule — the open items are days of work, not weeks.




> Scope indy SwitchPoint invalidation
> -----------------------------------
>
>                 Key: GROOVY-12191
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12191
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> h3. Problem
> With invokedynamic enabled (default since Groovy 4), linked MOP call sites 
> were guarded by a *single process-wide* {{SwitchPoint}} 
> ({{{}IndyInterface.switchPoint{}}}).
> Any MetaClass registry change or category enter/leave invalidated that switch 
> point, so *every* linked site fell back and re-linked — including sites whose 
> receiver type was unrelated.
> That global invalidation is expensive when MetaClass churn is common (e.g. 
> ExpandoMetaClass / mixins on startup or per-request paths, Grails-like 
> patterns). Unrelated hot monomorphic sites pay re-link and JIT deopt cost 
> they should not.
> h3. Goal
> Keep linked call sites optimized unless the *relevant* MetaClass state for 
> that site actually changed.
> h3. Approach
> One SwitchPoint domain {*}per class{*}, stored on {{{}ClassInfo{}}}:
>  * MetaClass change for type {{T}} retires {{{}T{}}}'s SwitchPoint *and* 
> those of loaded subtypes / implementors (hierarchy fan-out).
>  * Unrelated types keep their SwitchPoints; their call sites stay optimized.
>  * Category enter/leave (and {{{}VMPlugin.invalidateCallSites(){}}}) 
> bulk-retire *all* loaded class SwitchPoints so sites re-link under the new 
> category state. There is *no* second category SwitchPoint on the hot path.
>  * Linked handles always install a *single* class-domain guard via 
> {{IndyInvalidation.guardWithMopSwitchPoints(...)}} — same monomorphic guard 
> shape as before, without global deopt on unrelated MetaClass churn.
> Final classes short-circuit hierarchy fan-out (no full {{ClassInfo}} scan). 
> Non-final types batch retirements with {{{}SwitchPoint.invalidateAll{}}}.
> h3. Invalidation map
> ||Event||What is retired||
> |MetaClass change for type {{T}} (registry / 
> {{{}ClassInfo.incVersion{}}})|{{T}} + loaded subtypes / implementors|
> |Category enter/leave, {{invalidateCallSites()}}|All loaded class 
> SwitchPoints (bulk)|
> |Unattributed MetaClass registry event|All loaded class SwitchPoints (bulk)|
> |First MetaClass *install* on a class|Version bump only (no linked sites 
> yet); replacement / clear retires that class's SwitchPoint|
>  



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

Reply via email to