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

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

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

   # GROOVY-12191 Performance Verification Report V4
   
   **Scoped indy SwitchPoint invalidation — exact-class stock policy (no 
hierarchy fan-out)**
   
   | Item | Value |
   |---|---|
   | Issue | [GROOVY-12191](https://issues.apache.org/jira/browse/GROOVY-12191) 
|
   | Commit under test | `c81e62bccaad66e6d4816884ea40807d3a5ab199` |
   | Baseline (parent) | `0ca665dad0d4070bfa218d9b5577ec423c8b2181` |
   | Date | 2026-08-01 |
   | Verdict | **PASS** — cross-type MetaClass churn no longer globally 
deoptimizes hot monomorphic call sites; parent MetaClass churn no longer 
force-relinks subtype sites under stock policy; steady-state hot path is 
parity; correctness suite green (**94/94**) |
   
   ---
   
   ## 1. Executive summary
   
   This verification compares the **final GROOVY-12191 stack** at `c81e62bcca` 
against the pre-scoping parent `0ca665dad0` (process-wide MOP `SwitchPoint`).
   
   At HEAD, indy MOP invalidation uses:
   
   - **One SwitchPoint domain per MetaClass instance** (weak identity map in 
`IndyInvalidation`).
   - **ClassInfo pending domain** only for pre-MetaClass link (defineClass 
safety); first MetaClass install retires it.
   - **Two-width policy only:**
     - **Exact class** — stock `MetaClassImpl` / EMC changes 
(`invalidateClass`), including `incVersion` and per-instance MetaClass.
     - **All loaded** — category enter/leave, unscoped registry events, and 
non-stock custom MetaClass kinds.
   - **No hierarchy SwitchPoint fan-out** and **no `ClassHierarchyIndex`** — 
ancestor EMC visibility on linked miss sites is live via 
`MetaClass.invokeMethod` / property-miss hierarchy walks (PR #2736 / 
paulk-asert + blackdrag review).
   - Linked call sites still carry **one** `SwitchPoint.guardWithTest` — the 
pre-6.0 monomorphic hot-path shape.
   
   On the same host, JDK, and JMH annotation settings, A/B measurement of 
**HEAD (`c81e62bcca`) vs baseline (`0ca665dad0`)** shows:
   
   1. **Near-zero regression on steady-state monomorphic hot paths** (baseline 
ratios ≈ **1.01–1.07×**, within noise).
   2. **Large gains under cross-type MetaClass churn** — the primary win:
      - `CallSiteInvalidationBench.crossTypeInvalidationEvery1000`: **428.0 → 
2.92 ms/op** (≈ **147×**).
      - `ScopedInvalidationBench.hotLoop_unrelatedMetaClassChurn`: throughput ≈ 
**211×**.
   3. **Framework-style “unrelated burst then steady request loop”** 
(`burstThenSteadyState`): ≈ **101×**.
   4. **Parent MetaClass churn no longer deopts subtype hot sites** (stock 
exact-class):
      - `parentChild_parentMetaClassChurn`: ≈ **130×** vs parent (parent 
collapsed to global-deopt floor; HEAD pays mainly MetaClass-write cost, similar 
to unrelated churn).
   5. **Same-type invalidation and Category semantics are not “falsely 
optimized”**: same-type churn remains far slower than baseline (forced 
re-link); category enter/leave stays bulk and matches parent throughput (~1×).
   6. **Correctness: 94 passed / 0 failed** (scoped-invalidation suite on HEAD; 
hierarchy-index tests removed with the index).
   
   > In one line: without adding a second hot-path guard, the change shrinks 
the blast radius of unrelated (and parent) MetaClass mutations from 
process-global to **exact class** for stock metaclasses, with measured gains on 
the order of **10¹–10²×**, while preserving bulk semantics for Category and 
custom MetaClass kinds.
   
   ---
   
   ## 2. Change mechanism and performance hypotheses
   
   ### 2.1 Previous model (baseline `0ca665dad0`)
   
   - `IndyInterface.switchPoint` was a **process-wide shared** `SwitchPoint`.
   - Any MetaClass registry change → `invalidateSwitchPoints()` → **every** 
linked indy site’s guard failed → mass re-link.
   - Typical symptom: a framework/plugin extends MetaClass on *ColdType* at 
startup or hot-reload; application hot paths on *HotTarget.compute()* were 
deoptimized together with it. The same collapse applied when a **parent** 
type’s MetaClass changed — all process sites paid re-link tax.
   
   ```text
   // baseline (simplified)
   protected static SwitchPoint switchPoint = new SwitchPoint();
   // MetaClassRegistry listener → always:
   synchronized (IndyInterface.class) {
       SwitchPoint old = switchPoint;
       switchPoint = new SwitchPoint();
       SwitchPoint.invalidateAll(new SwitchPoint[]{old});
   }
   ```
   
   ### 2.2 New model (`c81e62bcca`)
   
   | Component | Role |
   |---|---|
   | `SwitchPointInvalidator` | Single-domain lifecycle: CAS `get` / 
`detachLive` / `invalidate` |
   | `IndyInvalidation` | Policy + weak identity map `MetaClass → 
SwitchPointInvalidator`; exact-class vs all-loaded bulk |
   | `ClassInfo` | Pending domain for pre-MetaClass link only; version bumps → 
exact-class invalidation |
   | `Selector` / `IndyCompoundAssign` / `ColdReflectiveMethodHandleWrapper` | 
Install MetaClass-domain (or pending) SwitchPoint guard at link time |
   
   **Hot-path shape (intentionally monomorphic):**
   
   ```text
   handle = mopSwitchPoint.guardWithTest(fast, fallback)
   ```
   
   Still a single guard—matching the pre-6.0 monomorphic shape. There is **no** 
second category SwitchPoint on the hot path.
   
   **Invalidation policy (two widths):**
   
   | Event | Behavior | Expected hot-path cost |
   |---|---|---|
   | Unrelated-type MetaClass change (stock) | Retire **that class’s** domain 
only | Hot sites **do not** re-link |
   | Same-type MetaClass change (stock) | Retire that class’s domain | Must 
re-link (correctness) |
   | Parent EMC / stock MetaClass change | Exact class of **parent only** — 
**no** subtype SwitchPoint fan-out | Subtype sites stay linked; live miss route 
observes ancestor EMC |
   | Non-stock custom MetaClass registry event | Process-wide bulk 
(`invalidateBulk`) | Correctness-first, rare |
   | Category enter/leave, `invalidateCallSites()` | Bulk-retire all loaded 
class domains | Same order of cost as old global invalidation |
   | Unattributed MetaClass event | `invalidateUnscoped()` bulk | Conservative, 
correctness-first |
   
   **What was deliberately removed after PR #2736 review (vs intermediate stack 
at `637bdaf`):**
   
   - `ClassHierarchyIndex` and parent→child SwitchPoint fan-out for stock 
