You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/07/31 16:51:19 UTC
(groovy) branch GROOVY-12191 updated: GROOVY-12191: Drop hierarchy SwitchPoint fan-out for stock metaclasses
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-12191
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-12191 by this push:
new c81e62bcca GROOVY-12191: Drop hierarchy SwitchPoint fan-out for stock metaclasses
c81e62bcca is described below
commit c81e62bccaad66e6d4816884ea40807d3a5ab199
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Aug 1 01:50:00 2026 +0900
GROOVY-12191: Drop hierarchy SwitchPoint fan-out for stock metaclasses
---
src/main/java/groovy/lang/MetaClassImpl.java | 14 +-
.../groovy/runtime/indy/ClassHierarchyIndex.java | 169 -------
.../groovy/runtime/indy/IndyInvalidation.java | 486 ++++++---------------
.../runtime/indy/SwitchPointInvalidator.java | 12 +-
.../apache/groovy/runtime/indy/package-info.java | 24 +-
.../org/codehaus/groovy/reflection/ClassInfo.java | 42 +-
.../codehaus/groovy/vmplugin/v8/IndyInterface.java | 13 +-
src/test/groovy/bugs/Groovy12191.groovy | 309 ++++++++-----
.../runtime/indy/ClassHierarchyIndexTest.groovy | 191 --------
.../runtime/indy/IndyInvalidationTest.groovy | 363 ++++++---------
.../vmplugin/v8/IndyScopedSwitchPointTest.groovy | 2 +-
.../runtime/indy/IndyInvalidationStatsProbe.java | 1 +
subprojects/performance/README.adoc | 11 +-
.../groovy/bench/ScopedInvalidationBench.groovy | 28 +-
14 files changed, 546 insertions(+), 1119 deletions(-)
diff --git a/src/main/java/groovy/lang/MetaClassImpl.java b/src/main/java/groovy/lang/MetaClassImpl.java
index 5db08394e8..2bc9e837c0 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -3903,7 +3903,9 @@ public class MetaClassImpl implements MetaClass, MutableMetaClass {
}
/**
- * Searches the class hierarchy for a mutable meta-class bean property.
+ * Searches the class hierarchy for a mutable meta-class bean property on the
+ * missing-property path. Linked miss sites re-enter this walk live (no
+ * SwitchPoint fan-out); see {@link org.apache.groovy.runtime.indy.IndyInvalidation}.
*
* @param propertyName the property name
* @param theClass the class to start searching from
@@ -3983,13 +3985,9 @@ public class MetaClassImpl implements MetaClass, MutableMetaClass {
* {@code Object.metaClass.foo = …} / parent EMC methods become visible on
* subclass receivers without giving every subtype its own EMC.
* <p>
- * <b>6.0 decision (GROOVY-12191):</b> {@code MetaClassImpl} continues to
- * observe ancestor MetaClass state here. Making {@code MetaClassImpl} immune
- * to other MetaClasses would remove this walk and the construction-time
- * snapshot of ancestor expando methods, but would also break the inheritance
- * patterns above. Indy hierarchy SwitchPoint fan-out exists solely so
- * already-linked subtype sites re-select when this walk’s result can change
- * while the receiver’s own MetaClass stays unchanged.
+ * <b>6.0 (GROOVY-12191):</b> this walk is retained. Linked miss sites re-enter
+ * it live without SwitchPoint fan-out — see
+ * {@link org.apache.groovy.runtime.indy.IndyInvalidation}.
*
* @param instanceKlazz the runtime receiver class
* @param methodName the method name
diff --git a/src/main/java/org/apache/groovy/runtime/indy/ClassHierarchyIndex.java b/src/main/java/org/apache/groovy/runtime/indy/ClassHierarchyIndex.java
deleted file mode 100644
index 7a4606a3d1..0000000000
--- a/src/main/java/org/apache/groovy/runtime/indy/ClassHierarchyIndex.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.groovy.runtime.indy;
-
-import groovy.transform.Internal;
-import org.apache.groovy.util.concurrent.ManagedIdentityConcurrentMap;
-import org.codehaus.groovy.reflection.ClassInfo;
-import org.codehaus.groovy.util.ManagedConcurrentLinkedQueue;
-import org.codehaus.groovy.util.ReferenceBundle;
-
-import java.io.Serializable;
-import java.lang.reflect.Array;
-import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
-/**
- * Ancestor → descendant {@link ClassInfo} index for scoped SwitchPoint fan-out
- * (GROOVY-12191).
- * <p>
- * Built once per {@link ClassInfo} at construction: each type is registered under
- * every MOP-relevant strict supertype (superclass chain, transitive interfaces,
- * and the full JLS array-covariance lattice). Hierarchy invalidation for type
- * {@code T} then walks only the descendants of {@code T} —
- * {@code O(|subtypes of T|)} — instead of scanning every loaded
- * {@code ClassInfo} with {@code isAssignableFrom}.
- * <p>
- * The index is consulted when MetaClass-aware policy chooses hierarchy fan-out
- * (EMC install/update, global EMC, interface / array MetaClass changes). It is
- * not modelling a shared {@code MetaClassImpl} table — each class still has its
- * own MetaClass instance domain (weak identity map in {@link IndyInvalidation}).
- * Subtype domains re-link so the live <em>missing-method</em> hierarchy walk
- * can observe ancestor expando methods. Pure {@code MetaClassImpl} replaces do
- * not walk this index (exact-class only).
- * <p>
- * Keys and values are weakly held so the index does not pin classes or
- * {@code ClassInfo} instances after they become unreachable.
- * <p>
- * <strong>Not a public API.</strong> Wired only from {@link ClassInfo} and
- * {@link IndyInvalidation}.
- *
- * @since 6.0.0
- */
-@Internal
-public final class ClassHierarchyIndex {
-
- private static final ReferenceBundle WEAK = ReferenceBundle.getWeakBundle();
-
- /**
- * Ancestor class (weak, identity) → weakly-reachable descendant ClassInfos.
- * A type is never registered under itself.
- */
- private static final ManagedIdentityConcurrentMap<Class<?>, ManagedConcurrentLinkedQueue<ClassInfo>> BY_ANCESTOR =
- new ManagedIdentityConcurrentMap<>(256);
-
- private ClassHierarchyIndex() {
- }
-
- /**
- * Indexes {@code info} under every strict supertype of its class.
- * Called exactly once from the {@link ClassInfo} constructor while the
- * {@link Class} is strongly reachable.
- *
- * @param info the newly constructed ClassInfo
- */
- @Internal
- public static void register(final ClassInfo info) {
- Class<?> type = info.getTheClass();
- if (type == null) {
- return;
- }
- Set<Class<?>> ancestors = new LinkedHashSet<>();
- collectStrictSupertypes(type, ancestors);
- for (Class<?> ancestor : ancestors) {
- addUnder(ancestor, info);
- }
- }
-
- private static void addUnder(final Class<?> ancestor, final ClassInfo info) {
- ManagedConcurrentLinkedQueue<ClassInfo> queue =
- BY_ANCESTOR.applyIfAbsent(ancestor, k -> new ManagedConcurrentLinkedQueue<>(WEAK));
- queue.add(info);
- }
-
- /**
- * Appends every ClassInfo registered as a descendant of {@code ancestor}
- * (not including {@code ancestor} itself) to {@code out}. Empty when no
- * subtypes have been indexed (e.g. finals, primitives, or unused parents).
- *
- * @param ancestor the root of the fan-out
- * @param out destination collection
- */
- @Internal
- public static void collectDescendants(final Class<?> ancestor, final Collection<ClassInfo> out) {
- ManagedConcurrentLinkedQueue<ClassInfo> queue = BY_ANCESTOR.get(ancestor);
- if (queue == null) {
- return;
- }
- for (ClassInfo info : queue) {
- // Iterator of ManagedConcurrentLinkedQueue skips cleared weak refs.
- out.add(info);
- }
- }
-
- /**
- * Collects every strict supertype {@code S} of {@code type} such that
- * {@code S.isAssignableFrom(type) && S != type}, computed structurally from
- * the type's hierarchy (no global ClassInfo scan).
- * <p>
- * Array types follow the JLS covariance lattice: for component {@code C},
- * every supertype {@code S} of {@code C} (including {@code C}) yields
- * {@code S[]} as a supertype of {@code C[]}, plus {@code Object},
- * {@code Cloneable}, and {@code Serializable}.
- *
- * @param type the type whose strict supertypes are collected
- * @param out destination set
- */
- static void collectStrictSupertypes(final Class<?> type, final Set<Class<?>> out) {
- if (type == null || type == Object.class || type.isPrimitive()) {
- return;
- }
- if (type.isArray()) {
- out.add(Object.class);
- out.add(Cloneable.class);
- out.add(Serializable.class);
- Class<?> component = type.getComponentType();
- // {component} ∪ strict-supers(component) — each lifted to a 1-D array.
- Set<Class<?>> componentLattice = new LinkedHashSet<>();
- componentLattice.add(component);
- collectStrictSupertypes(component, componentLattice);
- for (Class<?> componentType : componentLattice) {
- Class<?> arrayOf = Array.newInstance(componentType, 0).getClass();
- if (arrayOf != type) {
- out.add(arrayOf);
- }
- }
- return;
- }
- Class<?> superclass = type.getSuperclass();
- if (superclass != null) {
- out.add(superclass);
- collectStrictSupertypes(superclass, out);
- } else if (type.isInterface()) {
- // Class.getSuperclass() is null for interfaces; assignability still
- // treats Object as a supertype (Object.isAssignableFrom(iface) == true).
- out.add(Object.class);
- }
- for (Class<?> iface : type.getInterfaces()) {
- out.add(iface);
- collectStrictSupertypes(iface, out);
- }
- }
-}
diff --git a/src/main/java/org/apache/groovy/runtime/indy/IndyInvalidation.java b/src/main/java/org/apache/groovy/runtime/indy/IndyInvalidation.java
index f3c4d274f2..c45793f3e7 100644
--- a/src/main/java/org/apache/groovy/runtime/indy/IndyInvalidation.java
+++ b/src/main/java/org/apache/groovy/runtime/indy/IndyInvalidation.java
@@ -20,14 +20,9 @@ package org.apache.groovy.runtime.indy;
import groovy.lang.AdaptingMetaClass;
import groovy.lang.DelegatingMetaClass;
-import groovy.lang.ExpandoMetaClass;
-import groovy.lang.ExpandoMetaClassCreationHandle;
-import groovy.lang.GroovySystem;
import groovy.lang.MetaClass;
import groovy.lang.MetaClassImpl;
-import groovy.lang.MetaClassRegistry;
import groovy.lang.MetaClassRegistryChangeEvent;
-import groovy.lang.MutableMetaClass;
import org.apache.groovy.util.SystemUtil;
import org.apache.groovy.util.concurrent.ManagedIdentityConcurrentMap;
import org.codehaus.groovy.reflection.ClassInfo;
@@ -46,137 +41,33 @@ import java.util.logging.Logger;
* Scoped invokedynamic {@link SwitchPoint} invalidation for the Groovy MOP
* (GROOVY-12191).
* <p>
- * <b>Model (single domain):</b> one SwitchPoint domain per
- * <em>{@link MetaClass} instance</em>, held in a weak identity map in this
- * class — not on {@link ClassInfo}, and not as fields on {@link MetaClassImpl}.
- * Every MetaClass kind (EMC, plain {@code MetaClassImpl}, custom) uses the same
- * mechanism. Monomorphic indy sites install the SwitchPoint of the class-level
- * MetaClass observed at link time — a single {@code guardWithTest} — while
- * avoiding process-wide deopt on unrelated MetaClass churn.
- * <p>
- * When no class-level MetaClass is installed yet (e.g. link during
- * {@code defineClass} before nested types exist), sites use a
- * <em>pending</em> SwitchPoint on {@link ClassInfo} (live domain, retired on
- * first MetaClass install). An already-invalid SwitchPoint cannot be used:
- * monomorphic caching would re-enter fallback forever. After a MetaClass
- * exists, only that instance’s domain is used — never both. Per-instance
- * MetaClass still forces uncacheable PIC entries ({@code canSetTarget=false}).
- *
- * <h2>Why hierarchy fan-out still exists (and what it is not)</h2>
- * <p>
- * Hierarchy fan-out is <em>not</em> “{@code MetaClassImpl} shares one method table
- * up the hierarchy”. Each class still has its own MetaClass domain. Fan-out
- * exists solely because <em>selection can observe ancestor MetaClass state</em>
- * without the receiver’s MetaClass changing:
- * <ol>
- * <li><b>Missing-method path only (live) — the only case that requires
- * parent→child SwitchPoint fan-out:</b>
- * {@code MetaClassImpl.findMethodInClassHierarchy} returns immediately
- * unless some strong MetaClass in the receiver hierarchy is a
- * <em>modified</em> {@link MutableMetaClass} (EMC is the common case).
- * When that gate opens, a previously linked miss on a subtype can become
- * a hit (parent adds method), and a previously linked hierarchy hit can
- * become a miss (parent EMC removed). Present methods on a child keep
- * winning for applicable arguments after re-link (the walk does not
- * rebuild MetaClass tables).</li>
- * <li><b>Construction-time snapshot (MetaClassImpl init) — not fixed by SP:</b>
- * when a {@code MetaClassImpl} is first built <em>after</em> an ancestor
- * already carries expando methods, those methods can be copied into the
- * child’s method index. A sibling whose MetaClass was built <em>before</em>
- * the ancestor change keeps the older view. SwitchPoint retirement
- * re-links against the <em>same</em> MetaClass instance and does not
- * rebuild tables.</li>
- * </ol>
- * <p>
- * <b>6.0 decision on MetaClassImpl “hierarchy immunity”:</b>
- * {@code MetaClassImpl} <em>continues</em> to observe ancestor MetaClass state
- * on the missing-method path. Removing that walk would break well-used patterns
- * such as {@code Object.metaClass.foo = …} / parent EMC methods visible on
- * subclasses without giving every subtype its own EMC. Hierarchy SwitchPoint
- * fan-out is therefore retained, but only for the missing-method cases above —
- * pure unmodified {@code MetaClassImpl} ↔ {@code MetaClassImpl} (or null)
- * replace stays <b>exact-class</b>. Most “metaclass changed on the receiver”
- * cases are exact-class — the SwitchPoint retires because <em>that</em>
- * MetaClass changed, not because of hierarchy versioning.
+ * <b>Domain:</b> one SwitchPoint per {@link MetaClass} instance (weak identity
+ * map here). When no class-level MetaClass is installed yet, sites use a
+ * ClassInfo <em>pending</em> domain (retired on first install). Monomorphic
+ * indy sites install a single {@code guardWithTest} on the class-level domain
+ * observed at link time.
*
- * <h2>When do we invalidate, and how wide?</h2>
- *
- * <table border="1" summary="Invalidation policy">
- * <tr><th>Event</th><th>Domain retired</th><th>Why</th></tr>
- * <tr>
- * <td>EMC method/property update ({@link ClassInfo#incVersion()})</td>
- * <td>class + loaded subtypes / implementors / array lattice</td>
- * <td>In-place EMC update can make a previously missing name resolvable on
- * subtypes via the missing-method hierarchy walk (and can change
- * already-linked miss targets). Present methods on the child keep
- * winning after re-link.</td>
- * </tr>
- * <tr>
- * <td>Registry replace where old or new MC is {@link ExpandoMetaClass}</td>
- * <td>class + hierarchy</td>
- * <td>Installing / removing EMC changes cross-class MOP visibility for
- * already-linked subtype sites (miss path / EMC children).</td>
- * </tr>
- * <tr>
- * <td>Registry replace while global EMC creation handle is active</td>
- * <td>class + hierarchy</td>
- * <td>Every class MetaClass is an EMC; inheritance refresh can republish
- * super methods into subtypes.</td>
- * </tr>
- * <tr>
- * <td>Registry replace of an interface or array type</td>
- * <td>class + hierarchy (array lattice indexed)</td>
- * <td>Interface / array MetaClasses participate in the same hierarchy walk
- * (e.g. {@code Object[].metaClass.foo} is visible on {@code String[]}).</td>
- * </tr>
- * <tr>
- * <td>Class replace where <em>both</em> old and new MetaClass are
- * hierarchy-local (null, or unmodified {@code MetaClassImpl} /
- * non-EMC subclass with {@code isModified()==false})</td>
- * <td><b>exact class only</b></td>
- * <td>Each class owns its own tables; parent replace does not open
- * {@code findMethodInClassHierarchy}. Over-fan-out would re-link
- * subtype sites without a MOP-visibility reason.</td>
- * </tr>
- * <tr>
- * <td>Class replace involving unknown / custom MetaClass kinds</td>
- * <td>class + hierarchy</td>
- * <td>Correctness-first: custom MetaClasses may publish cross-class state;
- * exact-only is an allow-list, not a deny-list of EMC alone.</td>
- * </tr>
- * <tr>
- * <td>Per-instance MetaClass change</td>
- * <td>exact class only</td>
- * <td>Only that class's monomorphic sites must re-select; per-instance
- * dispatch is already uncacheable in the PIC. Subtypes are unaffected.</td>
- * </tr>
- * <tr>
- * <td>Category enter/leave, {@code invalidateCallSites()}, unscoped event</td>
- * <td>all loaded class domains</td>
- * <td>Category state is process-wide; no second hot-path guard.</td>
- * </tr>
- * </table>
- *
- * <p>
- * <b>Over- vs under-invalidation:</b> exact-class is an <em>allow-list</em>
- * (hierarchy-local MetaClasses only). Ambiguous or custom MetaClass kinds fan
- * out. {@link #invalidateClass(Class)} / {@code incVersion} always fan out
- * (EMC in-place updates). Linked sites for a method that did not change may
- * still re-link when their receiver class domain is retired — that is the
- * monomorphic SwitchPoint trade-off (same as pre-6.0, but scoped). Hierarchy
- * fan-out deliberately over-invalidates subtype sites whose present methods
- * are unchanged, so that sites which previously linked a <em>miss</em> cannot
- * stay stale; the registry cannot cheaply know which names were linked.
- * <p>
- * <b>Scalability:</b> hierarchy fan-out is {@code O(|loaded subtypes of T|)}
- * via {@link ClassHierarchyIndex}. Category bulk remains a full ClassInfo walk
- * with cheap no-op detaches for domains that never allocated a SwitchPoint.
+ * <h2>Width policy (two axes only)</h2>
+ * <ul>
+ * <li><b>Exact class</b> — {@link #invalidateClass(Class)}: stock
+ * {@code MetaClassImpl} / EMC changes (including {@link ClassInfo#incVersion()},
+ * registry replace, per-instance MetaClass).</li>
+ * <li><b>All loaded</b> — process-wide retire used for category enter/leave
+ * ({@link #invalidateCategory()}), unattributed registry events
+ * ({@link #invalidateUnscoped()}), and non-stock custom MetaClass kinds
+ * ({@link #invalidateBulk()}).</li>
+ * </ul>
+ * There is <em>no</em> parent→child SwitchPoint fan-out. Indy selection consults
+ * only the receiver’s own MetaClass tables; ancestor-dependent resolution on
+ * the missing-method / missing-property path re-enters
+ * {@code MetaClass.invokeMethod} / property miss and walks the hierarchy live
+ * from the registry each call. Construction-time snapshots of ancestor expando
+ * methods are pre-existing MOP behaviour and are not refreshed by SwitchPoint
+ * retirement. See also {@link MetaClassImpl#findMethodInClassHierarchy}.
* <p>
- * Install guards with
- * {@link #guardWithMopSwitchPoints(MethodHandle, MethodHandle, Object)}
- * (public / test entry). Production Selector wiring uses
- * {@code IndyInterface.applyMopSwitchPoints}.
- * Optional stats logging: {@code -Dgroovy.indy.invalidation.stats=true}.
+ * Optional stats: {@code -Dgroovy.indy.invalidation.stats=true}.
+ * Production guards: {@code IndyInterface.applyMopSwitchPoints}; tests may use
+ * {@link #guardWithMopSwitchPoints}.
*
* @since 6.0.0
*/
@@ -185,10 +76,15 @@ public final class IndyInvalidation {
private static final Logger LOG = Logger.getLogger(IndyInvalidation.class.getName());
private static final boolean STATS_LOG = SystemUtil.getBooleanSafe("groovy.indy.invalidation.stats");
+ /** Exact-class invalidation events ({@link #invalidateClass(Class)}). */
private static final AtomicLong CLASS_INVALIDATIONS = new AtomicLong();
+ /** Category enter/leave bulk events ({@link #invalidateCategory()}). */
private static final AtomicLong CATEGORY_INVALIDATIONS = new AtomicLong();
- private static final AtomicLong EXACT_ONLY_INVALIDATIONS = new AtomicLong();
- private static final AtomicLong HIERARCHY_INVALIDATIONS = new AtomicLong();
+ /**
+ * Non-category process-wide bulk events
+ * ({@link #invalidateBulk()} / {@link #invalidateUnscoped()}).
+ */
+ private static final AtomicLong BULK_INVALIDATIONS = new AtomicLong();
private static final SwitchPoint[] EMPTY_SWITCH_POINTS = new SwitchPoint[0];
@@ -212,32 +108,18 @@ public final class IndyInvalidation {
return new SwitchPointInvalidator();
}
- /**
- * Invalidates SwitchPoints for category enter/leave and
- * {@code VMPlugin.invalidateCallSites()}.
- * <p>
- * Implemented as a bulk class-domain invalidation so every linked site
- * re-links under the new category state. There is no separate category
- * SwitchPoint on the hot path.
- */
- public static void invalidateCategory() {
- invalidateAllLoadedClassSwitchPoints();
- CATEGORY_INVALIDATIONS.incrementAndGet();
- if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
- LOG.fine("category invalidation via bulk class SwitchPoints; total="
- + CATEGORY_INVALIDATIONS.get());
- }
- }
+ // -------------------------------------------------------------------------
+ // Width: exact class
+ // -------------------------------------------------------------------------
/**
- * Invalidates the per-class SwitchPoint for {@code type} and every loaded
- * subtype/implementor (hierarchy fan-out). No-op when {@code type} is
- * {@code null}. Does not bump {@link ClassInfo#getVersion()} on subtypes.
+ * Retires only {@code type}'s MetaClass SwitchPoint domain (exact class;
+ * no subtype fan-out). No-op when {@code type} is {@code null}. Does not
+ * bump {@link ClassInfo#getVersion()}.
* <p>
- * Used by {@link ClassInfo#incVersion()} (EMC method updates) and any
- * caller that needs the conservative full-hierarchy policy. Registry
- * listeners prefer {@link #invalidateForMetaClassChange(MetaClassRegistryChangeEvent)}
- * so pure {@code MetaClassImpl} replacements stay exact-class.
+ * Used by {@link ClassInfo#incVersion()}, stock registry MetaClass changes,
+ * and per-instance MetaClass changes. Non-stock MetaClass kinds use
+ * {@link #invalidateBulk()} instead.
*
* @param type the class whose MetaClass changed
*/
@@ -245,40 +127,81 @@ public final class IndyInvalidation {
if (type == null) {
return;
}
- invalidateClassHierarchy(type);
+ List<SwitchPoint> batch = new ArrayList<>(1);
+ collectLiveForClass(type, batch);
+ invalidateBatch(batch);
CLASS_INVALIDATIONS.incrementAndGet();
- HIERARCHY_INVALIDATIONS.incrementAndGet();
if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
- LOG.fine("class SwitchPoint hierarchy invalidated for " + type.getName()
- + "; total=" + CLASS_INVALIDATIONS.get());
+ LOG.fine("exact-class SwitchPoint invalidated for " + type.getName()
+ + "; totalClass=" + CLASS_INVALIDATIONS.get());
}
}
+ // -------------------------------------------------------------------------
+ // Width: all loaded (one primitive, reason-labelled public entries)
+ // -------------------------------------------------------------------------
+
/**
- * Retires only {@code type}'s MetaClass SwitchPoint domain (no subtype fan-out).
- * Used when MetaClass change cannot affect cross-class MOP visibility
- * (pure {@code MetaClassImpl} class replace, per-instance MetaClass).
- *
- * @param type the class whose domain is retired; no-op when {@code null}
+ * Category enter/leave and {@code VMPlugin.invalidateCallSites()}.
+ * Retires every loaded class-level domain so sites re-link under the new
+ * category state. No separate category SwitchPoint on the hot path.
*/
- public static void invalidateClassExact(final Class<?> type) {
- if (type == null) {
- return;
+ public static void invalidateCategory() {
+ retireAllLoadedDomains();
+ CATEGORY_INVALIDATIONS.incrementAndGet();
+ if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
+ LOG.fine("category bulk invalidation; total=" + CATEGORY_INVALIDATIONS.get());
}
- List<SwitchPoint> batch = new ArrayList<>(1);
- collectLiveForClass(type, batch);
- invalidateBatch(batch);
- CLASS_INVALIDATIONS.incrementAndGet();
- EXACT_ONLY_INVALIDATIONS.incrementAndGet();
+ }
+
+ /**
+ * Process-wide bulk retirement for non-stock (custom) MetaClass registry
+ * events — correctness-first when selection may consult state outside the
+ * stock miss walk. Rare.
+ */
+ public static void invalidateBulk() {
+ retireAllLoadedDomains();
+ BULK_INVALIDATIONS.incrementAndGet();
if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
- LOG.fine("exact-class SwitchPoint invalidated for " + type.getName()
- + "; totalExact=" + EXACT_ONLY_INVALIDATIONS.get());
+ LOG.fine("custom-MetaClass bulk invalidation; totalBulk=" + BULK_INVALIDATIONS.get());
+ }
+ }
+
+ /**
+ * Process-wide bulk retirement when a MetaClass registry event carries no
+ * {@code Class} attribution.
+ */
+ public static void invalidateUnscoped() {
+ retireAllLoadedDomains();
+ BULK_INVALIDATIONS.incrementAndGet();
+ if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
+ LOG.fine("unscoped bulk invalidation; totalBulk=" + BULK_INVALIDATIONS.get());
}
}
/**
- * Registry-driven invalidation with MetaClass-aware fan-out policy.
- * See class javadoc table for the decision matrix.
+ * Detaches and invalidates every live class-level MetaClass / pending domain
+ * for loaded types. Shared implementation for all process-wide entries.
+ */
+ private static void retireAllLoadedDomains() {
+ List<SwitchPoint> batch = new ArrayList<>();
+ try {
+ for (ClassInfo info : ClassInfo.getAllClassInfo()) {
+ info.collectLiveIndySwitchPoints(batch);
+ }
+ } finally {
+ invalidateBatch(batch);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Registry policy
+ // -------------------------------------------------------------------------
+
+ /**
+ * Registry-driven invalidation: exact class for stock MetaClass kinds and
+ * per-instance changes; process-wide bulk for custom MetaClass kinds or
+ * unattributed events. See class javadoc.
*
* @param event the registry change event (must not be {@code null})
*/
@@ -289,108 +212,43 @@ public final class IndyInvalidation {
return;
}
if (event.isPerInstanceMetaClassChange()) {
- // Per-instance MC: only this class's monomorphic sites re-select.
- invalidateClassExact(type);
+ invalidateClass(type);
return;
}
- if (needsHierarchyFanOut(type, event.getOldMetaClass(), event.getNewMetaClass())) {
- invalidateClass(type);
+ if (needsBulkInvalidation(event.getOldMetaClass(), event.getNewMetaClass())) {
+ invalidateBulk();
} else {
- invalidateClassExact(type);
+ invalidateClass(type);
}
}
/**
- * Whether a class-level MetaClass change for {@code type} can affect
- * already-linked sites on subtypes / array-covariance descendants.
+ * Whether a class-level MetaClass change involving {@code oldMc}/{@code newMc}
+ * requires process-wide bulk invalidation.
* <p>
- * Hierarchy fan-out is the default. Exact-class is an <em>allow-list</em>:
- * both old and new MetaClass must be {@linkplain #isHierarchyLocalMetaClass
- * hierarchy-local}, and {@code type} must not be an interface/array, and
- * global EMC must be off. Unknown / custom MetaClass kinds therefore fan
- * out (correctness-first).
- * <p>
- * Aligns with {@code MetaClassImpl.findMethodInClassHierarchy}: the live
- * hierarchy walk opens only when a modified {@link MutableMetaClass} is
- * present (EMC install/remove/update). Pure unmodified
- * {@code MetaClassImpl} pairs never open that walk, so they stay
- * exact-class. Fan-out is still required for the missing-method case even
- * when a subtype already has <em>other</em> methods of the same name with
- * different signatures — a previously unlinked {@code child.m2("")} has no
- * site to invalidate, but a previously linked miss must re-select.
+ * Bulk is reserved for <em>non-stock</em> MetaClass kinds: not
+ * {@link MetaClassImpl} after adapter unwrap. Stock pairs stay exact-class.
*
- * @param type the class being updated (not {@code null})
* @param oldMc previous MetaClass (may be {@code null})
* @param newMc new MetaClass (may be {@code null} on remove)
- * @return {@code true} if subtype fan-out is required
+ * @return {@code true} if bulk invalidation is required
*/
- public static boolean needsHierarchyFanOut(
- final Class<?> type, final MetaClass oldMc, final MetaClass newMc) {
- if (type.isInterface() || type.isArray()) {
- return true;
- }
- if (isGlobalEmcEnabled()) {
- return true;
- }
- // Exact-only only when BOTH sides are hierarchy-local.
- return !isHierarchyLocalMetaClass(oldMc) || !isHierarchyLocalMetaClass(newMc);
+ public static boolean needsBulkInvalidation(final MetaClass oldMc, final MetaClass newMc) {
+ return !isStockMetaClass(oldMc) || !isStockMetaClass(newMc);
}
/**
- * Whether {@code mc} cannot publish cross-class MOP state into subtype
- * selection via the live missing-method hierarchy walk (or EMC inheritance).
- * Hierarchy-local means:
- * <ul>
- * <li>{@code null} (absent / removed), or</li>
- * <li>unwrapped {@code MetaClassImpl} that is not an
- * {@link ExpandoMetaClass} and reports {@code isModified()==false}.</li>
- * </ul>
- * EMC, modified mutable MetaClasses, and unknown MetaClass kinds return
- * {@code false} so fan-out remains correctness-first.
- * <p>
- * Note: a hierarchy-local {@code MetaClassImpl} may still <em>contain</em>
- * methods snapshotted from an ancestor at construction time; that snapshot
- * is fixed for the MetaClass instance’s lifetime and is not refreshed by
- * SwitchPoint retirement. See class javadoc.
+ * Whether {@code mc} is a stock MOP MetaClass for which exact-class
+ * invalidation is sufficient: {@code null}, or unwrapped
+ * {@link MetaClassImpl} (includes EMC and subclasses). Any other kind is
+ * non-stock and triggers bulk invalidation on registry replace.
*
* @param mc MetaClass to classify (may be {@code null})
- * @return {@code true} if a replace involving only this kind needs no fan-out
+ * @return {@code true} if exact-class invalidation is sufficient
*/
- public static boolean isHierarchyLocalMetaClass(final MetaClass mc) {
+ public static boolean isStockMetaClass(final MetaClass mc) {
MetaClass current = unwrapMetaClass(mc);
- if (current == null) {
- return true;
- }
- // EMC always participates in cross-class visibility (even before the
- // first method marks isModified), so it is never hierarchy-local.
- if (current instanceof ExpandoMetaClass) {
- return false;
- }
- if (current instanceof MutableMetaClass mutable && mutable.isModified()) {
- return false;
- }
- // Unmodified MetaClassImpl (and non-EMC subclasses with isModified==false).
- return current instanceof MetaClassImpl;
- }
-
- /**
- * {@code true} when the registry creates {@link ExpandoMetaClass} instances
- * by default ({@link ExpandoMetaClass#enableGlobally()}).
- */
- static boolean isGlobalEmcEnabled() {
- MetaClassRegistry registry = GroovySystem.getMetaClassRegistry();
- // Registry may be null while GroovySystem is still constructing it.
- return registry != null
- && registry.getMetaClassCreationHandler() instanceof ExpandoMetaClassCreationHandle;
- }
-
- /**
- * Whether {@code mc} is (or wraps) an {@link ExpandoMetaClass}.
- * Wrappers are unwrapped via {@link AdaptingMetaClass#getAdaptee()} or
- * {@link DelegatingMetaClass#getAdaptee()} ({@code HandleMetaClass}).
- */
- static boolean isExpandoMetaClass(final MetaClass mc) {
- return unwrapMetaClass(mc) instanceof ExpandoMetaClass;
+ return current == null || current instanceof MetaClassImpl;
}
/**
@@ -423,6 +281,10 @@ public final class IndyInvalidation {
return null;
}
+ // -------------------------------------------------------------------------
+ // Domain map
+ // -------------------------------------------------------------------------
+
/**
* Domain key for a MetaClass (unwrapped adaptee when present).
*/
@@ -467,56 +329,6 @@ public final class IndyInvalidation {
ClassInfo.getClassInfo(type).collectLiveIndySwitchPoints(out);
}
- /**
- * Invalidates SwitchPoints for {@code type} and all loaded classes assignable
- * from {@code type}. Detach the root domain, then every descendant in
- * {@link ClassHierarchyIndex}. Batched through
- * {@link SwitchPoint#invalidateAll(SwitchPoint[])}.
- *
- * @param type the root of the invalidation fan-out
- */
- public static void invalidateClassHierarchy(final Class<?> type) {
- List<SwitchPoint> batch = new ArrayList<>();
- try {
- collectLiveForClass(type, batch);
- List<ClassInfo> descendants = new ArrayList<>();
- ClassHierarchyIndex.collectDescendants(type, descendants);
- for (ClassInfo info : descendants) {
- info.collectLiveIndySwitchPoints(batch);
- }
- } finally {
- invalidateBatch(batch);
- }
- }
-
- /**
- * Detaches and invalidates every live class-level MetaClass / pending domain
- * for loaded types. Used for category enter/leave and unattributed MetaClass
- * changes.
- */
- public static void invalidateAllLoadedClassSwitchPoints() {
- List<SwitchPoint> batch = new ArrayList<>();
- try {
- for (ClassInfo info : ClassInfo.getAllClassInfo()) {
- info.collectLiveIndySwitchPoints(batch);
- }
- } finally {
- invalidateBatch(batch);
- }
- }
-
- /**
- * Invalidates every live class-level MetaClass SwitchPoint. Used when a
- * MetaClass registry event carries no {@code Class} attribution.
- */
- public static void invalidateUnscoped() {
- invalidateAllLoadedClassSwitchPoints();
- CLASS_INVALIDATIONS.incrementAndGet();
- if (STATS_LOG && LOG.isLoggable(Level.FINE)) {
- LOG.fine("unscoped SwitchPoint invalidation; total=" + CLASS_INVALIDATIONS.get());
- }
- }
-
private static void invalidateBatch(final List<SwitchPoint> batch) {
if (batch.isEmpty()) {
return;
@@ -524,6 +336,10 @@ public final class IndyInvalidation {
SwitchPoint.invalidateAll(batch.toArray(EMPTY_SWITCH_POINTS));
}
+ // -------------------------------------------------------------------------
+ // Link-time domain resolution and guards
+ // -------------------------------------------------------------------------
+
/**
* Resolves the class used for the MetaClass SwitchPoint domain of a receiver.
* {@code null} maps to {@link NullObject}; a {@link Class} receiver uses itself.
@@ -543,7 +359,6 @@ public final class IndyInvalidation {
/**
* Returns the SwitchPoint domain for a MetaClass instance (unwraps adapters).
- * All MetaClass kinds share the same weak identity map of domains.
*
* @param mc MetaClass (must not be {@code null})
* @return the MetaClass-domain SwitchPoint
@@ -555,6 +370,7 @@ public final class IndyInvalidation {
/**
* Retires the SwitchPoint domain for {@code mc} (if one was allocated).
+ * Does not bump counters (local domain retire; width policy is separate).
*
* @param mc MetaClass (may be {@code null})
*/
@@ -580,8 +396,7 @@ public final class IndyInvalidation {
/**
* Returns the SwitchPoint for the given class at link time.
* Prefers the installed class-level MetaClass domain; if none is installed
- * yet, uses the ClassInfo pending domain (defineClass-safe — does not create
- * a MetaClass). First MetaClass install retires the pending domain.
+ * yet, uses the ClassInfo pending domain (defineClass-safe).
*
* @param type the class (must not be {@code null})
* @return the SwitchPoint for monomorphic MOP guards
@@ -597,12 +412,7 @@ public final class IndyInvalidation {
/**
* Installs a MetaClass SwitchPoint guard on {@code handle}.
- * MetaClass or category changes that retire that domain cause the site to
- * take {@code fallback}.
- * <p>
- * Public entry for tests and any code outside {@code vmplugin.v8}. Production
- * call-site linking goes through {@code IndyInterface.applyMopSwitchPoints}
- * (same {@code guardWithTest} after {@link #classSwitchPointFor(Object)}).
+ * Public entry for tests; production uses {@code IndyInterface.applyMopSwitchPoints}.
*
* @param handle the fast-path handle
* @param fallback the re-link / fallback handle
@@ -616,7 +426,6 @@ public final class IndyInvalidation {
/**
* Installs a MetaClass SwitchPoint guard on {@code handle}.
- * See {@link #guardWithMopSwitchPoints(MethodHandle, MethodHandle, Object)}.
*
* @param handle the fast-path handle
* @param fallback the re-link / fallback handle
@@ -628,10 +437,12 @@ public final class IndyInvalidation {
return classSwitchPointFor(receiverClass).guardWithTest(handle, fallback);
}
+ // -------------------------------------------------------------------------
+ // Stats (optional; tests)
+ // -------------------------------------------------------------------------
+
/**
- * Returns the total number of scoped class invalidations
- * ({@link #invalidateClass(Class)} / {@link #invalidateClassExact(Class)} /
- * {@link #invalidateForMetaClassChange} / {@link #invalidateUnscoped()} events).
+ * Exact-class invalidation event count ({@link #invalidateClass(Class)}).
*
* @return class invalidation count
*/
@@ -640,25 +451,17 @@ public final class IndyInvalidation {
}
/**
- * Returns how many class invalidations used hierarchy fan-out.
- *
- * @return hierarchy fan-out event count
- */
- public static long hierarchyInvalidationCount() {
- return HIERARCHY_INVALIDATIONS.get();
- }
-
- /**
- * Returns how many class invalidations were exact-class only.
+ * Non-category process-wide bulk event count
+ * ({@link #invalidateBulk()} / {@link #invalidateUnscoped()}).
*
- * @return exact-only event count
+ * @return bulk invalidation count
*/
- public static long exactOnlyInvalidationCount() {
- return EXACT_ONLY_INVALIDATIONS.get();
+ public static long bulkInvalidationCount() {
+ return BULK_INVALIDATIONS.get();
}
/**
- * Returns the total number of category-style bulk invalidations.
+ * Category-style bulk invalidation count ({@link #invalidateCategory()}).
*
* @return category invalidation count
*/
@@ -672,7 +475,6 @@ public final class IndyInvalidation {
public static void resetCountersForTesting() {
CLASS_INVALIDATIONS.set(0);
CATEGORY_INVALIDATIONS.set(0);
- EXACT_ONLY_INVALIDATIONS.set(0);
- HIERARCHY_INVALIDATIONS.set(0);
+ BULK_INVALIDATIONS.set(0);
}
}
diff --git a/src/main/java/org/apache/groovy/runtime/indy/SwitchPointInvalidator.java b/src/main/java/org/apache/groovy/runtime/indy/SwitchPointInvalidator.java
index 0b6c8dc37d..79cb0ddbc0 100644
--- a/src/main/java/org/apache/groovy/runtime/indy/SwitchPointInvalidator.java
+++ b/src/main/java/org/apache/groovy/runtime/indy/SwitchPointInvalidator.java
@@ -30,13 +30,13 @@ import java.util.concurrent.atomic.AtomicReference;
* point so linked sites fall back and re-link; the next {@link #getSwitchPoint()}
* allocates a fresh one.
* <p>
- * Hierarchy fan-out and other bulk paths should prefer {@link #detachLive()} and a
- * single {@link SwitchPoint#invalidateAll(SwitchPoint[])} over many individual
- * {@link #invalidate()} calls (GROOVY-12191).
+ * Bulk paths (category enter/leave, custom MetaClass events) should prefer
+ * {@link #detachLive()} and a single {@link SwitchPoint#invalidateAll(SwitchPoint[])}
+ * over many individual {@link #invalidate()} calls (GROOVY-12191).
* <p>
- * There is no permanent failover: after scoping invalidation to a single class
- * (plus subtypes), deopt storms from a process-wide SwitchPoint no longer apply,
- * so a simpler replace-on-invalidate model is sufficient.
+ * There is no permanent failover: after scoping stock invalidation to a single
+ * class, deopt storms from a process-wide SwitchPoint no longer apply, so a
+ * simpler replace-on-invalidate model is sufficient.
*
* @since 6.0.0
*/
diff --git a/src/main/java/org/apache/groovy/runtime/indy/package-info.java b/src/main/java/org/apache/groovy/runtime/indy/package-info.java
index 29eeffdce4..980309572f 100644
--- a/src/main/java/org/apache/groovy/runtime/indy/package-info.java
+++ b/src/main/java/org/apache/groovy/runtime/indy/package-info.java
@@ -20,28 +20,8 @@
/**
* Scoped invokedynamic SwitchPoint utilities for the Groovy MOP (GROOVY-12191).
* <p>
- * <b>Domain model:</b> one SwitchPoint per {@link groovy.lang.MetaClass}
- * instance (weak identity map in {@link IndyInvalidation}). No SwitchPoint field
- * on {@link groovy.lang.MetaClassImpl}; custom MetaClasses use the same map.
- * When no class-level MetaClass is installed yet, link uses a ClassInfo
- * <em>pending</em> domain (retired on first MetaClass install) — defineClass-
- * safe without an always-invalid SwitchPoint (which would monomorphic-loop).
- * MetaClass-aware policy decides exact-class vs hierarchy fan-out:
- * <ul>
- * <li>EMC, modified custom MetaClass, global EMC, interface and array types
- * → class + loaded subtypes (incl. array lattice)</li>
- * <li>hierarchy-local {@code MetaClassImpl} pair, per-instance MetaClass
- * → exact class only</li>
- * <li>category enter/leave and unscoped events → bulk retire class-level
- * MetaClass domains of loaded types</li>
- * </ul>
- * Hierarchy fan-out exists only for the missing-method walk
- * ({@code MetaClassImpl.findMethodInClassHierarchy}). MetaClassImpl continues
- * to observe ancestor MetaClass state on that path (6.0 decision). See
- * {@link IndyInvalidation}.
- * <p>
- * Hierarchy fan-out is indexed by {@link ClassHierarchyIndex}. Install guards
- * via {@link IndyInvalidation#guardWithMopSwitchPoints} or
+ * Canonical policy and domain model: {@link IndyInvalidation}.
+ * Install guards via {@link IndyInvalidation#guardWithMopSwitchPoints} or
* {@code IndyInterface.applyMopSwitchPoints}.
*
* @since 6.0.0
diff --git a/src/main/java/org/codehaus/groovy/reflection/ClassInfo.java b/src/main/java/org/codehaus/groovy/reflection/ClassInfo.java
index 5106930ea5..c002226ba9 100644
--- a/src/main/java/org/codehaus/groovy/reflection/ClassInfo.java
+++ b/src/main/java/org/codehaus/groovy/reflection/ClassInfo.java
@@ -26,7 +26,6 @@ import groovy.lang.MetaClass;
import groovy.lang.MetaClassRegistry;
import groovy.lang.MetaMethod;
import groovy.transform.Internal;
-import org.apache.groovy.runtime.indy.ClassHierarchyIndex;
import org.apache.groovy.runtime.indy.IndyInvalidation;
import org.apache.groovy.runtime.indy.SwitchPointInvalidator;
import org.apache.groovy.util.concurrent.ManagedIdentityConcurrentMap;
@@ -125,9 +124,6 @@ public class ClassInfo implements Finalizable {
this.classRef = new WeakReference<Class<?>>(klazz);
cachedClassRef = new LazyCachedClassRef(softBundle, this);
artifactClassLoader = new LazyClassLoaderRef(softBundle, this);
- // Index under MOP-relevant ancestors so hierarchy SwitchPoint fan-out is
- // O(descendants) rather than O(all loaded ClassInfos) (GROOVY-12191).
- ClassHierarchyIndex.register(this);
}
/**
@@ -142,23 +138,14 @@ public class ClassInfo implements Finalizable {
/**
* Increments the version number and invalidates this class's MetaClass
- * SwitchPoint (and subtype MetaClass SwitchPoints via hierarchy fan-out).
- * Called when metaclass modifications occur (e.g., adding methods to an {@code ExpandoMetaClass}).
- * <p>
- * Hierarchy fan-out is intentional here: {@code incVersion} is the path used
- * when an existing MetaClass is <em>updated in place</em> (EMC method/property
- * add). That update can make a previously <em>missing</em> name resolvable on
- * subtype receivers via {@code MetaClassImpl.findMethodInClassHierarchy}
- * (opens only when a modified {@code MutableMetaClass} exists in the
- * hierarchy), so already-linked subtype miss sites must re-select. Present
- * methods on the child keep winning after re-link. Registry-driven full
- * MetaClass <em>replacements</em> use MetaClass-aware policy instead (pure
- * {@code MetaClassImpl} replace stays exact-class; see
- * {@link IndyInvalidation#invalidateForMetaClassChange}).
+ * SwitchPoint domain (exact class only). Called when metaclass modifications
+ * occur (e.g., adding methods to an {@code ExpandoMetaClass}).
* <p>
* <strong>Behavioral change in 6.0 (GROOVY-12191):</strong> this method no longer
* triggers a process-wide call-site flush. It scopes invalidation to this class
- * and loaded subtypes/implementors only. Callers that previously relied on
+ * only. Subtype sites are not retired: stock missing-method / missing-property
+ * hierarchy walks re-resolve live on the dynamic route (see
+ * {@link IndyInvalidation}). Callers that previously relied on
* {@code incVersion()} as a global “flush everything” hammer should invoke
* {@link org.codehaus.groovy.vmplugin.VMPlugin#invalidateCallSites()} (or
* {@link IndyInvalidation#invalidateCategory()}) explicitly when a bulk flush
@@ -167,7 +154,7 @@ public class ClassInfo implements Finalizable {
* Category enter/leave still bulk-invalidates MetaClass SwitchPoints via
* {@link org.codehaus.groovy.vmplugin.VMPlugin#invalidateCallSites()}.
* SwitchPoint policy is owned by {@link IndyInvalidation}; this method only
- * bumps generation then delegates hierarchy invalidation.
+ * bumps generation then delegates exact-class invalidation.
*/
public void incVersion() {
version.incrementAndGet();
@@ -184,14 +171,16 @@ public class ClassInfo implements Finalizable {
* <ul>
* <li>First MetaClass install: retire pending domain only (sites linked
* pre-MC re-select onto the MetaClass domain).</li>
- * <li>Replace/clear: retire the current MetaClass domain (and any leftover
- * pending). Hierarchy fan-out is applied by the registry listener.</li>
+ * <li>Replace/clear: retire this ClassInfo’s domains locally. When the
+ * change also fires a MetaClass registry event, the listener applies
+ * width policy ({@link IndyInvalidation#invalidateForMetaClassChange})
+ * — often a second detach of an already-empty domain (idempotent).</li>
* </ul>
*/
private void bumpGenerationLocal(final boolean retireSwitchPoint) {
version.incrementAndGet();
if (retireSwitchPoint) {
- // Local only: registry listener will fan out to subtypes when present.
+ // Local domain ownership: width policy is IndyInvalidation's job.
invalidateIndySwitchPoint();
} else {
// First MetaClass install — retire pending pre-MC domain only.
@@ -418,15 +407,16 @@ public class ClassInfo implements Finalizable {
* <p>
* On replacement or clear, also invalidates this class's indy SwitchPoint
* (GROOVY-12191). First install ({@code null →} MC) only bumps version;
- * hierarchy fan-out for subtypes is applied by MetaClass registry listeners
- * or {@link #incVersion()}.
+ * exact-class invalidation for the registry event is applied by MetaClass
+ * registry listeners or {@link #incVersion()}.
*
* @param answer the metaclass to set, or {@code null} to clear
*/
public void setStrongMetaClass(MetaClass answer) {
// Version always; SwitchPoint when replacing/clearing an established MC.
- // Hierarchy fan-out for subtypes is applied by registry listeners / incVersion
- // (GROOVY-12191). First install (null → MC) only bumps version.
+ // Exact-class (stock) or bulk (custom) policy is applied by registry
+ // listeners / incVersion (GROOVY-12191). First install (null → MC) only
+ // bumps version.
boolean replaceOrClear = hasClassLevelMetaClass() || answer == null;
bumpGenerationLocal(replaceOrClear);
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
index 85ff2a2798..05cd876150 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
@@ -221,17 +221,14 @@ public class IndyInterface {
}
static {
- // MetaClass registry changes invalidate the affected class + subtypes (GROOVY-12191).
- // Hierarchy fan-out is owned here (not duplicated in setStrongMetaClass beyond a
- // local retire of the exact class for paths that never fire the registry).
+ // MetaClass registry changes invalidate the affected class domain (GROOVY-12191).
+ // Stock MetaClassImpl/EMC → exact class; custom MetaClass kinds → bulk.
//
// Pre-6.0 this listener rotated a process-wide SwitchPoint field on this class.
// That field is removed (vmplugin is internal-by-intent): call sites now guard on
// MetaClass-owned domains via applyMopSwitchPoints.
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> {
- // MetaClass-aware fan-out: EMC / interface / array / global-EMC → hierarchy;
- // pure MetaClassImpl class replace and per-instance MC → exact class only.
- // See IndyInvalidation class javadoc for the decision matrix (GROOVY-12191).
+ // Exact-class (stock) vs process-wide bulk (custom / unscoped) — IndyInvalidation.
IndyInvalidation.invalidateForMetaClassChange(cmcu);
if (LOG_ENABLED) {
Class<?> type = cmcu.getClassToUpdate();
@@ -250,8 +247,8 @@ public class IndyInterface {
* Bulk-invalidates every loaded class SwitchPoint so sites re-link under the
* new category state. Per-class MetaClass changes use the MetaClass-aware
* registry path ({@link IndyInvalidation#invalidateForMetaClassChange}) or
- * {@link org.codehaus.groovy.reflection.ClassInfo#incVersion()} (hierarchy
- * fan-out for in-place EMC updates).
+ * {@link org.codehaus.groovy.reflection.ClassInfo#incVersion()} (exact-class
+ * for in-place EMC updates).
* <p>
* Pre-6.0 this method also rotated a process-wide {@code switchPoint} field.
* That field is gone; bulk policy lives entirely in {@link IndyInvalidation}.
diff --git a/src/test/groovy/bugs/Groovy12191.groovy b/src/test/groovy/bugs/Groovy12191.groovy
index 865a133a13..2f66eb0c90 100644
--- a/src/test/groovy/bugs/Groovy12191.groovy
+++ b/src/test/groovy/bugs/Groovy12191.groovy
@@ -34,7 +34,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue
* GROOVY-12191: scoped indy SwitchPoint invalidation.
* Metaclass changes for class A must not invalidate class B's SwitchPoint;
* category enter/leave bulk-invalidates class SwitchPoints so category methods
- * become visible to previously linked sites.
+ * become visible to previously linked sites. Stock MetaClassImpl/EMC changes
+ * are exact-class only — no parent→child SwitchPoint fan-out (PR #2736).
*/
final class Groovy12191 {
@@ -77,7 +78,6 @@ final class Groovy12191 {
assert 'decorated' == new TypeD().label()
}
- // Category enter/leave bulk-invalidates class domains (single-guard model).
assertTrue(classSp.hasBeenInvalidated())
}
@@ -116,17 +116,16 @@ final class Groovy12191 {
}
@Test
- void parentMetaClassChange_invalidatesSubclassSwitchPoint() {
- // Hierarchy fan-out is required for cross-class MOP visibility:
- // Parent.metaClass (EMC) mutations must retire Child-linked sites so
- // new Child().hello() re-selects. This is not MetaClassImpl sharing a
- // table up the hierarchy — each class keeps its own MetaClass.
+ void parentMetaClassChange_doesNotInvalidateSubclassSwitchPoint() {
+ // Stock policy: parent EMC mutation retires only the parent's domain.
+ // Child sites stay warm; the live miss route observes the new method.
SwitchPoint childSp = ClassInfo.getClassInfo(HierChild).indySwitchPoint
assertFalse(childSp.hasBeenInvalidated())
HierParent.metaClass.hello = { -> 'from-parent' }
- assertTrue(childSp.hasBeenInvalidated())
+ assertFalse(childSp.hasBeenInvalidated(),
+ 'parent EMC must not retire subclass SwitchPoint (stock exact-class)')
assertScript '''
class HierParent {}
class HierChild extends HierParent {}
@@ -137,9 +136,6 @@ final class Groovy12191 {
@Test
void parentMetaClassImplReplace_doesNotFanOutToSubclassSwitchPoint() {
- // Pure MetaClassImpl ↔ MetaClassImpl replace has no cross-class EMC
- // visibility: each class owns its own tables. Hierarchy fan-out would
- // only re-link subtype sites without a MOP reason (blackdrag review).
SwitchPoint parentSp = ClassInfo.getClassInfo(HierParentMcImpl).indySwitchPoint
SwitchPoint childSp = ClassInfo.getClassInfo(HierChildMcImpl).indySwitchPoint
assertFalse(parentSp.hasBeenInvalidated())
@@ -159,9 +155,7 @@ final class Groovy12191 {
}
@Test
- void parentEmcReplace_fansOutToSubclassSwitchPoint() {
- // Installing EMC on Parent publishes methods into Child via hierarchy
- // walk — subtype sites must re-link.
+ void parentEmcReplace_doesNotFanOutToSubclassSwitchPoint() {
SwitchPoint childSp = ClassInfo.getClassInfo(HierChildEmc).indySwitchPoint
assertFalse(childSp.hasBeenInvalidated())
@@ -169,8 +163,8 @@ final class Groovy12191 {
emc.initialize()
GroovySystem.metaClassRegistry.setMetaClass(HierParentEmc, emc)
try {
- assertTrue(childSp.hasBeenInvalidated(),
- 'EMC install on parent must retire subclass SwitchPoint')
+ assertFalse(childSp.hasBeenInvalidated(),
+ 'EMC install on parent must not retire subclass SwitchPoint')
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(HierParentEmc)
}
@@ -178,7 +172,6 @@ final class Groovy12191 {
@Test
void unrelatedTypeMetaClassChange_doesNotInvalidateSiblingHierarchy() {
- // Hierarchy fan-out is only to subtypes of the changed type — not global.
SwitchPoint hotSp = ClassInfo.getClassInfo(HierChildScope).indySwitchPoint
assertFalse(hotSp.hasBeenInvalidated())
@@ -210,7 +203,6 @@ final class Groovy12191 {
SwitchPoint hotSp = ClassInfo.getClassInfo(TypeD).indySwitchPoint
assertFalse(hotSp.hasBeenInvalidated())
- // Churn an unrelated type — scoped invalidation must leave TypeD alone.
10.times { i ->
TypeC.metaClass."dyn${i}" = { -> i }
}
@@ -218,9 +210,9 @@ final class Groovy12191 {
}
@Test
- void objectArrayMetaClassChange_invalidatesStringArraySwitchPoint() {
- // Array types are final, yet Object[] is the MOP supertype of reference
- // arrays — scoped invalidation must fan out (GROOVY-12191 review).
+ void objectArrayMetaClassChange_doesNotInvalidateStringArraySwitchPoint() {
+ // Stock exact-class: Object[] EMC does not retire String[] domain.
+ // Dispatch still sees the method via the live miss hierarchy walk.
SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
assertFalse(stringArraySp.hasBeenInvalidated())
@@ -228,15 +220,15 @@ final class Groovy12191 {
emc.initialize()
GroovySystem.metaClassRegistry.setMetaClass(Object[], emc)
try {
- assertTrue(stringArraySp.hasBeenInvalidated(),
- 'Object[] MetaClass change must retire String[] SwitchPoint')
+ assertFalse(stringArraySp.hasBeenInvalidated(),
+ 'Object[] MetaClass change must not retire String[] SwitchPoint')
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(Object[])
}
}
@Test
- void incVersion_scopesToClassHierarchy_notGlobal() {
+ void incVersion_scopesToExactClass_notGlobal() {
SwitchPoint hotSp = ClassInfo.getClassInfo(TypeD).indySwitchPoint
SwitchPoint targetSp = ClassInfo.getClassInfo(TypeC).indySwitchPoint
assertFalse(hotSp.hasBeenInvalidated())
@@ -252,12 +244,10 @@ final class Groovy12191 {
/**
* Blackdrag scenario matrix (PR #2736): {@code call(c)} uses receiver class C.
* Mutations on B/A must not retire C's domain; methods added on B/A must not
- * become visible on a pure C instance. Documents old-vs-new blast radius and
- * actual MOP visibility.
+ * become visible on a pure C instance. Stock policy is exact-class throughout.
*/
@Test
void blackdragScenario_callOnC_notInvalidatedBySubclassMetaClassChurn() {
- // Touch receivers first so MetaClass-owned domains exist; capture SPs after.
def c = new BdC()
assert c.fooC() == 'fooC'
new BdB()
@@ -269,13 +259,13 @@ final class Groovy12191 {
assertFalse(spB.hasBeenInvalidated())
assertFalse(spA.hasBeenInvalidated())
- // B.metaClass = EMC + method — fans out to A (subtype of B), not to C (supertype).
+ // B.metaClass = EMC + method — exact-class B only (not A, not C).
def emcB = new ExpandoMetaClass(BdB, true, true)
emcB.initialize()
GroovySystem.metaClassRegistry.setMetaClass(BdB, emcB)
try {
assertTrue(spB.hasBeenInvalidated())
- assertTrue(spA.hasBeenInvalidated(), 'EMC on B must fan out to A')
+ assertFalse(spA.hasBeenInvalidated(), 'EMC on B must not fan out to A')
assertFalse(spC.hasBeenInvalidated(), 'EMC on B must not retire C (supertype)')
assert c.fooC() == 'fooC'
assert !c.metaClass.respondsTo(c, 'fooB0')
@@ -283,17 +273,17 @@ final class Groovy12191 {
spC = ClassInfo.getClassInfo(BdC).indySwitchPoint
spA = ClassInfo.getClassInfo(BdA).indySwitchPoint
BdB.metaClass.fooB0 = { -> 'fooB0' }
- assertTrue(spA.hasBeenInvalidated(), 'B EMC update (incVersion) fans out to A')
+ // in-place EMC update retires B's domain again; A/C stay warm
+ assertFalse(spA.hasBeenInvalidated(), 'B EMC update must not fan out to A')
assertFalse(spC.hasBeenInvalidated())
assert new BdB().fooB0() == 'fooB0'
- assert new BdA().fooB0() == 'fooB0'
+ assert new BdA().fooB0() == 'fooB0' // live hierarchy walk
assert !c.metaClass.respondsTo(c, 'fooB0')
assert c.fooC() == 'fooC'
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(BdB)
}
- // A.fooA = {} — exact class A (+ subtypes of A if any), not C.
spC = ClassInfo.getClassInfo(BdC).indySwitchPoint
BdA.metaClass.fooA = { -> 'fooA' }
try {
@@ -305,7 +295,6 @@ final class Groovy12191 {
GroovySystem.metaClassRegistry.removeMetaClass(BdA)
}
- // Per-instance EMC on a B instance — does not affect C domain or visibility on c.
spC = ClassInfo.getClassInfo(BdC).indySwitchPoint
def b = new BdB()
def emcInst = new ExpandoMetaClass(BdB, false, true)
@@ -323,7 +312,6 @@ final class Groovy12191 {
@Test
void objectArrayEmcMethod_visibleOnStringArray() {
- // Empirical MOP fact that justifies array lattice fan-out for EMC.
assertScript '''
Object[].metaClass.arrHello = { -> 'arr' }
try {
@@ -335,6 +323,28 @@ final class Groovy12191 {
'''
}
+ /**
+ * Behaviour-only: linked miss on String[] then Object[] EMC add must become
+ * visible without String[] SwitchPoint retirement (PR #2736 experiment).
+ */
+ @Test
+ void linkedMiss_stringArray_thenObjectArrayEmcAdd_isVisible() {
+ assertScript '''
+ def probe(x) {
+ try { return x.arrHello() } catch (MissingMethodException e) { return 'miss' }
+ }
+ String[] arr = ['a', 'b']
+ 200.times { assert probe(arr) == 'miss' }
+ try {
+ Object[].metaClass.arrHello = { -> 'array-visible' }
+ assert probe(arr) == 'array-visible'
+ } finally {
+ GroovySystem.metaClassRegistry.removeMetaClass(Object[])
+ GroovySystem.metaClassRegistry.removeMetaClass(String[])
+ }
+ '''
+ }
+
/**
* Blackdrag PR #2736 comment (corrected parent→child direction): hierarchy
* walk is for <em>missing</em> methods only. Present methods on the child
@@ -355,8 +365,6 @@ final class Groovy12191 {
class C extends A {
def m2(x) { 40 }
}
- // Declared at script level (not inside try): construction-time snapshot
- // of A is taken when D's MetaClass is first built *after* A has EMC.
class D extends A {
def m2(x) { 50 }
}
@@ -374,17 +382,11 @@ final class Groovy12191 {
assert a.m2(0) == -2
assert a.m2('') == -3
- // Present methods on B are unchanged (not replaced by parent EMC).
b.metaClass = null
assert b.m1() == 1
assert b.m2(0) == 2
- // Missing m2(String) on B is found via hierarchy walk — first call,
- // so no prior monomorphic site needed invalidation for this name.
assert b.m2('') == -3
- // C was MetaClassImpl-initialized before A gained EMC methods:
- // present m2(Object) handles Integer/String/null; parent overloads
- // are not selected for this MetaClass instance.
assert c.m2(null) == 40
assert c.m2(0) == 40
assert c.m2('') == 40
@@ -393,15 +395,12 @@ final class Groovy12191 {
assert c.m2(0) == 40
assert c.m2('') == 40
- // Promote C to EMC: missing-method / EMC inheritance sees A.
c.metaClass.m1 = { -> -40 }
assert c.m1() == -40
assert c.m2(null) == 40
assert c.m2(0) == -2
assert c.m2('') == -3
- // D first used after A was modified: MetaClassImpl construction can
- // snapshot ancestor expando methods (timing gap vs C).
def d = new D()
assert d.m2(null) == 50
assert d.m2(0) == -2
@@ -418,8 +417,8 @@ final class Groovy12191 {
/**
* Blackdrag callsite-caching matrix: receiver MetaClass change (exact class)
* drives re-link; parent EMC remove does not rewrite a child MetaClassImpl
- * snapshot; hierarchy SwitchPoint on parent EMC is for miss re-select, not
- * for rebuilding MetaClassImpl tables.
+ * snapshot. Stock policy does not require hierarchy SwitchPoint for the
+ * miss path.
*/
@Test
void blackdragScenario_callsiteCaching_metaClassChangeNotHierarchyVersion() {
@@ -440,7 +439,6 @@ final class Groovy12191 {
A.metaClass.m2 = { Integer x -> -2 }
A.metaClass.m2 = { String x -> -3 }
try {
- // C MetaClassImpl from before / without snapshot of A overloads.
assert call(c, 0) == 40
c = new C()
assert call(c, 0) == 40
@@ -449,7 +447,6 @@ final class Groovy12191 {
assert mc instanceof MetaClassImpl
SwitchPoint spC = ClassInfo.getClassInfo(C).indySwitchPoint
- // Receiver class becomes EMC → class-domain retire (MC changed).
c.metaClass.m1 = { -> -40 }
assert spC.hasBeenInvalidated()
assert call(c, 0) == -2
@@ -457,17 +454,12 @@ final class Groovy12191 {
def emc = c.metaClass.delegate
assert emc instanceof ExpandoMetaClass
- // Restore old MetaClassImpl via registry (class-level replace).
- // Instance-only metaClass= may clear a per-instance MC without a
- // class-domain SwitchPoint event; class-level replace always does.
spC = ClassInfo.getClassInfo(C).indySwitchPoint
GroovySystem.metaClassRegistry.setMetaClass(C, mc)
c.metaClass = null
assert spC.hasBeenInvalidated()
assert call(c, 0) == 40
- // Late init: remove C's MetaClass so a fresh MetaClassImpl is built
- // while A still has EMC → construction-time snapshot sees A.
def registry = MetaClassRegistryImpl.getInstance(0)
spC = ClassInfo.getClassInfo(C).indySwitchPoint
registry.removeMetaClass(C)
@@ -478,11 +470,12 @@ final class Groovy12191 {
assert newMc instanceof MetaClassImpl
assert newMc != mc
- // Reset A: hierarchy fans out and re-links, but child MetaClassImpl
- // still holds the construction-time snapshot — SP does not rebuild MC.
+ // Reset A: stock exact-class — C's SwitchPoint is NOT retired.
+ // Child MetaClassImpl still holds the construction-time snapshot.
SwitchPoint spAfterA = ClassInfo.getClassInfo(C).indySwitchPoint
A.metaClass = null
- assert spAfterA.hasBeenInvalidated()
+ assert !spAfterA.hasBeenInvalidated() :
+ 'parent EMC remove must not retire child SwitchPoint'
assert call(c, 0) == -2
def catched = false
@@ -493,11 +486,6 @@ final class Groovy12191 {
}
assert catched
- // Re-install the EMC that was created while A still had methods.
- // blackdrag's draft expected "clueless about A" → 40, but measured
- // EMC retains ClosureMetaMethods copied from A (m2 Integer/String),
- // so resolution stays -2 — similar retention to MetaClassImpl
- // construction-time snapshot, not live super lookup.
spC = ClassInfo.getClassInfo(C).indySwitchPoint
GroovySystem.metaClassRegistry.setMetaClass(C, emc)
c.metaClass = null
@@ -511,8 +499,8 @@ final class Groovy12191 {
}
/**
- * Hierarchy fan-out is required so a previously linked hit on a parent EMC
- * method re-selects after that parent EMC is removed (miss path inverse).
+ * Linked hit via live miss route: after parent EMC is removed, the next
+ * call misses again — without requiring child SwitchPoint retirement.
*/
@Test
void parentEmcRemoveAfterLinkedHit_childCallSiteMissesAgain() {
@@ -529,7 +517,8 @@ final class Groovy12191 {
assert call(c) == 'from-parent'
SwitchPoint childSp = ClassInfo.getClassInfo(ChildHit).indySwitchPoint
GroovySystem.metaClassRegistry.removeMetaClass(ParentHit)
- assert childSp.hasBeenInvalidated()
+ assert !childSp.hasBeenInvalidated() :
+ 'parent remove must not retire child SwitchPoint'
def missed = false
try {
call(c)
@@ -545,8 +534,8 @@ final class Groovy12191 {
}
/**
- * Hierarchy fan-out is required so a previously linked miss on the child
- * re-selects after parent EMC install (missing-method path).
+ * Linked miss then parent EMC add: live miss route observes the new method
+ * without child SwitchPoint retirement.
*/
@Test
void parentEmcAfterLinkedMiss_childCallSiteSeesNewMethod() {
@@ -569,7 +558,7 @@ final class Groovy12191 {
SwitchPoint childSp = ClassInfo.getClassInfo(ChildMiss).indySwitchPoint
ParentMiss.metaClass.hello = { -> 'from-parent' }
try {
- assert childSp.hasBeenInvalidated()
+ assert !childSp.hasBeenInvalidated()
assert call(c) == 'from-parent'
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(ParentMiss)
@@ -579,11 +568,11 @@ final class Groovy12191 {
}
/**
- * Hierarchy SwitchPoint re-link must not change present-method resolution on
- * a MetaClassImpl child when the parent later gains EMC overloads.
+ * Present child method must keep winning after parent EMC add (no SP
+ * fan-out needed; re-link would not change the outcome either).
*/
@Test
- void parentEmc_doesNotReplacePresentChildMethod_afterHierarchyRelink() {
+ void parentEmc_doesNotReplacePresentChildMethod() {
assertScript '''
import org.codehaus.groovy.reflection.ClassInfo
import java.lang.invoke.SwitchPoint
@@ -599,8 +588,7 @@ final class Groovy12191 {
SwitchPoint childSp = ClassInfo.getClassInfo(ChildPresent).indySwitchPoint
ParentPresent.metaClass.m2 = { Integer x -> -2 }
try {
- assert childSp.hasBeenInvalidated() // hierarchy fan-out (over-invalidate OK)
- // Present m2(Object) still wins for Integer after re-link.
+ assert !childSp.hasBeenInvalidated()
assert call(c, 0) == 40
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(ParentPresent)
@@ -610,13 +598,11 @@ final class Groovy12191 {
}
/**
- * Pure Java inheritance matches Groovy for parent EMC visibility
- * ({@code LinkedHashMap} extends {@code HashMap}): hierarchy fan-out is
- * required so already-linked subtype sites re-select (blackdrag: same for
- * Java-based classes).
+ * Pure Java inheritance: parent EMC visible on subtype via live hierarchy
+ * walk, without subtype SwitchPoint retirement.
*/
@Test
- void javaSubtype_seesParentEmc_andHierarchyFanOut() {
+ void javaSubtype_seesParentEmc_withoutHierarchyFanOut() {
assertScript '''
import org.codehaus.groovy.reflection.ClassInfo
import java.lang.invoke.SwitchPoint
@@ -635,7 +621,7 @@ final class Groovy12191 {
HashMap.metaClass.jHello = { -> 'jh' }
try {
assert new HashMap().jHello() == 'jh'
- assert lhmSp.hasBeenInvalidated()
+ assert !lhmSp.hasBeenInvalidated()
assert call(lhm) == 'jh'
} finally {
GroovySystem.metaClassRegistry.removeMetaClass(HashMap)
@@ -671,9 +657,9 @@ final class Groovy12191 {
static class BdA extends BdB {}
/**
- * Follow-up 1: SwitchPoint domain is owned by the MetaClass instance, not
- * solely by ClassInfo. ClassInfo.getIndySwitchPoint delegates to the
- * installed MetaClassImpl when present.
+ * SwitchPoint domain is owned by the MetaClass instance, not solely by
+ * ClassInfo. ClassInfo.getIndySwitchPoint delegates to the installed
+ * MetaClass when present.
*/
@Test
void metaClassOwnsSwitchPoint_singleDomainViaIdentityMap() {
@@ -687,7 +673,6 @@ final class Groovy12191 {
info.strongMetaClass = mc
assertSame(onMc, info.indySwitchPoint)
assertSame(onMc, IndyInvalidation.classSwitchPointFor(McOwnerHost))
- // Custom non-MetaClassImpl also uses the same map (identity domain).
def custom = new groovy.lang.DelegatingMetaClass(mc) {}
assertSame(onMc, IndyInvalidation.switchPointForMetaClass(custom))
} finally {
@@ -696,13 +681,11 @@ final class Groovy12191 {
}
/**
- * Follow-up 2 decision: MetaClassImpl continues to observe ancestor MetaClass
- * state on the missing-method path ({@code Object.metaClass.foo} visible on
- * subtypes). Hierarchy SwitchPoint fan-out remains necessary for linked miss
- * → parent EMC add (and the inverse remove).
+ * MetaClassImpl continues to observe ancestor MetaClass state on the
+ * missing-method path ({@code Object.metaClass.foo} visible on subtypes).
*/
@Test
- void followUp2_metaClassImplStillObservesAncestorEmc_objectMetaClassPattern() {
+ void metaClassImplStillObservesAncestorEmc_objectMetaClassPattern() {
assertScript '''
class FollowUp2Child {}
try {
@@ -717,40 +700,148 @@ final class Groovy12191 {
'''
}
+ // ------------------------------------------------------------------
+ // Property-site probes (blackdrag: "True we need to be sure the
+ // property case is covered." — PR #2736 follow-up)
+ // ------------------------------------------------------------------
+
/**
- * Precise hierarchy case blackdrag asked for: receiver MetaClass unchanged,
- * parent EMC add must re-link an already-linked miss on the child.
+ * Linked property miss on child, then parent EMC property add: live property
+ * miss walk observes the new property without child SwitchPoint retirement.
*/
@Test
- void hierarchySwitchPoint_requiredOnlyForLinkedMissThenParentEmcAdd() {
+ void property_linkedMissThenParentEmcAdd_isVisible() {
assertScript '''
import org.codehaus.groovy.reflection.ClassInfo
import java.lang.invoke.SwitchPoint
- class HParent {}
- class HChild extends HParent {}
- def call(x) {
- try {
- x.onlyOnParent()
- return 'hit'
- } catch (MissingMethodException e) {
- return 'miss'
- }
+ class PropParent {}
+ class PropChild extends PropParent {}
+ def probe(x) {
+ try { return x.onlyOnParentProp } catch (MissingPropertyException e) { return 'miss' }
+ }
+ def c = new PropChild()
+ 200.times { assert probe(c) == 'miss' }
+ SwitchPoint childSp = ClassInfo.getClassInfo(PropChild).indySwitchPoint
+ try {
+ PropParent.metaClass.onlyOnParentProp = 'now-visible'
+ assert !childSp.hasBeenInvalidated() :
+ 'parent property EMC must not retire child SwitchPoint'
+ assert probe(c) == 'now-visible'
+ } finally {
+ GroovySystem.metaClassRegistry.removeMetaClass(PropParent)
+ GroovySystem.metaClassRegistry.removeMetaClass(PropChild)
+ }
+ '''
+ }
+
+ /**
+ * Linked property hit via live miss route, then parent EMC remove: next
+ * access misses again without child SwitchPoint retirement.
+ */
+ @Test
+ void property_parentEmcRemoveAfterLinkedHit_missesAgain() {
+ assertScript '''
+ import org.codehaus.groovy.reflection.ClassInfo
+ import java.lang.invoke.SwitchPoint
+
+ class PropParentHit {}
+ class PropChildHit extends PropParentHit {}
+ def probe(x) {
+ try { return x.helloProp } catch (MissingPropertyException e) { return 'miss' }
+ }
+ def c = new PropChildHit()
+ PropParentHit.metaClass.helloProp = 'from-parent'
+ try {
+ assert probe(c) == 'from-parent'
+ SwitchPoint childSp = ClassInfo.getClassInfo(PropChildHit).indySwitchPoint
+ GroovySystem.metaClassRegistry.removeMetaClass(PropParentHit)
+ assert !childSp.hasBeenInvalidated()
+ assert probe(c) == 'miss'
+ } finally {
+ GroovySystem.metaClassRegistry.removeMetaClass(PropParentHit)
+ GroovySystem.metaClassRegistry.removeMetaClass(PropChildHit)
+ }
+ '''
+ }
+
+ /**
+ * Array-lattice <em>method</em> form of a property getter on Object[] is
+ * visible on String[] via the live miss route (same as
+ * {@link #linkedMiss_stringArray_thenObjectArrayEmcAdd_isVisible}). Plain
+ * {@code arr.foo} on a Java array is GPath collect, not MetaClass property
+ * miss — so property-style EMC on arrays is exercised through getXxx.
+ */
+ @Test
+ void propertyGetter_linkedMiss_stringArray_thenObjectArrayEmcAdd_isVisible() {
+ assertScript '''
+ import org.codehaus.groovy.reflection.ClassInfo
+ import java.lang.invoke.SwitchPoint
+
+ def probe(x) {
+ try { return x.getArrOnlyProp() } catch (MissingMethodException e) { return 'miss' }
+ }
+ def arr = new String[0]
+ 50.times { assert probe(arr) == 'miss' }
+ SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
+ try {
+ Object[].metaClass.getArrOnlyProp = { -> 'array-visible' }
+ assert !stringArraySp.hasBeenInvalidated()
+ assert probe(arr) == 'array-visible'
+ } finally {
+ GroovySystem.metaClassRegistry.removeMetaClass(Object[])
+ GroovySystem.metaClassRegistry.removeMetaClass(String[])
+ }
+ '''
+ }
+
+ /**
+ * Present child property keeps winning after parent EMC adds a same-name
+ * property (construction-time / present-property selection).
+ */
+ @Test
+ void property_parentEmc_doesNotReplacePresentChildProperty() {
+ assertScript '''
+ import org.codehaus.groovy.reflection.ClassInfo
+ import java.lang.invoke.SwitchPoint
+
+ class PropParentPresent {}
+ class PropChildPresent extends PropParentPresent {
+ def getM2() { 40 }
}
- def c = new HChild()
- assert call(c) == 'miss' // monomorphic miss linked against child MetaClass
- SwitchPoint childSp = ClassInfo.getClassInfo(HChild).indySwitchPoint
+ def probe(x) { x.m2 }
+ def c = new PropChildPresent()
+ assert probe(c) == 40
+
+ SwitchPoint childSp = ClassInfo.getClassInfo(PropChildPresent).indySwitchPoint
+ PropParentPresent.metaClass.m2 = -2
try {
- HParent.metaClass.onlyOnParent = { -> 'now-visible' }
- assert childSp.hasBeenInvalidated()
- assert call(c) == 'hit'
- assert call(c) == 'hit'
+ assert !childSp.hasBeenInvalidated()
+ assert probe(c) == 40
} finally {
- GroovySystem.metaClassRegistry.removeMetaClass(HParent)
- GroovySystem.metaClassRegistry.removeMetaClass(HChild)
+ GroovySystem.metaClassRegistry.removeMetaClass(PropParentPresent)
+ GroovySystem.metaClassRegistry.removeMetaClass(PropChildPresent)
}
'''
}
+ /**
+ * Exact-class property update on the receiver class itself must retire that
+ * class domain and make the property visible (same as methods).
+ */
+ @Test
+ void property_receiverEmcAdd_invalidatesOwnSwitchPointAndIsVisible() {
+ SwitchPoint sp = ClassInfo.getClassInfo(PropOwnHost).indySwitchPoint
+ assertFalse(sp.hasBeenInvalidated())
+ PropOwnHost.metaClass.ownProp = 'yes'
+ try {
+ assertTrue(sp.hasBeenInvalidated())
+ assert new PropOwnHost().ownProp == 'yes'
+ } finally {
+ GroovySystem.metaClassRegistry.removeMetaClass(PropOwnHost)
+ }
+ }
+
static class McOwnerHost {}
+ static class PropOwnHost {}
}
diff --git a/src/test/groovy/org/apache/groovy/runtime/indy/ClassHierarchyIndexTest.groovy b/src/test/groovy/org/apache/groovy/runtime/indy/ClassHierarchyIndexTest.groovy
deleted file mode 100644
index 13bd897c9c..0000000000
--- a/src/test/groovy/org/apache/groovy/runtime/indy/ClassHierarchyIndexTest.groovy
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.groovy.runtime.indy
-
-import org.codehaus.groovy.reflection.ClassInfo
-import org.junit.jupiter.api.Test
-
-import java.lang.invoke.SwitchPoint
-
-import static org.junit.jupiter.api.Assertions.assertFalse
-import static org.junit.jupiter.api.Assertions.assertTrue
-
-/**
- * Structural and integration tests for {@link ClassHierarchyIndex}.
- */
-final class ClassHierarchyIndexTest {
-
- @Test
- void collectStrictSupertypes_classHierarchy() {
- def supers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(IndexChild, supers)
- assertTrue(supers.contains(IndexParent))
- assertTrue(supers.contains(Object))
- assertTrue(supers.contains(IndexMarker))
- assertFalse(supers.contains(IndexChild))
- assertFalse(supers.contains(IndexSibling))
- }
-
- @Test
- void collectStrictSupertypes_arrayCovariance() {
- def supers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(String[], supers)
- assertTrue(supers.contains(Object[]), 'Object[] is a covariant super of String[]')
- assertTrue(supers.contains(CharSequence[]), 'CharSequence[] via component interfaces')
- assertTrue(supers.contains(Serializable[]))
- assertTrue(supers.contains(Object))
- assertTrue(supers.contains(Cloneable))
- assertTrue(supers.contains(Serializable))
- assertFalse(supers.contains(String[]))
- assertFalse(supers.contains(Integer[]))
- }
-
- @Test
- void collectStrictSupertypes_multiDimArray() {
- def supers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(String[][], supers)
- assertTrue(supers.contains(Object[][]))
- assertTrue(supers.contains(Object[]), 'String[][] <: Object[]')
- assertTrue(supers.contains(CharSequence[][]))
- assertFalse(supers.contains(CharSequence[]), 'String[][] is not <: CharSequence[]')
- assertFalse(supers.contains(String[][]))
- }
-
- @Test
- void collectStrictSupertypes_primitiveArray() {
- def supers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(int[], supers)
- assertTrue(supers.contains(Object))
- assertTrue(supers.contains(Cloneable))
- assertTrue(supers.contains(Serializable))
- assertFalse(supers.contains(Object[]), 'int[] is not <: Object[]')
- assertFalse(supers.contains(long[]))
- }
-
- @Test
- void index_registersDescendantsUnderAncestors() {
- // Force ClassInfo construction (registers in the index).
- ClassInfo.getClassInfo(IndexParent)
- ClassInfo.getClassInfo(IndexChild)
- ClassInfo.getClassInfo(IndexSibling)
- ClassInfo.getClassInfo(IndexMarkerImpl)
-
- def underParent = []
- ClassHierarchyIndex.collectDescendants(IndexParent, underParent)
- assertTrue(underParent.any { it.theClass == IndexChild })
- assertFalse(underParent.any { it.theClass == IndexSibling })
-
- def underMarker = []
- ClassHierarchyIndex.collectDescendants(IndexMarker, underMarker)
- assertTrue(underMarker.any { it.theClass == IndexChild || it.theClass == IndexMarkerImpl },
- 'implementors indexed under IndexMarker')
- }
-
- @Test
- void index_arrayDescendants_underObjectArrayAndInterfaceArray() {
- ClassInfo.getClassInfo(String[])
- ClassInfo.getClassInfo(Integer[])
- ClassInfo.getClassInfo(Object[])
-
- def underObjectArray = []
- ClassHierarchyIndex.collectDescendants(Object[], underObjectArray)
- assertTrue(underObjectArray.any { it.theClass == String[] })
- assertTrue(underObjectArray.any { it.theClass == Integer[] })
-
- def underCharSeqArray = []
- ClassHierarchyIndex.collectDescendants(CharSequence[], underCharSeqArray)
- assertTrue(underCharSeqArray.any { it.theClass == String[] })
- assertFalse(underCharSeqArray.any { it.theClass == Integer[] })
- }
-
- @Test
- void invalidateViaIndex_fansOutWithoutMissingDescendants() {
- SwitchPoint parentSp = ClassInfo.getClassInfo(IndexFanParent).indySwitchPoint
- SwitchPoint childSp = ClassInfo.getClassInfo(IndexFanChild).indySwitchPoint
- SwitchPoint otherSp = ClassInfo.getClassInfo(IndexFanOther).indySwitchPoint
-
- IndyInvalidation.invalidateClassHierarchy(IndexFanParent)
-
- assertTrue(parentSp.hasBeenInvalidated())
- assertTrue(childSp.hasBeenInvalidated())
- assertFalse(otherSp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateViaIndex_finalLeaf_emptyDescendantsStillRetiresRoot() {
- // Finals have no indexed subtypes; single-path fan-out still retires the root.
- SwitchPoint stringSp = ClassInfo.getClassInfo(String).indySwitchPoint
- SwitchPoint siblingSp = ClassInfo.getClassInfo(IndexFanOther).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(String)
- assertTrue(stringSp.hasBeenInvalidated())
- assertFalse(siblingSp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateViaIndex_primitive_emptyDescendantsStillRetiresRoot() {
- SwitchPoint intSp = ClassInfo.getClassInfo(int).indySwitchPoint
- SwitchPoint siblingSp = ClassInfo.getClassInfo(IndexFanOther).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(int)
- assertTrue(intSp.hasBeenInvalidated())
- assertFalse(siblingSp.hasBeenInvalidated())
- }
-
- @Test
- void collectStrictSupertypes_objectAndPrimitive_areEmpty() {
- def objectSupers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(Object, objectSupers)
- assertTrue(objectSupers.isEmpty())
-
- def intSupers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(int, intSupers)
- assertTrue(intSupers.isEmpty())
-
- def nullSupers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(null, nullSupers)
- assertTrue(nullSupers.isEmpty())
- }
-
- @Test
- void collectDescendants_unknownAncestor_isEmpty() {
- def out = []
- ClassHierarchyIndex.collectDescendants(NeverRegisteredAncestor, out)
- assertTrue(out.isEmpty())
- }
-
- @Test
- void collectStrictSupertypes_interface_includesObject() {
- def supers = new LinkedHashSet<Class<?>>()
- ClassHierarchyIndex.collectStrictSupertypes(IndexMarker, supers)
- assertTrue(supers.contains(Object))
- }
-
- // --- fixtures ---
-
- private static class IndexParent {}
- private static final class IndexChild extends IndexParent implements IndexMarker {}
- private static final class IndexSibling {}
- private static interface IndexMarker {}
- private static final class IndexMarkerImpl implements IndexMarker {}
-
- private static class IndexFanParent {}
- private static final class IndexFanChild extends IndexFanParent {}
- private static final class IndexFanOther {}
- /** Never passed to ClassInfo.getClassInfo — no index entry. */
- private static final class NeverRegisteredAncestor {}
-}
diff --git a/src/test/groovy/org/apache/groovy/runtime/indy/IndyInvalidationTest.groovy b/src/test/groovy/org/apache/groovy/runtime/indy/IndyInvalidationTest.groovy
index 4eea80b6ab..3cf15effd1 100644
--- a/src/test/groovy/org/apache/groovy/runtime/indy/IndyInvalidationTest.groovy
+++ b/src/test/groovy/org/apache/groovy/runtime/indy/IndyInvalidationTest.groovy
@@ -20,6 +20,7 @@ package org.apache.groovy.runtime.indy
import groovy.lang.ExpandoMetaClass
import groovy.lang.GroovySystem
+import groovy.lang.MetaClass
import org.codehaus.groovy.reflection.ClassInfo
import org.codehaus.groovy.runtime.NullObject
import org.junit.jupiter.api.BeforeEach
@@ -37,7 +38,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue
/**
* Unit tests for {@link IndyInvalidation} — MetaClass-owned SwitchPoint model
- * with MetaClass-aware exact-class vs hierarchy fan-out (GROOVY-12191).
+ * with exact-class stock policy and bulk invalidation for custom MetaClasses
+ * (GROOVY-12191).
*/
final class IndyInvalidationTest {
@@ -74,7 +76,8 @@ final class IndyInvalidationTest {
}
@Test
- void invalidateClass_fansOutToSubtypes() {
+ void invalidateClass_doesNotFanOutToSubtypes() {
+ // Stock policy: parent invalidation is exact-class only (PR #2736).
SwitchPoint parentSp = ClassInfo.getClassInfo(Parent).indySwitchPoint
SwitchPoint childSp = ClassInfo.getClassInfo(Child).indySwitchPoint
SwitchPoint siblingSp = ClassInfo.getClassInfo(ClassB).indySwitchPoint
@@ -82,7 +85,8 @@ final class IndyInvalidationTest {
IndyInvalidation.invalidateClass(Parent)
assertTrue(parentSp.hasBeenInvalidated())
- assertTrue(childSp.hasBeenInvalidated())
+ assertFalse(childSp.hasBeenInvalidated(),
+ 'stock invalidateClass must not retire subtype SwitchPoints')
assertFalse(siblingSp.hasBeenInvalidated())
}
@@ -94,57 +98,39 @@ final class IndyInvalidationTest {
}
@Test
- void invalidateClassHierarchy_finalClass_emptyDescendantsStillRetiresRoot() {
- // Finals have no indexed subtypes; single-path fan-out still retires only the root.
- SwitchPoint stringSp = ClassInfo.getClassInfo(String).indySwitchPoint
- SwitchPoint siblingSp = ClassInfo.getClassInfo(ClassB).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(String)
- assertTrue(stringSp.hasBeenInvalidated())
- assertFalse(siblingSp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateClassHierarchy_objectArray_fansOutToReferenceArrays() {
- // Arrays are final per Class.getModifiers(), but Object[] is still a
- // MOP-relevant supertype of every reference array (GROOVY-12191 review).
+ void invalidateClass_objectArray_doesNotRetireStringArray() {
SwitchPoint objectArraySp = ClassInfo.getClassInfo(Object[]).indySwitchPoint
SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
- SwitchPoint integerArraySp = ClassInfo.getClassInfo(Integer[]).indySwitchPoint
SwitchPoint siblingSp = ClassInfo.getClassInfo(ClassB).indySwitchPoint
- assertFalse(objectArraySp.hasBeenInvalidated())
- assertFalse(stringArraySp.hasBeenInvalidated())
- IndyInvalidation.invalidateClassHierarchy(Object[])
+ IndyInvalidation.invalidateClass(Object[])
assertTrue(objectArraySp.hasBeenInvalidated())
- assertTrue(stringArraySp.hasBeenInvalidated(), 'String[] must retire when Object[] domain is invalidated')
- assertTrue(integerArraySp.hasBeenInvalidated(), 'Integer[] must retire when Object[] domain is invalidated')
+ assertFalse(stringArraySp.hasBeenInvalidated(),
+ 'array lattice fan-out is not required; miss path is live')
assertFalse(siblingSp.hasBeenInvalidated())
}
@Test
- void invalidateClassHierarchy_stringArray_doesNotRetireUnrelatedArrays() {
- // String[] has no array subtypes; sibling array types must stay live.
+ void invalidateClass_stringArray_doesNotRetireUnrelatedArrays() {
SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
SwitchPoint integerArraySp = ClassInfo.getClassInfo(Integer[]).indySwitchPoint
SwitchPoint objectArraySp = ClassInfo.getClassInfo(Object[]).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(String[])
+ IndyInvalidation.invalidateClass(String[])
assertTrue(stringArraySp.hasBeenInvalidated())
assertFalse(integerArraySp.hasBeenInvalidated())
- assertFalse(objectArraySp.hasBeenInvalidated(), 'supertype Object[] must not be retired by String[] invalidation')
+ assertFalse(objectArraySp.hasBeenInvalidated())
}
@Test
- void invalidateClassHierarchy_primitiveArray_retiresOnlyThatArrayType() {
- // Primitive arrays are final and have no subtypes, but must not take the
- // non-array final short-circuit incorrectly relative to Object[] handling.
+ void invalidateClass_primitiveArray_retiresOnlyThatArrayType() {
SwitchPoint intArraySp = ClassInfo.getClassInfo(int[]).indySwitchPoint
SwitchPoint longArraySp = ClassInfo.getClassInfo(long[]).indySwitchPoint
SwitchPoint objectArraySp = ClassInfo.getClassInfo(Object[]).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(int[])
+ IndyInvalidation.invalidateClass(int[])
assertTrue(intArraySp.hasBeenInvalidated())
assertFalse(longArraySp.hasBeenInvalidated())
@@ -152,55 +138,34 @@ final class IndyInvalidationTest {
}
@Test
- void invalidateClass_objectArray_viaApi_fansOut() {
- SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
- IndyInvalidation.invalidateClass(Object[])
- assertTrue(stringArraySp.hasBeenInvalidated())
+ void invalidateClass_interface_doesNotFanOutToImplementors() {
+ SwitchPoint ifaceSp = ClassInfo.getClassInfo(Marker).indySwitchPoint
+ SwitchPoint implSp = ClassInfo.getClassInfo(MarkerImpl).indySwitchPoint
+ SwitchPoint otherSp = ClassInfo.getClassInfo(ClassB).indySwitchPoint
+
+ IndyInvalidation.invalidateClass(Marker)
+
+ assertTrue(ifaceSp.hasBeenInvalidated())
+ assertFalse(implSp.hasBeenInvalidated())
+ assertFalse(otherSp.hasBeenInvalidated())
}
@Test
- void guardWithMopSwitchPoints_stringArrayFallsBackOnObjectArrayInvalidate() {
+ void guardWithMopSwitchPoints_stringArrayFallsBackOnOwnInvalidateOnly() {
def target = MethodHandles.constant(int, 11)
def fallback = MethodHandles.constant(int, 12)
def receiver = new String[0]
def guarded = IndyInvalidation.guardWithMopSwitchPoints(target, fallback, receiver)
assertEquals(11, guarded.invokeWithArguments())
+ // Parent array-type invalidation must not deopt String[] sites.
IndyInvalidation.invalidateClass(Object[])
- assertEquals(12, guarded.invokeWithArguments(),
- 'String[]-linked site must fall back after Object[] MetaClass-domain invalidation')
- }
+ assertEquals(11, guarded.invokeWithArguments(),
+ 'String[] site must stay linked when only Object[] domain retires')
- @Test
- void invalidateClassHierarchy_interfaceArray_fansOutToImplementorArrays() {
- // Same final-array trap as Object[]: CharSequence[] is final yet
- // assignable-from String[] (interface-array covariance).
- SwitchPoint charSeqArraySp = ClassInfo.getClassInfo(CharSequence[]).indySwitchPoint
- SwitchPoint stringArraySp = ClassInfo.getClassInfo(String[]).indySwitchPoint
- SwitchPoint integerArraySp = ClassInfo.getClassInfo(Integer[]).indySwitchPoint
-
- IndyInvalidation.invalidateClassHierarchy(CharSequence[])
-
- assertTrue(charSeqArraySp.hasBeenInvalidated())
- assertTrue(stringArraySp.hasBeenInvalidated(),
- 'String[] must retire when CharSequence[] domain is invalidated')
- assertFalse(integerArraySp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateClassHierarchy_multiDimObjectArray_fansOut() {
- // Multi-dim arrays are also final classes; Object[][] still fans out to String[][].
- SwitchPoint object2dSp = ClassInfo.getClassInfo(Object[][]).indySwitchPoint
- SwitchPoint string2dSp = ClassInfo.getClassInfo(String[][]).indySwitchPoint
- SwitchPoint string1dSp = ClassInfo.getClassInfo(String[]).indySwitchPoint
-
- IndyInvalidation.invalidateClassHierarchy(Object[][])
-
- assertTrue(object2dSp.hasBeenInvalidated())
- assertTrue(string2dSp.hasBeenInvalidated(),
- 'String[][] must retire when Object[][] domain is invalidated')
- assertFalse(string1dSp.hasBeenInvalidated(),
- '1-D String[] is not assignable-from Object[][]')
+ IndyInvalidation.invalidateClass(String[])
+ assertEquals(12, guarded.invokeWithArguments(),
+ 'String[] site must fall back after its own domain retires')
}
@Test
@@ -212,7 +177,6 @@ final class IndyInvalidationTest {
assertTrue(classSp.hasBeenInvalidated())
assertEquals(catCount + 1, IndyInvalidation.categoryInvalidationCount())
- // Fresh SP available after bulk retire
SwitchPoint fresh = ClassInfo.getClassInfo(ClassA).indySwitchPoint
assertFalse(fresh.hasBeenInvalidated())
}
@@ -221,23 +185,54 @@ final class IndyInvalidationTest {
void invalidateUnscoped_retiresLoadedClassSwitchPoints() {
SwitchPoint spA = ClassInfo.getClassInfo(ClassA).indySwitchPoint
SwitchPoint spB = ClassInfo.getClassInfo(ClassB).indySwitchPoint
- long before = IndyInvalidation.classInvalidationCount()
+ long bulkBefore = IndyInvalidation.bulkInvalidationCount()
+ long classBefore = IndyInvalidation.classInvalidationCount()
IndyInvalidation.invalidateUnscoped()
assertTrue(spA.hasBeenInvalidated())
assertTrue(spB.hasBeenInvalidated())
- assertEquals(before + 1, IndyInvalidation.classInvalidationCount())
+ assertEquals(bulkBefore + 1, IndyInvalidation.bulkInvalidationCount(),
+ 'unscoped is process-wide bulk (non-category)')
+ assertEquals(classBefore, IndyInvalidation.classInvalidationCount(),
+ 'unscoped must not increment exact-class counter')
}
@Test
- void classInfoIncVersion_invalidatesClassDomain_notSiblings() {
+ void invalidateBulk_retiresAllLoadedClassDomains() {
SwitchPoint spA = ClassInfo.getClassInfo(ClassA).indySwitchPoint
SwitchPoint spB = ClassInfo.getClassInfo(ClassB).indySwitchPoint
- int versionBefore = ClassInfo.getClassInfo(ClassA).version
+ long bulkBefore = IndyInvalidation.bulkInvalidationCount()
+ long classBefore = IndyInvalidation.classInvalidationCount()
+ long catBefore = IndyInvalidation.categoryInvalidationCount()
- ClassInfo.getClassInfo(ClassA).incVersion()
+ IndyInvalidation.invalidateBulk()
- assertEquals(versionBefore + 1, ClassInfo.getClassInfo(ClassA).version)
assertTrue(spA.hasBeenInvalidated())
+ assertTrue(spB.hasBeenInvalidated())
+ assertEquals(bulkBefore + 1, IndyInvalidation.bulkInvalidationCount())
+ assertEquals(classBefore, IndyInvalidation.classInvalidationCount())
+ assertEquals(catBefore, IndyInvalidation.categoryInvalidationCount())
+ }
+
+ @Test
+ void invalidateCategory_doesNotIncrementBulkCounter() {
+ long bulkBefore = IndyInvalidation.bulkInvalidationCount()
+ IndyInvalidation.invalidateCategory()
+ assertEquals(bulkBefore, IndyInvalidation.bulkInvalidationCount(),
+ 'category has its own counter; must not share bulk tally')
+ }
+
+ @Test
+ void classInfoIncVersion_invalidatesClassDomain_notSiblingsOrSubtypes() {
+ SwitchPoint spA = ClassInfo.getClassInfo(Parent).indySwitchPoint
+ SwitchPoint childSp = ClassInfo.getClassInfo(Child).indySwitchPoint
+ SwitchPoint spB = ClassInfo.getClassInfo(ClassB).indySwitchPoint
+ int versionBefore = ClassInfo.getClassInfo(Parent).version
+
+ ClassInfo.getClassInfo(Parent).incVersion()
+
+ assertEquals(versionBefore + 1, ClassInfo.getClassInfo(Parent).version)
+ assertTrue(spA.hasBeenInvalidated())
+ assertFalse(childSp.hasBeenInvalidated())
assertFalse(spB.hasBeenInvalidated())
}
@@ -263,7 +258,6 @@ final class IndyInvalidationTest {
def guarded = IndyInvalidation.guardWithMopSwitchPoints(target, fallback, receiver)
assertEquals(1, guarded.invokeWithArguments())
- // Category maps to bulk class-domain invalidation (single-guard model).
IndyInvalidation.invalidateCategory()
assertEquals(2, guarded.invokeWithArguments())
}
@@ -292,90 +286,51 @@ final class IndyInvalidationTest {
void resetCountersForTesting_zerosCounters() {
IndyInvalidation.invalidateClass(ClassA)
IndyInvalidation.invalidateCategory()
- IndyInvalidation.invalidateClassExact(ClassB)
+ IndyInvalidation.invalidateBulk()
+ IndyInvalidation.invalidateUnscoped()
assertTrue(IndyInvalidation.classInvalidationCount() > 0)
assertTrue(IndyInvalidation.categoryInvalidationCount() > 0)
- assertTrue(IndyInvalidation.hierarchyInvalidationCount() > 0)
- assertTrue(IndyInvalidation.exactOnlyInvalidationCount() > 0)
+ assertTrue(IndyInvalidation.bulkInvalidationCount() > 0)
IndyInvalidation.resetCountersForTesting()
assertEquals(0, IndyInvalidation.classInvalidationCount())
assertEquals(0, IndyInvalidation.categoryInvalidationCount())
- assertEquals(0, IndyInvalidation.hierarchyInvalidationCount())
- assertEquals(0, IndyInvalidation.exactOnlyInvalidationCount())
+ assertEquals(0, IndyInvalidation.bulkInvalidationCount())
}
@Test
- void invalidateClassExact_doesNotFanOutToSubtypes() {
- SwitchPoint parentSp = ClassInfo.getClassInfo(Parent).indySwitchPoint
- SwitchPoint childSp = ClassInfo.getClassInfo(Child).indySwitchPoint
- IndyInvalidation.invalidateClassExact(Parent)
- assertTrue(parentSp.hasBeenInvalidated())
- assertFalse(childSp.hasBeenInvalidated())
- }
-
- @Test
- void needsHierarchyFanOut_emcVsMetaClassImpl() {
+ void isStockMetaClass_andNeedsBulkInvalidation() {
def mcImpl = new groovy.lang.MetaClassImpl(ClassA)
mcImpl.initialize()
def emc = new ExpandoMetaClass(ClassA, true, true)
emc.initialize()
+ def custom = new PureCustomMetaClass(ClassA)
- assertTrue(IndyInvalidation.isHierarchyLocalMetaClass(mcImpl))
- assertTrue(IndyInvalidation.isHierarchyLocalMetaClass(null))
- assertFalse(IndyInvalidation.isHierarchyLocalMetaClass(emc))
-
- assertFalse(IndyInvalidation.needsHierarchyFanOut(ClassA, mcImpl, mcImpl),
- 'pure MetaClassImpl replace must not require fan-out')
- assertFalse(IndyInvalidation.needsHierarchyFanOut(ClassA, null, mcImpl),
- 'first MetaClassImpl install must not require fan-out')
- assertTrue(IndyInvalidation.needsHierarchyFanOut(ClassA, null, emc),
- 'EMC install must require fan-out')
- assertTrue(IndyInvalidation.needsHierarchyFanOut(ClassA, emc, mcImpl),
- 'EMC remove must require fan-out')
- assertTrue(IndyInvalidation.needsHierarchyFanOut(Marker, mcImpl, mcImpl),
- 'interface MetaClass change must require fan-out')
- assertTrue(IndyInvalidation.needsHierarchyFanOut(String[], mcImpl, mcImpl),
- 'array MetaClass change must require fan-out')
- }
-
- @Test
- void needsHierarchyFanOut_modifiedCustomMetaClass_fansOut() {
- // MetaClassImpl subclass with isModified()==true is not hierarchy-local
- // (same gate as findMethodInClassHierarchy).
- def custom = new ModifiedCustomMetaClass(ClassA)
- custom.initialize()
- assertFalse(IndyInvalidation.isHierarchyLocalMetaClass(custom))
- def mcImpl = new groovy.lang.MetaClassImpl(ClassA)
- mcImpl.initialize()
- assertTrue(IndyInvalidation.needsHierarchyFanOut(ClassA, mcImpl, custom),
- 'modified custom MetaClass must fan out (correctness-first)')
- }
+ assertTrue(IndyInvalidation.isStockMetaClass(mcImpl))
+ assertTrue(IndyInvalidation.isStockMetaClass(null))
+ assertTrue(IndyInvalidation.isStockMetaClass(emc),
+ 'EMC is stock MetaClassImpl; exact-class is sufficient')
+ assertFalse(IndyInvalidation.isStockMetaClass(custom),
+ 'non-MetaClassImpl custom kinds require bulk')
- @Test
- void needsHierarchyFanOut_globalEmcEnabled() {
- def mcImpl = new groovy.lang.MetaClassImpl(ClassA)
- mcImpl.initialize()
- try {
- ExpandoMetaClass.enableGlobally()
- assertTrue(IndyInvalidation.isGlobalEmcEnabled())
- assertTrue(IndyInvalidation.needsHierarchyFanOut(ClassA, mcImpl, mcImpl),
- 'global EMC must force hierarchy fan-out even for MetaClassImpl pair')
- } finally {
- ExpandoMetaClass.disableGlobally()
- assertFalse(IndyInvalidation.isGlobalEmcEnabled())
- }
+ assertFalse(IndyInvalidation.needsBulkInvalidation(mcImpl, mcImpl))
+ assertFalse(IndyInvalidation.needsBulkInvalidation(null, mcImpl))
+ assertFalse(IndyInvalidation.needsBulkInvalidation(null, emc),
+ 'EMC install is stock → exact-class')
+ assertFalse(IndyInvalidation.needsBulkInvalidation(emc, mcImpl))
+ assertFalse(IndyInvalidation.needsBulkInvalidation(mcImpl, mcImpl))
+ assertTrue(IndyInvalidation.needsBulkInvalidation(mcImpl, custom),
+ 'custom MetaClass must bulk-invalidate')
+ assertTrue(IndyInvalidation.needsBulkInvalidation(custom, null))
}
@Test
- void isExpandoMetaClass_detectsDirectAndWrapped() {
+ void isStockMetaClass_unwrapsHandleMetaClass() {
def emc = new ExpandoMetaClass(ClassA, true, true)
emc.initialize()
- assertTrue(IndyInvalidation.isExpandoMetaClass(emc))
- assertFalse(IndyInvalidation.isExpandoMetaClass(new groovy.lang.MetaClassImpl(ClassA)))
- assertFalse(IndyInvalidation.isExpandoMetaClass(null))
def handle = new org.codehaus.groovy.runtime.HandleMetaClass(emc)
- assertTrue(IndyInvalidation.isExpandoMetaClass(handle),
- 'HandleMetaClass wrapping EMC must be recognised')
+ assertTrue(IndyInvalidation.isStockMetaClass(handle),
+ 'HandleMetaClass wrapping EMC must unwrap to stock MetaClassImpl')
+ assertFalse(IndyInvalidation.needsBulkInvalidation(null, handle))
}
@Test
@@ -397,7 +352,8 @@ final class IndyInvalidationTest {
}
@Test
- void invalidateForMetaClassChange_hierarchyForEmc() {
+ void invalidateForMetaClassChange_exactForEmc() {
+ SwitchPoint parentSp = ClassInfo.getClassInfo(PolicyParentEmc).indySwitchPoint
SwitchPoint childSp = ClassInfo.getClassInfo(PolicyChildEmc).indySwitchPoint
def emc = new ExpandoMetaClass(PolicyParentEmc, true, true)
emc.initialize()
@@ -406,8 +362,26 @@ final class IndyInvalidationTest {
IndyInvalidation.invalidateForMetaClassChange(event)
- assertTrue(childSp.hasBeenInvalidated(),
- 'EMC install on parent must fan out to child')
+ assertTrue(parentSp.hasBeenInvalidated())
+ assertFalse(childSp.hasBeenInvalidated(),
+ 'EMC install is stock → exact-class, no subtype fan-out')
+ }
+
+ @Test
+ void invalidateForMetaClassChange_bulkForCustomMetaClass() {
+ SwitchPoint spA = ClassInfo.getClassInfo(ClassA).indySwitchPoint
+ SwitchPoint spB = ClassInfo.getClassInfo(ClassB).indySwitchPoint
+ def custom = new PureCustomMetaClass(ClassA)
+ def event = new groovy.lang.MetaClassRegistryChangeEvent(
+ GroovySystem.metaClassRegistry, null, ClassA, null, custom)
+
+ long bulkBefore = IndyInvalidation.bulkInvalidationCount()
+ IndyInvalidation.invalidateForMetaClassChange(event)
+
+ assertTrue(spA.hasBeenInvalidated())
+ assertTrue(spB.hasBeenInvalidated(),
+ 'custom MetaClass must bulk-retire loaded class domains')
+ assertEquals(bulkBefore + 1, IndyInvalidation.bulkInvalidationCount())
}
@Test
@@ -446,7 +420,6 @@ final class IndyInvalidationTest {
def mc = new groovy.lang.MetaClassImpl(LocalGen)
mc.initialize()
- // Domain is per MetaClass identity; first install must not retire it.
SwitchPoint sp = IndyInvalidation.switchPointForMetaClass(mc)
assertFalse(sp.hasBeenInvalidated())
info.setStrongMetaClass(mc)
@@ -492,50 +465,13 @@ final class IndyInvalidationTest {
}
@Test
- void invalidateClassHierarchy_interface_fansOutToImplementors() {
- SwitchPoint ifaceSp = ClassInfo.getClassInfo(Marker).indySwitchPoint
- SwitchPoint implSp = ClassInfo.getClassInfo(MarkerImpl).indySwitchPoint
- SwitchPoint otherSp = ClassInfo.getClassInfo(ClassB).indySwitchPoint
-
- IndyInvalidation.invalidateClassHierarchy(Marker)
-
- assertTrue(ifaceSp.hasBeenInvalidated())
- assertTrue(implSp.hasBeenInvalidated())
- assertFalse(otherSp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateClassHierarchy_primitive_emptyDescendantsStillRetiresRoot() {
- // Primitives have no indexed subtypes; single-path fan-out still retires the root.
- SwitchPoint intSp = ClassInfo.getClassInfo(int).indySwitchPoint
- SwitchPoint sibling = ClassInfo.getClassInfo(ClassB).indySwitchPoint
- IndyInvalidation.invalidateClassHierarchy(int)
- assertTrue(intSp.hasBeenInvalidated())
- assertFalse(sibling.hasBeenInvalidated())
- }
-
- @Test
- void invalidateClassHierarchy_whenRootAlreadyDetached_stillFansOut() {
- // Root detach returns null; subtypes still retire (covers rootSp == null branch).
- def parentInfo = ClassInfo.getClassInfo(ParentAlreadyDetached)
- def childInfo = ClassInfo.getClassInfo(ChildAlreadyDetached)
- SwitchPoint parentSp = parentInfo.indySwitchPoint
- SwitchPoint childSp = childInfo.indySwitchPoint
- parentInfo.detachLiveIndySwitchPoint() // retire without invalidateAll
- assertTrue(parentSp != null)
- IndyInvalidation.invalidateClassHierarchy(ParentAlreadyDetached)
- // parent was already detached (not necessarily invalidated); child must be retired
- assertTrue(childSp.hasBeenInvalidated())
- }
-
- @Test
- void invalidateAllLoaded_whenNoLiveSwitchPoints_isNoOpBatch() {
+ void bulkWhenNoLiveSwitchPoints_isSafeNoOp() {
// Detach every tracked type used by this suite so the bulk batch is empty.
ClassInfo.getClassInfo(ClassA).detachLiveIndySwitchPoint()
ClassInfo.getClassInfo(ClassB).detachLiveIndySwitchPoint()
- IndyInvalidation.invalidateAllLoadedClassSwitchPoints() // empty-batch early return
- // still callable and safe
- IndyInvalidation.invalidateAllLoadedClassSwitchPoints()
+ IndyInvalidation.invalidateBulk()
+ IndyInvalidation.invalidateCategory()
+ IndyInvalidation.invalidateUnscoped()
}
@Test
@@ -551,13 +487,12 @@ final class IndyInvalidationTest {
@Test
void statsLogging_enabled_inChildProcess() {
- // STATS_LOG is fixed at class init; cover the fine-log branches in a fresh JVM.
def javaBin = System.getProperty('java.home') + '/bin/java'
def cp = System.getProperty('java.class.path')
def pb = new ProcessBuilder(
javaBin,
'-Dgroovy.indy.invalidation.stats=true',
- '-Djava.util.logging.config.file=', // allow programmatic FINE below via default
+ '-Djava.util.logging.config.file=',
'-cp', cp,
'org.apache.groovy.runtime.indy.IndyInvalidationStatsProbe')
pb.redirectErrorStream(true)
@@ -567,20 +502,6 @@ final class IndyInvalidationTest {
assertTrue(out.contains('OK'), out)
}
- @Test
- void invalidateClassHierarchy_skipsClearedClassInfoWeakRefs() {
- // Force a ClassInfo whose weak Class ref is cleared so the scan hits loaded == null.
- def info = ClassInfo.getClassInfo(ClearedRefHost)
- info.indySwitchPoint // ensure live
- def field = ClassInfo.getDeclaredField('classRef')
- field.accessible = true
- def wr = (WeakReference) field.get(info)
- wr.clear()
- assertEquals(null, info.theClass)
- // Must not throw; other ClassInfos still scanned.
- IndyInvalidation.invalidateClassHierarchy(Parent)
- }
-
@Test
void classInfoIncVersion_whenTheClassCleared_invalidatesLocally() {
def info = ClassInfo.getClassInfo(ClearedIncVersionHost)
@@ -589,7 +510,7 @@ final class IndyInvalidationTest {
field.accessible = true
((WeakReference) field.get(info)).clear()
assertEquals(null, info.theClass)
- info.incVersion() // else branch: invalidateIndySwitchPoint only
+ info.incVersion()
assertTrue(sp.hasBeenInvalidated())
}
@@ -597,7 +518,6 @@ final class IndyInvalidationTest {
void setPerInstanceMetaClass_retiresClassSwitchPoint() {
def type = Groovy12191PerInstHost
def info = ClassInfo.getClassInfo(type)
- // Construct before installing a custom class-level MetaClass.
def instance = new Groovy12191PerInstHost()
SwitchPoint sp = info.indySwitchPoint
assertFalse(sp.hasBeenInvalidated())
@@ -608,7 +528,6 @@ final class IndyInvalidationTest {
info.setPerInstanceMetaClass(instance, null)
}
-
@Test
void hasClassLevelMetaClass_weakOnly_firstInstallDoesNotRetire() {
def info = ClassInfo.getClassInfo(WeakOnlyHost)
@@ -617,7 +536,7 @@ final class IndyInvalidationTest {
def mc = new groovy.lang.MetaClassImpl(WeakOnlyHost)
mc.initialize()
SwitchPoint sp = IndyInvalidation.switchPointForMetaClass(mc)
- info.setWeakMetaClass(mc) // first weak install
+ info.setWeakMetaClass(mc)
assertFalse(sp.hasBeenInvalidated())
assertSame(sp, info.indySwitchPoint)
info.setWeakMetaClass(null)
@@ -642,7 +561,6 @@ final class IndyInvalidationTest {
assertFalse(sp.hasBeenInvalidated())
IndyInvalidation.invalidateMetaClass(mc)
assertTrue(sp.hasBeenInvalidated())
- // Fresh domain after retirement.
assertFalse(IndyInvalidation.switchPointForMetaClass(mc).hasBeenInvalidated())
}
@@ -656,7 +574,7 @@ final class IndyInvalidationTest {
assertFalse(pending.hasBeenInvalidated())
def mc = new groovy.lang.MetaClassImpl(type)
mc.initialize()
- info.strongMetaClass = mc // first install retires pending
+ info.strongMetaClass = mc
assertTrue(pending.hasBeenInvalidated())
SwitchPoint post = IndyInvalidation.classSwitchPointFor(type)
assertSame(IndyInvalidation.switchPointForMetaClass(mc), post)
@@ -666,8 +584,6 @@ final class IndyInvalidationTest {
@Test
void classSwitchPointFor_doesNotForceMetaClassDuringLink() {
- // Regression for script+closure defineClass order: classSwitchPointFor
- // must not call getMetaClass() which loads nested classes early.
def type = NoForceMcHost
GroovySystem.metaClassRegistry.removeMetaClass(type)
assertEquals(null, ClassInfo.getClassInfo(type).metaClassForClass)
@@ -682,23 +598,24 @@ final class IndyInvalidationTest {
impl.initialize()
def custom = new groovy.lang.DelegatingMetaClass(impl) {}
SwitchPoint sp = IndyInvalidation.switchPointForMetaClass(custom)
- // Unwraps to adaptee MetaClassImpl — same domain key.
assertSame(sp, IndyInvalidation.switchPointForMetaClass(impl))
IndyInvalidation.invalidateMetaClass(impl)
assertTrue(sp.hasBeenInvalidated())
}
+ @Test
+ void invalidateMetaClass_null_isNoOp() {
+ IndyInvalidation.invalidateMetaClass(null)
+ }
+
private static final class ClassA {}
private static final class ClassB {}
private static class Parent {}
private static final class Child extends Parent {}
- private static class ParentAlreadyDetached {}
- private static final class ChildAlreadyDetached extends ParentAlreadyDetached {}
private static final class LocalGen {}
private static final class LocalGenReplace {}
private static final class LocalGenWeak {}
private static final class WeakOnlyHost {}
- private static final class ClearedRefHost {}
private static final class ClearedIncVersionHost {}
private static interface Marker {}
private static final class MarkerImpl implements Marker {}
@@ -715,10 +632,18 @@ final class IndyInvalidationTest {
/** Top-level host for per-instance MetaClass SwitchPoint coverage. */
class Groovy12191PerInstHost {}
-/** MetaClassImpl subclass that reports modified, forcing hierarchy fan-out. */
-class ModifiedCustomMetaClass extends groovy.lang.MetaClassImpl {
- ModifiedCustomMetaClass(Class theClass) { super(theClass) }
- @Override
- boolean isModified() { true }
-}
+/**
+ * Pure custom MetaClass (not a {@code MetaClassImpl}) used to exercise bulk
+ * invalidation for non-stock kinds. {@code @Delegate} fills the MetaClass
+ * surface; {@link IndyInvalidation#isStockMetaClass} sees this type itself
+ * (not the adaptee), so it is classified as non-stock.
+ */
+class PureCustomMetaClass implements MetaClass {
+ @Delegate
+ final MetaClass delegate
+ PureCustomMetaClass(Class theClass) {
+ this.delegate = new groovy.lang.MetaClassImpl(theClass)
+ this.delegate.initialize()
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyScopedSwitchPointTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyScopedSwitchPointTest.groovy
index 23477b02f3..37e2799517 100644
--- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyScopedSwitchPointTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyScopedSwitchPointTest.groovy
@@ -40,7 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue
final class IndyScopedSwitchPointTest {
@Test
- void registryListener_withClass_invalidatesHierarchy() {
+ void registryListener_withClass_invalidatesClassDomain() {
// Ensure the MetaClass registry listener path (type != null) runs.
SwitchPoint sp = ClassInfo.getClassInfo(RegistryHost).indySwitchPoint
def mc = new ExpandoMetaClass(RegistryHost, true, true)
diff --git a/src/test/java/org/apache/groovy/runtime/indy/IndyInvalidationStatsProbe.java b/src/test/java/org/apache/groovy/runtime/indy/IndyInvalidationStatsProbe.java
index 3e4822d117..1ff3bc7cb9 100644
--- a/src/test/java/org/apache/groovy/runtime/indy/IndyInvalidationStatsProbe.java
+++ b/src/test/java/org/apache/groovy/runtime/indy/IndyInvalidationStatsProbe.java
@@ -41,6 +41,7 @@ public final class IndyInvalidationStatsProbe {
}
IndyInvalidation.invalidateClass(ProbeHost.class);
+ IndyInvalidation.invalidateBulk();
IndyInvalidation.invalidateCategory();
IndyInvalidation.invalidateUnscoped();
System.out.println("OK");
diff --git a/subprojects/performance/README.adoc b/subprojects/performance/README.adoc
index 50f5d476aa..c6085a9baf 100644
--- a/subprojects/performance/README.adoc
+++ b/subprojects/performance/README.adoc
@@ -137,11 +137,12 @@ hit, GROOVY-11935), hence the two benches.
== Scoped invalidation (GROOVY-12191)
`ScopedInvalidationBench` measures per-class SwitchPoint scoping: unrelated
-MetaClass churn must not deoptimize a hot monomorphic site; same-type re-link,
-category enter/leave (bulk class-domain invalidation), and hierarchy fan-out
-must still force re-link. Steady-state monomorphic sites carry a single
-SwitchPoint guard. Complements the Grails-oriented `CallSiteInvalidationBench`
-under `org.apache.groovy.perf.grails`.
+MetaClass churn must not deoptimize a hot monomorphic site; same-type re-link
+and category enter/leave (bulk class-domain invalidation) must still force
+re-link; parent MetaClass churn must not deoptimize subclass sites (stock
+exact-class policy — no hierarchy fan-out). Steady-state monomorphic sites
+carry a single SwitchPoint guard. Complements the Grails-oriented
+`CallSiteInvalidationBench` under `org.apache.groovy.perf.grails`.
== Dashboard pages
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ScopedInvalidationBench.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ScopedInvalidationBench.groovy
index 340109db61..a46754c777 100644
--- a/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ScopedInvalidationBench.groovy
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ScopedInvalidationBench.groovy
@@ -43,16 +43,16 @@ import java.util.concurrent.TimeUnit
* per-class SwitchPoint domain. Baseline / cross-type / same-type rows
* intentionally overlap that bench so ratios are available under the same
* {@code .bench.} include pattern; unique coverage here is category enter/leave
- * (bulk class-domain invalidation), hierarchy fan-out, and setup-isolated
- * post-burst steady state:
+ * (bulk class-domain invalidation), parent-vs-child isolation (stock exact-class
+ * — no hierarchy fan-out), and setup-isolated post-burst steady state:
* <ul>
* <li><b>Per-class</b> — MetaClass changes on an unrelated type must not
* deoptimize a hot monomorphic site;</li>
* <li><b>Same-type</b> — MetaClass changes on the hot type must re-link;</li>
* <li><b>Category</b> — {@code use(Category)} enter/leave bulk-invalidates
* class SwitchPoints so category methods become visible;</li>
- * <li><b>Hierarchy fan-out</b> — parent MetaClass changes invalidate
- * subclass-linked sites.</li>
+ * <li><b>Parent churn</b> — parent MetaClass changes must <em>not</em>
+ * deoptimize subclass-linked sites (stock exact-class policy).</li>
* </ul>
* Compare {@link #hotLoop_afterUnrelatedBurst} / {@link #hotLoop_unrelatedMetaClassChurn}
* against {@link #hotLoop_baseline} (should stay close after scoping) and against
@@ -90,14 +90,15 @@ class ScopedInvalidationBench {
}
/**
- * Parent of {@link HierChild} for hierarchy fan-out churn.
+ * Parent of {@link HierChild} for parent-vs-child isolation churn.
*/
static class HierParent {
int base() { 1 }
}
/**
- * Subclass receiver whose SwitchPoint must clear when the parent MetaClass changes.
+ * Subclass receiver whose SwitchPoint must stay live when only the parent
+ * MetaClass changes (stock exact-class policy).
*/
static class HierChild extends HierParent {
int leaf() { 2 }
@@ -194,8 +195,8 @@ class ScopedInvalidationBench {
}
/**
- * Category enter/leave interleaved with hot calls — exercises the
- * process-wide category SwitchPoint domain (never fails over).
+ * Category enter/leave interleaved with hot calls — exercises
+ * {@code invalidateCategory()} bulk retirement of loaded class domains.
* Uses a modest number of {@code use} blocks so category re-link cost
* is visible without dominating wall-clock.
* @param bh blackhole for the accumulated result
@@ -215,12 +216,13 @@ class ScopedInvalidationBench {
}
/**
- * Subclass receiver calls with periodic parent MetaClass changes (hierarchy
- * fan-out must invalidate the child SwitchPoint).
+ * Subclass receiver calls with periodic parent MetaClass changes. Under
+ * stock exact-class policy this should stay close to
+ * {@link #parentChild_baseline} (parent churn must not deopt the child).
* @param bh blackhole for the accumulated result
*/
@Benchmark
- void hierarchy_parentMetaClassChurn(Blackhole bh) {
+ void parentChild_parentMetaClassChurn(Blackhole bh) {
int sum = 0
for (int i = 0; i < ITERATIONS; i++) {
sum += child.leaf()
@@ -233,11 +235,11 @@ class ScopedInvalidationBench {
}
/**
- * Subclass receiver calls with no MetaClass mutation (hierarchy baseline).
+ * Subclass receiver calls with no MetaClass mutation (parent/child baseline).
* @param bh blackhole for the accumulated result
*/
@Benchmark
- void hierarchy_baseline(Blackhole bh) {
+ void parentChild_baseline(Blackhole bh) {
int sum = 0
for (int i = 0; i < ITERATIONS; i++) {
sum += child.leaf()