From 0c266ff94cc5ad80a918976ba85a348c33c1f69d Mon Sep 17 00:00:00 2001 From: James Fredley Date: Wed, 4 Feb 2026 15:20:45 -0500 Subject: [PATCH 1/2] Fix JarJarTask annotations for Windows compatibility Change @InputFiles @Classpath to @Input on untouchedFiles field. This field contains glob patterns (strings), not actual files, so @InputFiles and @Classpath were inappropriate and caused Gradle to treat patterns containing '*' as literal file paths on Windows. --- .../src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy b/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy index c49ef67a0e0..cc24c8873f5 100644 --- a/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy +++ b/build-logic/src/main/groovy/org/apache/groovy/gradle/JarJarTask.groovy @@ -60,8 +60,7 @@ class JarJarTask extends DefaultTask { final protected String projectName = project.name - @InputFiles - @Classpath + @Input @Optional List untouchedFiles = [] From ddf0121c388a4ba79382cac3db744e8abc6ee8b8 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Wed, 4 Feb 2026 15:21:00 -0500 Subject: [PATCH 2/2] GROOVY-10307: Improve invokedynamic performance with optimized caching Reduce the performance impact of metaclass changes on invokedynamic call sites: - Disable global SwitchPoint guard by default (controlled via groovy.indy.switchpoint.guard) - Track all call sites via WeakReference set for targeted invalidation - Add clearCache() method to CacheableCallSite for cache invalidation on metaclass change - When metaclass changes, clear caches and reset targets on all registered call sites This provides ~19% improvement on metaclass invalidation stress tests compared to baseline Groovy 6.x, and ~24% improvement compared to Groovy 4.0.30. --- .../groovy/vmplugin/v8/CacheableCallSite.java | 12 +++++ .../groovy/vmplugin/v8/IndyInterface.java | 45 ++++++++++++++++++- .../codehaus/groovy/vmplugin/v8/Selector.java | 21 +++++++-- 3 files changed, 73 insertions(+), 5 deletions(-) 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 73527d52272..31651885347 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -123,6 +123,18 @@ public void resetFallbackCount() { fallbackRound.incrementAndGet(); } + /** + * Clear the LRU cache and reset fallback count. + * Called when metaclass changes to ensure stale method handles are discarded. + */ + public void clearCache() { + synchronized (lruCache) { + lruCache.clear(); + } + latestHitMethodHandleWrapperSoftReference = null; + resetFallbackCount(); + } + public AtomicLong getFallbackRound() { return fallbackRound; } 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 ddb12975c61..9ebe2d6530e 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; @@ -51,6 +54,11 @@ public class IndyInterface { private static final long INDY_OPTIMIZE_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.optimize.threshold", 1_000L); private static final long INDY_FALLBACK_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.fallback.threshold", 1_000L); private static final long INDY_FALLBACK_CUTOFF = SystemUtil.getLongSafe("groovy.indy.fallback.cutoff", 100L); + /** + * Initial capacity for the call site registry used to track all active call sites + * for cache invalidation when metaclass changes occur. + */ + private static final int INDY_CALLSITE_INITIAL_CAPACITY = SystemUtil.getIntegerSafe("groovy.indy.callsite.initial.capacity", 1024); /** * flags for method and property calls @@ -181,17 +189,31 @@ public int getOrderNumber() { } 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. + */ + private static final Set> ALL_CALL_SITES = ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY); static { GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints()); } + + /** + * Register a call site for cache invalidation when metaclass changes. + */ + 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) { @@ -199,6 +221,22 @@ protected static void invalidateSwitchPoints() { switchPoint = new SwitchPoint(); SwitchPoint.invalidateAll(new SwitchPoint[]{old}); } + + // Invalidate all call site caches and reset targets to default (cache lookup). + 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; + }); } /** @@ -247,6 +285,9 @@ private static CallSite realBootstrap(MethodHandles.Lookup caller, String name, 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 5c24c16487a..70cc0706496 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -31,6 +31,7 @@ import groovy.lang.MissingMethodException; import groovy.lang.ProxyMetaClass; import groovy.transform.Internal; +import org.apache.groovy.util.SystemUtil; import org.codehaus.groovy.GroovyBugError; import org.codehaus.groovy.reflection.CachedField; import org.codehaus.groovy.reflection.CachedMethod; @@ -122,6 +123,18 @@ public abstract class Selector { * Cache values for read-only access */ private static final CallType[] CALL_TYPE_VALUES = CallType.values(); + + /** + * Controls whether the global SwitchPoint guard is applied to method handles. + * When {@code false} (default), the SwitchPoint guard is NOT applied, which improves + * performance when metaclass changes occur by avoiding global invalidation of all call sites. + * Instead, call sites are invalidated individually via {@link IndyInterface#invalidateSwitchPoints()}. + *

+ * 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 @@ -959,9 +972,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> nonFinalOrNullUnsafe = (t) -> { return !Modifier.isFinal(t.getModifiers())