`MetaClassImpl`/EMC.
   - Dual exact APIs / multi-width matrix residue — collapsed to exact vs bulk.
   
   ### 2.3 Hypotheses under test
   
   | ID | Hypothesis | How verified |
   |---|---|---|
   | H1 | Baseline hot loop has no material regression | A/B ratio ≈ 1 |
   | H2 | Cross-type churn throughput/latency improves substantially | 
Unrelated ≫ parent; on HEAD, unrelated ≫ same-type |
   | H3 | Pure hot loop after unrelated burst approaches baseline | 
`afterUnrelatedBurst` ≈ `baseline` |
   | H4a | Same-type / category still pay re-link tax | Clearly slower than 
baseline |
   | H4b | Parent MetaClass churn does **not** force-relink subtype sites 
(stock exact-class) | `parentChild_parentMetaClassChurn` ≫ parent floor; ratio 
to `parentChild_baseline` similar to unrelated-vs-baseline |
   | H5 | Correctness suite stays green | Unit / integration tests |
   
   ---
   
   ## 3. Methodology
   
   ### 3.1 Environment
   
   | Item | Value |
   |---|---|
   | OS | Linux 6.15.5 x86_64 (hostname: `hera`) |
   | CPU | 6 vCPUs visible |
   | Memory | 23 GiB |
   | JDK | Amazon Corretto **25.0.2** (`25.0.2+10-LTS`) |
   | Build | Gradle wrapper, `./gradlew :perf:jmh`, indy enabled by default |
   | Trees | HEAD worktree `@ c81e62bcca`; baseline worktree `@ 0ca665dad0` |
   | Bench source parity | `ScopedInvalidationBench` copied into parent 
