diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java index a356f0a20c5..eab3dbf0416 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -124,4 +124,26 @@ public MethodHandle getFallbackTarget() { public void setFallbackTarget(MethodHandle fallbackTarget) { this.fallbackTarget = fallbackTarget; } + + /** + * Clear the cache entirely. Called when metaclass changes to ensure + * stale method handles are discarded. + *
+ * The volatile field {@code latestHitMethodHandleWrapperSoftReference} is cleared + * outside the synchronized block to be consistent with {@link #getAndPut}, which + * also accesses this field outside the lock. Volatile provides sufficient visibility + * guarantees for this field. + */ + public void clearCache() { + // Clear the latest hit reference using volatile semantics + latestHitMethodHandleWrapperSoftReference = null; + + // Clear the LRU cache under lock to synchronize with concurrent getAndPut operations + synchronized (lruCache) { + lruCache.clear(); + } + + // Reset fallback count (atomic operation, doesn't need to be in sync block) + fallbackCount.set(0); + } } 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 5f738118df5..0d10e109cfa 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -32,7 +32,10 @@ import java.lang.invoke.MethodType; import java.lang.invoke.MutableCallSite; import java.lang.invoke.SwitchPoint; +import java.lang.ref.WeakReference; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Function; import java.util.logging.Level; @@ -50,6 +53,13 @@ public class IndyInterface { private static final long INDY_OPTIMIZE_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.optimize.threshold", 10_000L); private static final long INDY_FALLBACK_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.fallback.threshold", 10_000L); + /** + * Initial capacity for the call site registry used to track all active call sites + * for cache invalidation when metaclass changes occur. The default of 1024 balances + * memory usage against resize overhead. Tune via {@code groovy.indy.callsite.initial.capacity} + * system property for larger applications. + */ + private static final int INDY_CALLSITE_INITIAL_CAPACITY = SystemUtil.getIntegerSafe("groovy.indy.callsite.initial.capacity", 1024); /** * flags for method and property calls @@ -168,17 +178,42 @@ public static CallType fromCallSiteName(String callSiteName) { } protected static SwitchPoint switchPoint = new SwitchPoint(); + + /** + * Weak set of all CacheableCallSites. Used to invalidate caches when metaclass changes. + * Uses WeakReferences so call sites can be garbage collected when no longer referenced. + *
+ * Note: Stale (garbage-collected) WeakReferences are cleaned up during each call to
+ * {@link #invalidateSwitchPoints()}. In applications with infrequent metaclass changes,
+ * the set may accumulate some dead references between invalidations. This is acceptable
+ * because: (1) the memory overhead per dead reference is minimal (~16 bytes), and
+ * (2) frameworks like Grails that benefit most from this optimization have frequent
+ * metaclass changes that trigger regular cleanup.
+ */
+ private static final Set
+ * Registered call sites are held via WeakReferences and will be automatically
+ * removed when garbage collected. Cleanup of stale references occurs during
+ * {@link #invalidateSwitchPoints()}.
+ */
+ static void registerCallSite(CacheableCallSite callSite) {
+ ALL_CALL_SITES.add(new WeakReference<>(callSite));
+ }
/**
- * Callback for constant metaclass update change
+ * Callback for constant metaclass update change.
+ * Invalidates all call site caches to ensure metaclass changes are visible.
*/
protected static void invalidateSwitchPoints() {
if (LOG_ENABLED) {
- LOG.info("invalidating switch point");
+ LOG.info("invalidating switch point and call site caches");
}
synchronized (IndyInterface.class) {
@@ -186,6 +221,27 @@ protected static void invalidateSwitchPoints() {
switchPoint = new SwitchPoint();
SwitchPoint.invalidateAll(new SwitchPoint[]{old});
}
+
+ // Invalidate all call site caches and reset targets to default (cache lookup).
+ // This ensures metaclass changes are visible without using expensive switchpoint guards.
+ // Note: This is best-effort invalidation. A concurrent thread in fromCache() may briefly
+ // reinstall an optimized target after we reset it. This is acceptable because the method
+ // handle guards (SAME_MC, SAME_CLASS) will fail on the next call if the metaclass changed,
+ // forcing a fallback to selectMethod() which will resolve the correct method.
+ ALL_CALL_SITES.removeIf(ref -> {
+ CacheableCallSite cs = ref.get();
+ if (cs == null) {
+ return true; // Remove garbage collected references
+ }
+ // Reset target to default (fromCache) so next call goes through cache lookup
+ MethodHandle defaultTarget = cs.getDefaultTarget();
+ if (defaultTarget != null && cs.getTarget() != defaultTarget) {
+ cs.setTarget(defaultTarget);
+ }
+ // Clear the cache so stale method handles are discarded
+ cs.clearCache();
+ return false;
+ });
}
/**
@@ -230,6 +286,9 @@ private static CallSite realBootstrap(Lookup caller, String name, int callID, Me
mc.setTarget(mh);
mc.setDefaultTarget(mh);
mc.setFallbackTarget(makeFallBack(mc, sender, name, callID, type, safe, thisCall, spreadCall));
+
+ // Register for cache invalidation on metaclass changes
+ registerCallSite(mc);
return mc;
}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
index 14704cfb65c..f8865032a45 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -33,6 +33,7 @@
import groovy.lang.MissingMethodException;
import groovy.transform.Internal;
import org.apache.groovy.runtime.ObjectUtil;
+import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.reflection.CachedField;
import org.codehaus.groovy.reflection.CachedMethod;
@@ -124,6 +125,21 @@ public abstract class Selector {
*/
private static final CallType[] CALL_TYPE_VALUES = CallType.values();
+ /**
+ * Whether to use a global SwitchPoint guard for metaclass changes.
+ *
+ * Default is {@code false} to avoid the performance cost of globally
+ * invalidating all indy call sites whenever any metaclass changes.
+ * Enabling this will significantly degrade performance in frameworks or applications
+ * with frequent metaclass changes (for example, Grails), because every such change
+ * forces all guarded call sites to be re-linked.
+ *
+ * Set {@code groovy.indy.switchpoint.guard=true} only for specific
+ * debugging or backward-compatibility scenarios where strict metaclass change
+ * detection is required, and not for general production use.
+ */
+ private static final boolean INDY_SWITCHPOINT_GUARD = SystemUtil.getBooleanSafe("groovy.indy.switchpoint.guard");
+
/**
* Returns the Selector
*/
@@ -940,9 +956,11 @@ public void setGuards(Object receiver) {
}
}
- // handle constant metaclass and category changes
- handle = switchPoint.guardWithTest(handle, fallback);
- if (LOG_ENABLED) LOG.info("added switch point guard");
+ // Apply global switchpoint guard if enabled (disabled by default for performance)
+ if (INDY_SWITCHPOINT_GUARD) {
+ handle = switchPoint.guardWithTest(handle, fallback);
+ if (LOG_ENABLED) LOG.info("added switch point guard");
+ }
java.util.function.Predicate