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

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

daniellansun commented on PR #2736:
URL: https://github.com/apache/groovy/pull/2736#issuecomment-5120228130

   @blackdrag Thank you for the correction and for the detailed parent→child 
matrix — that is exactly the right level of precision.
   
   ## 1. Hierarchy direction
   
   Understood: the scenario of interest is **superclass MetaClass changes 
observed from a child receiver**, not the reverse. Sorry the earlier matrix 
inverted that ladder. The tests now follow your corrected direction (`A` parent 
→ `B`/`C`/`D` children).
   
   ## 2. Missing-method-only walk (and the C/D gap)
   
   Agreed on the important refinement:
   
   - `MetaClassImpl.findMethodInClassHierarchy` is a **missing-method** path. 
It only opens a real walk when some strong MetaClass in the hierarchy is a 
**modified** `MutableMetaClass` (EMC is the usual case).
   - A method that already exists on the child keeps winning for applicable 
arguments after hierarchy re-link; SwitchPoint retirement does **not** rebuild 
MetaClass tables.
   - The **C vs D** divergence (same structure, different MetaClass 
construction time relative to parent EMC) is pre-existing MOP behaviour: 
construction-time snapshot / inheritance of ancestor expando methods into a new 
`MetaClassImpl`, not something class-domain SwitchPoints can honestly “fix”.
   
   **6.0 decision for this PR:** preserve current MOP visibility rules; scope 
invalidation to match those rules. Making `MetaClassImpl` immune to other 
MetaClasses (and removing the hierarchy walk from it) is a separate semantic 
redesign — see follow-ups below.
   
   Documented on `IndyInvalidation`, `ClassHierarchyIndex`, `package-info`, and 
`ClassInfo.incVersion`.
   
   ## 3. Callsite caching vs hierarchy version
   
   Agreed that **most of your second script is driven by MetaClass change on 
the receiver**, not by a hierarchy version:
   
   | Step | What retires the site | Runtime |
   |------|----------------------|---------|
   | `C` becomes EMC | exact-class domain (receiver MC changed) | `call(c,0) → 
-2` |
   | restore class-level `MetaClassImpl` | exact-class | `→ 40` |
   | `removeMetaClass(C)` + fresh `MetaClassImpl` while `A` has EMC | 
exact-class | `→ -2` (construction snapshot) |
   | `A.metaClass = null` | hierarchy fan-out (parent EMC remove) | still `→ 
-2` (snapshot retained) |
   
   Hierarchy SwitchPoint is only there so **already-linked miss** sites on 
subtypes re-select when an ancestor becomes / stops being a modified mutable 
MetaClass (e.g. linked miss → parent adds method; linked hit → parent EMC 
removed).
   
   Measured note on the last line of your draft (`c.metaClass = emc` after `A` 
is cleared): the draft expected a “clueless” EMC → `40`, but the EMC created 
while `A` still had methods **retains** `ClosureMetaMethod`s for `m2(Integer)` 
/ `m2(String)`, so resolution stays `-2`. Same retention idea as the 
`MetaClassImpl` snapshot — not live super lookup after `A` is gone. Captured in 
`Groovy12191.blackdragScenario_callsiteCaching_metaClassChangeNotHierarchyVersion`.
   
   Pure Java inheritance matches the miss path (`LinkedHashMap` / `HashMap`).
   
   ## 4. Policy (your +1)
   
   Unchanged and reaffirmed:
   
   - pure unmodified `MetaClassImpl` ↔ `MetaClassImpl` (or null) → **exact 
class only**
   - EMC install / remove / in-place update, global EMC, interface / array → 
**class + hierarchy**
   - per-instance MetaClass → **exact class only**
   - category / unscoped → bulk
   
   ## 5. MetaClass-owned SwitchPoint — follow-up
   
   Agreed, and we will file follow-up JIRAs (not landing in this PR):
   
   1. **MetaClass-owned SwitchPoints** for indy MOP invalidation (natural home 
for MC-specific update/replace, true per-instance domains, custom MetaClass 
kinds).  
   2. **`MetaClassImpl` hierarchy immunity** — decide whether `MetaClassImpl` 
should stop observing other MetaClasses (construction-time snapshot + 
`findMethodInClassHierarchy`), so C/D timing is no longer load-order dependent.
   
   I will link the issue keys in `IndyInvalidation` / `package-info` once they 
exist.
   
   ## 6. Regression coverage added
   
   - `blackdragScenario_missingMethodOnly_andMetaClassImplTiming`  
   - `blackdragScenario_callsiteCaching_metaClassChangeNotHierarchyVersion`  
   - `parentEmcAfterLinkedMiss_childCallSiteSeesNewMethod`  
   - `parentEmcRemoveAfterLinkedHit_childCallSiteMissesAgain`  
   - `parentEmc_doesNotReplacePresentChildMethod_afterHierarchyRelink`  
   - `javaSubtype_seesParentEmc_andHierarchyFanOut`  
   
   Happy to adjust further if any row of the matrix still disagrees with the 
behaviour you want for 6.0.




> 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
>             Fix For: 6.0.0-beta-1
>
>
> 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