worktree for fair A/B (not present on parent history); 
`CallSiteInvalidationBench` workloads identical |
   | Run order | Sequential: HEAD scoped → HEAD callsite → parent scoped → 
parent callsite (avoid dual-JVM core contention); no CPU pinning |
   
   ### 3.2 Benchmark suites
   
   1. **`org.apache.groovy.bench.ScopedInvalidationBench`** (Throughput, 
ops/ms)  
      - 100 000 monomorphic calls per op; MetaClass write every 1000 iterations 
in churn cases.  
      - JMH: `@Warmup(3×2s) @Measurement(5×2s) @Fork(2)` → 10 samples per 
benchmark.  
      - Includes `parentChild_*` rows (stock exact-class: parent churn must 
**not** deopt child).
   
   2. **`org.apache.groovy.perf.grails.CallSiteInvalidationBench`** 
(AverageTime, ms/op)  
      - Same 100 000-iteration loops; cross-type / same-type / burst patterns.  
      - Same JMH annotation settings.
   
   ### 3.3 Correctness
   
   ```bash
   ./gradlew :test --tests Groovy12191 \
     --tests org.apache.groovy.runtime.indy.IndyInvalidationTest \
     --tests org.apache.groovy.runtime.indy.SwitchPointInvalidatorTest \
     --tests org.codehaus.groovy.vmplugin.v8.IndyScopedSwitchPointTest 
--rerun-tasks
   ```
   
   **Result (HEAD `c81e62bcca`): 94 passed / 0 failed / 0 skipped / 0 error.**
   
   | Test class | Cases |
   |---|---:|
   | `bugs.Groovy12191` | 29 |
   | `IndyInvalidationTest` | 41 |
   | `SwitchPointInvalidatorTest` | 12 |
   | `IndyScopedSwitchPointTest` | 12 |
   | **Total** | **94** |
   
   > Note: intermediate PR heads included `ClassHierarchyIndexTest` (~12 
cases). That index and its tests are **intentionally gone** at `c81e62bcca`. 
Behavioural coverage of live miss / property / array lattice lives in 
`Groovy12191` and unit policy tests.
   
   ### 3.4 Statistics and artifacts
   
   - Scores are JMH means; `±` is JMH’s default **99.9% CI**.
   - **Throughput** ratio = HEAD / parent (>1 is better).
   - **AverageTime** speedup = parent / HEAD (>1 is better).
   - Conclusions rest on **order-of-magnitude and structural separation**, not 
±1% micro-deltas.
   
   Raw JSON (local run artifacts, not committed):
   
   - `/tmp/groovy-12191-perf-c81e62bcc/head/scoped-results.json`
   - `/tmp/groovy-12191-perf-c81e62bcc/parent/scoped-results.json`
   - `/tmp/groovy-12191-perf-c81e62bcc/head/callsite-results.json`
   - `/tmp/groovy-12191-perf-c81e62bcc/parent/callsite-results.json`
   - `/tmp/groovy-12191-perf-c81e62bcc/summary.json`
   
   ---
   
   ## 4. Results
   
   ### 4.1 ScopedInvalidationBench (Throughput, ops/ms — higher is better)
   
   | Benchmark | Parent | HEAD | HEAD/Parent | Interpretation |
   |---|---:|---:|---:|---|
   | `hotLoop_baseline` | 3.847 ± 0.207 | 4.017 ± 0.364 | **1.04×** | H1: no 
hot-path regression |
   | `hotLoop_afterUnrelatedBurst` | 3.798 ± 0.677 | 4.432 ± 0.229 | **1.17×** 
| H3: full speed after unrelated burst |
   | `hotLoop_unrelatedMetaClassChurn` | 0.00243 ± 0.00018 | 0.512 ± 0.147 | 
**≈211×** | **H2 primary win** |
   | `hotLoop_sameTypeMetaClassChurn` | 0.00252 ± 0.00011 | 0.0188 ± 0.0012 | 
**≈7.4×** | Still forced re-link; narrower blast radius than parent |
   | `hotLoop_categoryEnterLeave` | 0.00238 ± 0.00022 | 0.00230 ± 0.00034 | 
**≈0.97×** | H4a: bulk semantics retained (parity within noise) |
   | `parentChild_baseline` | 3.234 ± 0.383 | 2.889 ± 0.068 | 0.89× | Both 
full-speed order; container noise |
   | `parentChild_parentMetaClassChurn` | 0.00242 ± 0.00027 | 0.316 ± 0.034 | 
**≈130×** | **H4b: parent churn no longer global-deopts child** |
   
   **Structural fingerprint on HEAD only:**
   
   ```text
   baseline              ≈ 4.02 ops/ms
   after burst           ≈ 4.43 ops/ms   (≥ baseline: no residual deopt)
   unrelated churn       ≈ 0.51 ops/ms   (~8× slower than baseline: mostly 
MetaClass write cost)
   same-type churn       ≈ 0.019 ops/ms  (~214× slower than baseline: write + 
same-class re-link)
   category              ≈ 0.002 ops/ms  (bulk re-link tax)
   parentChild baseline  ≈ 2.89 ops/ms
   parentChild + parent  ≈ 0.32 ops/ms   (~9× slower than parentChild baseline 
— write-dominated, not re-link floor)
   ```
   
   On parent, `unrelated`, `same-type`, and `parentChild_parentMetaClassChurn` 
**all collapse to ~0.002**, showing that the old global SwitchPoint made every 
MetaClass mutation equivalent to “global deopt.”
   
   HEAD separates:
   
   - **unrelated vs same-type** by ~**27×** (0.512 / 0.019) — scoping is live.
   - **parentChild parent-churn vs parent global floor** by ~**130×** — stock 
exact-class means parent EMC writes no longer retire the child’s SwitchPoint.
   
   ### 4.2 CallSiteInvalidationBench (AverageTime, ms/op — lower is better)
   
   | Benchmark | Parent | HEAD | Speedup (P/H) | Interpretation |
   |---|---:|---:|---:|---|
   | `baselineHotLoop` | 0.260 ± 0.022 | 0.254 ± 0.014 | **1.02×** | H1 |
   | `baselineListSize` | 0.241 ± 0.008 | 0.240 ± 0.007 | **1.01×** | H1 |
   | `baselineSteadyStateNoBurst` | 0.608 ± 0.022 | 0.589 ± 0.040 | **1.03×** | 
H1 |
   | `baselineMultipleCallSites` | 20.86 ± 1.48 | 19.41 ± 1.46 | **1.07×** | H1 
|
   | `crossTypeInvalidationEvery10000` | 301.3 ± 20.4 | 2.462 ± 0.360 | 
**≈122×** | H2 |
   | `crossTypeInvalidationEvery1000` | 428.0 ± 35.2 | 2.921 ± 0.596 | 
**≈147×** | **H2 primary win** |
   | `crossTypeInvalidationEvery100` | 134.8 ± 6.6 | 7.393 ± 0.250 | **≈18×** | 
Still large under denser churn |
   | `listSizeWithCrossTypeInvalidation` | 311.6 ± 26.5 | 1.657 ± 0.466 | 
**≈188×** | JDK-type hot sites benefit too |
   | `multipleCallSitesWithInvalidation` | 1081 ± 89 | 24.93 ± 3.42 | **≈43×** 
| Multi-site still much improved |
   | `burstThenSteadyState` | 141.9 ± 19.3 | 1.406 ± 0.203 | **≈101×** | 
“Framework MC extend + request steady state” |
   | `sameTypeInvalidationEvery1000` | 401.4 ± 31.0 | 51.58 ± 4.15 | **≈7.8×** 
| Same-type still slow; local invalidation cheaper than global rotate |
   
   **Penalty vs baseline on HEAD:**
   
   | Scenario | Relative cost | Meaning |
   |---|---:|---|
   | crossType @1000 | 2.92 / 0.25 ≈ **12×** `baselineHotLoop` | Dominated by 
~100 ColdType MetaClass writes, not hot-site deopt |
   | sameType @1000 | 51.6 / 0.25 ≈ **203×** `baselineHotLoop` | Clear re-link 
tax |
   | burstThenSteady | 1.41 / 0.59 ≈ **2.4×** steady baseline | Burst write 
cost dominates; steady calls recovered |
   
   On parent, crossType@1000 and sameType@1000 are **both ~400+ ms/op**, again 
confirming global invalidation collapsed cross-type into full deopt.
   
   ---
   
   ## 5. Why it is faster
   
   ```text
                       Old path                              New path (stock)
     ColdType.metaClass.foo = ...           ColdType.metaClass.foo = ...
               │                                      │
               ▼                                      ▼
      invalidate global SwitchPoint        retire MetaClass(ColdType) domain 
only
               │                                      │
               ▼                                      ▼
     ALL sites: guard fails                ColdType sites only
     HotTarget.compute → full re-select    HotTarget.compute → stays linked
     (MH rebuild / re-guard / cold path)   (single SwitchPoint check passes)
   
     Parent.metaClass.bar = ...             Parent.metaClass.bar = ...
               │                                      │
               ▼                                      ▼
      global SwitchPoint (same collapse)   retire Parent domain only
               │                           Child linked sites stay warm
               ▼                           Live miss path sees Parent EMC
     Child sites re-link (unnecessary)     without SwitchPoint fan-out
   ```
   
   Cost breakdown:
   
   1. **Avoided re-links** — After each global invalidation, hot sites re-run 
`Selector`, reinstall guards, and rewrite `CallSite`s. At 100 churn events × 
many sites this dominates (parent’s 400+ ms/op).
   2. **Avoided JIT churn** — Frequent process-wide `SwitchPoint.invalidateAll` 
breaks monomorphic inlining assumptions and amplifies deopt/recompile cost.
   3. **Unchanged hot-path guard count** — Still one `guardWithTest`, so 
baselines stay flat (H1).
   4. **Same-type still correctly charged** — Prevents “fast but wrong” 
semantics when the receiver’s own MetaClass changes (H4a + 94 tests).
   5. **Parent EMC no longer over-invalidates subtypes** — stock exact-class 
policy; miss-path self-heals via live hierarchy walk (H4b; PR #2736 experiment).
   6. **Category remains bulk** — No second hot-path guard; category visibility 
stays simple and correct; cost matches the old global path (measured ~1.0×).
   
   Same-type still improves ~**8×** on HEAD vs parent, not because re-link is 
skipped, but because the invalidation surface shrinks from “all process sites” 
to “this class” and batch `invalidateAll` is cheaper—a secondary benefit, not 
the primary goal.
   
   ---
   
   ## 6. Risks and boundaries
   
   | Topic | Assessment |
   |---|---|
   | Hot-path regression | **Not observed** (baselines 1.01–1.07×) |
   | Semantic regression | 94/94 scoped suite green; Category / per-instance / 
MetaClass identity domain / live miss & property probes covered |
   | Dense Category enter/leave | **Still expensive** (by design); 
Category-heavy workloads do not get faster |
   | Parent EMC / hierarchy | **No SwitchPoint fan-out** for stock; visibility 
via live miss walk. Construction-time MetaClassImpl snapshots remain 
pre-existing MOP behaviour |
   | Custom non-`MetaClassImpl` | Process-wide bulk on registry replace 
(correctness-first, rare) |
   | Legacy `IndyInterface.switchPoint` | **Removed** on this branch 
(`vmplugin` internal-by-intent); external code must use `IndyInvalidation` |
   | Environment noise | Shared/container CPUs; conclusions use 
order-of-magnitude gaps (10¹–10²×), robust to ±10% noise |
   | Not covered here | Concurrent multi-thread churn throughput, full Grails 
end-to-end, JDK 17/21 matrix (recommended for CI) |
   
   ---
   
   ## 7. Hypothesis scorecard
   
   | Hypothesis | Result | Evidence |
   |---|---|---|
   | H1 baseline parity | **Holds** | Scoped baseline 1.04×; CallSite baselines 
1.01–1.07× |
   | H2 cross-type speedup | **Holds** | ≈211× thrpt; ≈147× avgt (@1000) |
   | H3 post-unrelated-burst full speed | **Holds** | `afterUnrelatedBurst` ≥ 
baseline on HEAD |
   | H4a necessary invalidation still paid | **Holds** | same-type / category ≪ 
baseline |
   | H4b parent churn does not fan out | **Holds** | parentChild parent-churn 
≈130× vs parent floor; write-dominated vs child baseline |
   | H5 correctness | **Holds** | 94/94 tests |
   
   ---
   
   ## 8. Comparison to V3 report (`637bdaf` stack)
   
   | Aspect | V3 (`637bdaf`) | V4 (`c81e62bcca`, this report) |
   |---|---|---|
   | Per-MetaClass SP domains | Yes | Yes (unchanged win) |
   | Hierarchy fan-out / `ClassHierarchyIndex` | Yes (EMC / array / interface) 
| **Removed** for stock |
   | Cross-type win | ~151–171× | **~147–211×** (same order) |
   | Parent→child SP on parent EMC | Forced child re-link | **Child SP stays 
live**; live miss path |
   | API surface | Multi-width matrix | **Exact vs bulk only** |
   | Correctness count | 107 (incl. index tests) | **94** (index tests gone; 
more behaviour probes) |
   
   The primary performance story (**cross-type deopt removal**) is unchanged. 
V4 additionally removes inert hierarchy invalidations, which **improves** 
parent-churn isolation for stock metaclasses and simplifies the machine.
   
   ---
   
   ## 9. Conclusions and recommendations
   
   ### Verdict
   
   `c81e62bcca` 
([GROOVY-12191](https://issues.apache.org/jira/browse/GROOVY-12191)) keeps a 
**single-guard monomorphic hot path** while scoping indy MOP invalidation to 
**per-MetaClass domains** with **exact-class stock policy** (bulk only for 
Category / unscoped / custom MetaClass). For **unrelated-type MetaClass churn 
with hot monomorphic call sites**—a Grails/framework-shaped load—measured 
improvement is on the order of **10¹–10²×**. **Parent MetaClass churn** no 
longer force-relinks subtype sites under stock policy. Steady-state undisturbed 
paths show **no material regression**. Correctness costs for Category and 
same-type changes **remain visible and intentional**.
   
   **Performance verification: PASS.**
   
   ### Recommendations
   
   1. Keep `ScopedInvalidationBench` (including `parentChild_*`) and the 
cross-type / burst rows of `CallSiteInvalidationBench` in regular JMH 
regression so a return to a global SwitchPoint (or accidental reintroduction of 
hierarchy fan-out) cannot land silently.
   2. In release notes, state clearly that Category and unscoped MetaClass 
events may still bulk-invalidate; the win is **type-/MetaClass-local stock 
MetaClass change**, including parent EMC without subtype SP fan-out.
   3. Optionally re-run CallSite `crossType@1000` and baselines on JDK 17/21 to 
confirm cross-LTS consistency.
   4. Document that MOP guards are resolved via `IndyInvalidation` (MetaClass 
identity map + ClassInfo pending), not a process-wide 
`IndyInterface.switchPoint`.
   
   ---
   
   ## Appendix A — Reproduction commands
   
   ```bash
   # Correctness (HEAD @ c81e62bcca)
   ./gradlew :test --tests Groovy12191 \
     --tests org.apache.groovy.runtime.indy.IndyInvalidationTest \
     --tests org.apache.groovy.runtime.indy.SwitchPointInvalidatorTest \
     --tests org.codehaus.groovy.vmplugin.v8.IndyScopedSwitchPointTest 
--rerun-tasks
   
   # Performance (HEAD)
   ./gradlew :perf:jmh -PbenchInclude=ScopedInvalidation -PjmhResultFormat=JSON
   ./gradlew :perf:jmh -PbenchInclude=CallSiteInvalidation 
-PjmhResultFormat=JSON
   
   # Performance (parent @ 0ca665dad0; copy ScopedInvalidationBench sources for 
fair A/B)
   # Run the same two :perf:jmh invocations in a clean worktree at the parent 
commit.
   ```
   
   ## Appendix B — Commits between baseline and HEAD
   
   ```text
   c81e62bcca GROOVY-12191: Drop hierarchy SwitchPoint fan-out for stock 
metaclasses
   637bdaf68a GROOVY-12191: Attach indy MOP SwitchPoints to MetaClass instances
   3558689a06 GROOVY-12191: Document missing-method hierarchy and add blackdrag 
scenario tests
   0303da79dc GROOVY-12191: MetaClass-aware SwitchPoint fan-out and PIC 
sentinel rename
   1a31f276ca GROOVY-12191: Address blackdrag review on scoped SwitchPoint 
invalidation
   f48ab19256 GROOVY-12191: Address PR review and add index hierarchy for 
SwitchPoint fan-out
   ae8e49741e GROOVY-12191: Scope indy SwitchPoint invalidation
   ```
   
   ## Appendix C — Key files (HEAD)
   
   - `org.apache.groovy.runtime.indy.IndyInvalidation` — MetaClass identity 
domains + exact/bulk policy
   - `org.apache.groovy.runtime.indy.SwitchPointInvalidator`
   - `ClassInfo` — pending domain + version coordination
   - `IndyInterface` / `Selector` / `ColdReflectiveMethodHandleWrapper` / 
`IndyCompoundAssign` — link-time guards
   - Tests: `Groovy12191`, `IndyInvalidationTest`, 
`SwitchPointInvalidatorTest`, `IndyScopedSwitchPointTest`
   - Benchmarks: `ScopedInvalidationBench`, `CallSiteInvalidationBench`
   
   ## Appendix D — Headline numbers (copy-ready)
   
   | Metric | Parent | HEAD | Gain |
   |---|---:|---:|---:|
   | Scoped unrelated thrpt | 0.00243 ops/ms | 0.512 ops/ms | **211×** |
   | CallSite crossType@1000 | 428 ms/op | 2.92 ms/op | **147×** |
   | CallSite burstThenSteady | 142 ms/op | 1.41 ms/op | **101×** |
   | Scoped parentChild parent-churn | 0.00242 ops/ms | 0.316 ops/ms | **130×** 
|
   | Scoped / CallSite baselines | — | — | **1.01–1.07×** (parity) |
   | Correctness | — | 94/94 | **green** |
   
   ---
   
   *Report generated from local JMH A/B measurements on 2026-08-01 (hostname 
`hera`, Corretto 25.0.2). Raw JSON and run logs: 
`/tmp/groovy-12191-perf-c81e62bcc/`.*
   




> 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