From 254c08cacfd4fe0ddfffc55416268e34614cc764 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Wed, 20 May 2026 02:10:02 +0200 Subject: [PATCH 01/19] adding a lock free MRU field to CacheableCallSite to avoid SoftReference overhead. Since the target holds a hard reference as well, this should not introduce any extra memory pressure --- .../groovy/vmplugin/v8/CacheableCallSite.java | 82 ++++++++++++++----- .../groovy/vmplugin/v8/IndyInterface.java | 68 +++++++++------ .../v8/IndyInterfaceCallSiteTargetTest.groovy | 8 +- 3 files changed, 108 insertions(+), 50 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 b6a9a4622c2..ff57d8b979e 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -46,13 +46,13 @@ public class CacheableCallSite extends MutableCallSite { private static final float LOAD_FACTOR = 0.75f; private static final int INITIAL_CAPACITY = (int) Math.ceil(CACHE_SIZE / LOAD_FACTOR) + 1; private final MethodHandles.Lookup lookup; - private volatile SoftReference latestHitMethodHandleWrapperSoftReference = null; + private volatile MRUEntry mruEntry; private final AtomicLong fallbackCount = new AtomicLong(); private final AtomicLong fallbackRound = new AtomicLong(); private MethodHandle defaultTarget; private MethodHandle fallbackTarget; - private final Map> lruCache = - new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { + private final Map> lruCache = + new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { @Serial private static final long serialVersionUID = 7785958879964294463L; /** @@ -78,54 +78,85 @@ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { this.lookup = lookup; } + /** + * Returns the cached method-handle wrapper for the receiver key if it is the most recently used. + * + * @param key the receiver cache key + * @return the cached wrapper, or {@code null} if not found or not MRU + */ + public MethodHandleWrapper get(Object key) { + MRUEntry entry = mruEntry; + if (entry != null && entry.key == key) { + return entry.wrapper; + } + return null; + } + /** * Returns a cached method-handle wrapper for the receiver class, computing and storing it if needed. * - * @param className the receiver cache key + * @param key the receiver cache key * @param valueProvider the provider used to compute a missing entry + * @param sender the caller class * @return the cached or newly created wrapper */ - public MethodHandleWrapper getAndPut(String className, MemoizeCache.ValueProvider valueProvider) { + public MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider valueProvider, Class sender) { MethodHandleWrapper result = null; SoftReference resultSoftReference; synchronized (lruCache) { - resultSoftReference = lruCache.get(className); + resultSoftReference = lruCache.get(key); if (null != resultSoftReference) { result = resultSoftReference.get(); if (null == result) removeAllStaleEntriesOfLruCache(); } if (null == result) { - result = valueProvider.provide(className); + result = valueProvider.provide(key); resultSoftReference = new SoftReference<>(result); - lruCache.put(className, resultSoftReference); + lruCache.put(key, resultSoftReference); } } - final SoftReference mhwsr = latestHitMethodHandleWrapperSoftReference; - final MethodHandleWrapper methodHandleWrapper = null == mhwsr ? null : mhwsr.get(); - - if (methodHandleWrapper == result) { - result.incrementLatestHitCount(); - } else { - result.resetLatestHitCount(); - if (null != methodHandleWrapper) methodHandleWrapper.resetLatestHitCount(); - latestHitMethodHandleWrapperSoftReference = resultSoftReference; - } + + updateMRU(key, result, sender); return result; } + private void updateMRU(Object key, MethodHandleWrapper result, Class sender) { + if (result == null || result == MethodHandleWrapper.getNullMethodHandleWrapper()) return; + + // Leak-Awareness: only store strongly if the target loader is safe + var method = result.getMethod(); + if (method != null) { + Class declaringClass = method.getDeclaringClass().getTheClass(); + if (isSafeLoader(sender.getClassLoader(), declaringClass.getClassLoader())) { + mruEntry = new MRUEntry(key, result); + } + } + } + + private static boolean isSafeLoader(ClassLoader callerLoader, ClassLoader targetLoader) { + if (targetLoader == null) return true; // Bootstrap is always safe + if (callerLoader == targetLoader) return true; + ClassLoader cl = callerLoader; + while (cl != null) { + if (cl == targetLoader) return true; + cl = cl.getParent(); + } + return false; + } + /** * Stores a method-handle wrapper under the supplied cache key. * - * @param name the receiver cache key + * @param key the receiver cache key * @param mhw the wrapper to cache * @return the previously cached wrapper, or {@code null} if none existed */ - public MethodHandleWrapper put(String name, MethodHandleWrapper mhw) { + public MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { synchronized (lruCache) { final SoftReference methodHandleWrapperSoftReference; - methodHandleWrapperSoftReference = lruCache.put(name, new SoftReference<>(mhw)); + methodHandleWrapperSoftReference = lruCache.put(key, new SoftReference<>(mhw)); if (null == methodHandleWrapperSoftReference) return null; final MethodHandleWrapper methodHandleWrapper = methodHandleWrapperSoftReference.get(); if (null == methodHandleWrapper) removeAllStaleEntriesOfLruCache(); @@ -133,6 +164,15 @@ public MethodHandleWrapper put(String name, MethodHandleWrapper mhw) { } } + private static final class MRUEntry { + final Object key; + final MethodHandleWrapper wrapper; + MRUEntry(Object key, MethodHandleWrapper wrapper) { + this.key = key; + this.wrapper = wrapper; + } + } + private void removeAllStaleEntriesOfLruCache() { CACHE_CLEANER_QUEUE.offer(() -> { synchronized (lruCache) { 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 c0889923370..91ef98d6805 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -56,7 +56,6 @@ public class IndyInterface { public static final int SAFE_NAVIGATION=1, THIS_CALL=2, GROOVY_OBJECT=4, IMPLICIT_THIS=8, SPREAD_CALL=16, UNCACHED_CALL=32; private static final MethodHandleWrapper NULL_METHOD_HANDLE_WRAPPER = MethodHandleWrapper.getNullMethodHandleWrapper(); - private static final String NULL_OBJECT_CLASS_NAME = "org.codehaus.groovy.runtime.NullObject"; /** * Enum for easy differentiation between call types. @@ -352,18 +351,38 @@ public static Object fromCache(CacheableCallSite callSite, Class sender, Stri return mh.invokeExact(arguments); } + private static final Object NULL_KEY = new Object(); + private static final ClassValue STATIC_KEYS = new ClassValue() { + @Override + protected Object computeValue(Class type) { + return new Object(); + } + }; + /** * Get the cached methodHandle. if the related methodHandle is not found in the inline cache, cache and return it. */ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { - FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); Object receiver = arguments[0]; - String receiverClassName = receiverCacheKey(receiver); - MethodHandleWrapper mhw = callSite.getAndPut(receiverClassName, (theName) -> { + Object receiverKey = receiverCacheKey(receiver); + + MethodHandleWrapper mhw = callSite.get(receiverKey); + if (mhw != null) { + mhw.incrementLatestHitCount(); + if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) { + if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { + optimizeCallSite(callSite, mhw); + } + } + return mhw.getCachedMethodHandle(); + } + + FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + mhw = callSite.getAndPut(receiverKey, (theKey) -> { MethodHandleWrapper fallback = fallbackSupplier.get(); if (fallback.isCanSetTarget()) return fallback; return NULL_METHOD_HANDLE_WRAPPER; - }); + }, sender); if (mhw == NULL_METHOD_HANDLE_WRAPPER) { // The PIC stores a sentinel to remember "do not relink this receiver shape"; @@ -381,25 +400,27 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class } if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { - if (callSite.getTarget() != callSite.getDefaultTarget()) { - // reset the call site target to default forever to avoid JIT deoptimization storm further - callSite.setTarget(callSite.getDefaultTarget()); - } - } else { - if (callSite.getTarget() != mhw.getTargetMethodHandle()) { - callSite.setTarget(mhw.getTargetMethodHandle()); - if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); - } - } - - mhw.resetLatestHitCount(); + optimizeCallSite(callSite, mhw); } } return mhw.getCachedMethodHandle(); } + private static void optimizeCallSite(CacheableCallSite callSite, MethodHandleWrapper mhw) { + if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { + if (callSite.getTarget() != callSite.getDefaultTarget()) { + callSite.setTarget(callSite.getDefaultTarget()); + } + } else { + if (callSite.getTarget() != mhw.getTargetMethodHandle()) { + callSite.setTarget(mhw.getTargetMethodHandle()); + if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); + } + } + mhw.resetLatestHitCount(); + } + /** * Core method for indy method selection using runtime types. * @deprecated Use the new bootHandle-based approach instead. @@ -438,14 +459,11 @@ private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class /** * Computes the PIC cache key for the given receiver. - * Different {@code Class} objects (e.g. {@code A} vs {@code B}) share the same runtime class - * ({@code java.lang.Class}) but dispatch to different methods. Including the represented class - * name avoids PIC cache collisions for static-method call sites. */ - private static String receiverCacheKey(Object receiver) { - if (receiver == null) return NULL_OBJECT_CLASS_NAME; - if (receiver instanceof Class c) return "java.lang.Class:" + c.getName(); - return receiver.getClass().getName(); + static Object receiverCacheKey(Object receiver) { + if (receiver == null) return NULL_KEY; + if (receiver instanceof Class c) return STATIC_KEYS.get(c); + return receiver.getClass(); } private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) { diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index 2a499a222aa..9f9226dee24 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -411,11 +411,11 @@ final class IndyInterfaceCallSiteTargetTest { } private static void cacheWrapper(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper) { - callSite.put(receiverClassName(receiver), wrapper) + callSite.put(IndyInterface.receiverCacheKey(receiver), wrapper) } private static void primeLatestHitCount(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper, long value) { - assertSame(wrapper, callSite.getAndPut(receiverClassName(receiver), { wrapper })) + assertSame(wrapper, callSite.getAndPut(IndyInterface.receiverCacheKey(receiver), { wrapper }, IndyInterfaceCallSiteTargetTest)) latestHitCountField().get(wrapper).set(value) } @@ -449,10 +449,10 @@ final class IndyInterfaceCallSiteTargetTest { private static MethodHandleWrapper requireCachedWrapper(CacheableCallSite callSite, Object receiver) { AtomicInteger providerCalls = new AtomicInteger() - MethodHandleWrapper wrapper = callSite.getAndPut(receiverClassName(receiver), { key -> + MethodHandleWrapper wrapper = callSite.getAndPut(IndyInterface.receiverCacheKey(receiver), { key -> providerCalls.incrementAndGet() MethodHandleWrapper.getNullMethodHandleWrapper() - }) + }, IndyInterfaceCallSiteTargetTest) assertEquals(0, providerCalls.get()) wrapper } From 1acf9b981592cced38ea4941bc760ff712034a1c Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Wed, 20 May 2026 10:18:08 +0200 Subject: [PATCH 02/19] GROOVY-12023: add a PIC with configurable size for indy in fron of the existing cache --- .../groovy/vmplugin/v8/CacheableCallSite.java | 34 +++++++++++++++- .../groovy/vmplugin/v8/IndyInterface.java | 28 ++++++++++--- .../codehaus/groovy/vmplugin/v8/Selector.java | 40 ++++++++++++------- 3 files changed, 81 insertions(+), 21 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 ff57d8b979e..87ba5573f10 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -51,6 +51,9 @@ public class CacheableCallSite extends MutableCallSite { private final AtomicLong fallbackRound = new AtomicLong(); private MethodHandle defaultTarget; private MethodHandle fallbackTarget; + private volatile MethodHandle picChain; + private Object[] picKeys; + private int picCount; private final Map> lruCache = new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { @Serial private static final long serialVersionUID = 7785958879964294463L; @@ -164,6 +167,33 @@ public MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { } } + public void recordInPic(Object key, int maxPicSize) { + if (picKeys == null) picKeys = new Object[maxPicSize]; + if (picCount < picKeys.length) { + picKeys[picCount++] = key; + } + } + + public boolean picIncludes(Object key) { + if (picKeys == null) return false; + for (int i = 0; i < picCount; i++) { + if (picKeys[i] == key) return true; + } + return false; + } + + public MethodHandle getPicChain() { + return picChain; + } + + public void setPicChain(MethodHandle picChain) { + this.picChain = picChain; + } + + public int getPicCount() { + return picCount; + } + private static final class MRUEntry { final Object key; final MethodHandleWrapper wrapper; @@ -195,7 +225,9 @@ public long incrementFallbackCount() { */ public void resetFallbackCount() { fallbackCount.set(0); - fallbackRound.incrementAndGet(); + picCount = 0; + picChain = null; + picKeys = null; } /** 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 91ef98d6805..6943533faa3 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -49,6 +49,7 @@ 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); + private static final int INDY_PIC_SIZE = SystemUtil.getIntegerSafe("groovy.indy.pic.size", 4); /** * Flags for method and property calls. @@ -371,7 +372,7 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class mhw.incrementLatestHitCount(); if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) { if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - optimizeCallSite(callSite, mhw); + optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, mhw); } } return mhw.getCachedMethodHandle(); @@ -400,22 +401,37 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class } if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - optimizeCallSite(callSite, mhw); + optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, mhw); } } return mhw.getCachedMethodHandle(); } - private static void optimizeCallSite(CacheableCallSite callSite, MethodHandleWrapper mhw) { + private static void optimizeCallSite(CacheableCallSite callSite, Class sender, String methodName, int callID, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments, MethodHandleWrapper mhw) { if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { if (callSite.getTarget() != callSite.getDefaultTarget()) { callSite.setTarget(callSite.getDefaultTarget()); } } else { - if (callSite.getTarget() != mhw.getTargetMethodHandle()) { - callSite.setTarget(mhw.getTargetMethodHandle()); - if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); + Object receiverKey = receiverCacheKey(arguments[0]); + if (!callSite.picIncludes(receiverKey) && callSite.getPicCount() < INDY_PIC_SIZE) { + Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); + selector.skipSwitchPoint = true; + MethodHandle picChain = callSite.getPicChain(); + if (picChain == null) picChain = callSite.getDefaultTarget(); + selector.fallback = picChain; + selector.setCallSiteTarget(); + + MethodHandle newChain = selector.handle; + callSite.setPicChain(newChain); + + // wrap with top-level SwitchPoint guard + MethodHandle target = switchPoint.guardWithTest(newChain, callSite.getDefaultTarget()); + callSite.setTarget(target); + callSite.recordInPic(receiverKey, INDY_PIC_SIZE); + + if (LOG_ENABLED) LOG.info("call site target updated with PIC link, pic size: " + callSite.getPicCount()); } } mhw.resetLatestHitCount(); 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 12ce177122f..44b9ee61c45 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -161,11 +161,30 @@ public abstract class Selector { * Controls whether Groovy runtime exceptions are unwrapped around the target. */ public boolean catchException = true; + /** + * Indicates whether the global SwitchPoint should be skipped for this selector. + */ + public boolean skipSwitchPoint = false; + /** + * Custom fallback method handle to use during re-linking or PIC building. + */ + public MethodHandle fallback; /** * Call-site category associated with this selector. */ public CallType callType; + public static MethodHandle maybeWrapWithExceptionHandler(MethodHandle handle, boolean catchException) { + if (handle == null || !catchException) return handle; + Class returnType = handle.type().returnType(); + if (returnType != Object.class) { + MethodType mtype = MethodType.methodType(returnType, GroovyRuntimeException.class); + return MethodHandles.catchException(handle, GroovyRuntimeException.class, UNWRAP_EXCEPTION.asType(mtype)); + } else { + return MethodHandles.catchException(handle, GroovyRuntimeException.class, UNWRAP_EXCEPTION); + } + } + /** * Cache values for read-only access */ @@ -1033,17 +1052,8 @@ public void correctSpreading() { * Adds the standard exception handler. */ public void addExceptionHandler() { - //TODO: if we would know exactly which paths require the exceptions - // and which paths not, we can sometimes save this guard - if (handle == null || !catchException) return; - Class returnType = handle.type().returnType(); - if (returnType != Object.class) { - MethodType mtype = MethodType.methodType(returnType, GroovyRuntimeException.class); - handle = MethodHandles.catchException(handle, GroovyRuntimeException.class, UNWRAP_EXCEPTION.asType(mtype)); - } else { - handle = MethodHandles.catchException(handle, GroovyRuntimeException.class, UNWRAP_EXCEPTION); - } - if (LOG_ENABLED) LOG.info("added GroovyRuntimeException unwrapper"); + handle = maybeWrapWithExceptionHandler(handle, catchException); + if (handle != null && LOG_ENABLED) LOG.info("added GroovyRuntimeException unwrapper"); } /** @@ -1052,7 +1062,7 @@ public void addExceptionHandler() { public void setGuards(Object receiver) { if (!cache || handle == null) return; - MethodHandle fallback = callSite.getFallbackTarget(); + MethodHandle fallback = this.fallback != null ? this.fallback : callSite.getFallbackTarget(); // special guards for receiver if (receiver instanceof GroovyObject go) { @@ -1087,8 +1097,10 @@ 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"); + if (!skipSwitchPoint) { + 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()) From 539e4a47a201ac7e7986f01fe05985edb9d092d9 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Wed, 20 May 2026 10:41:44 +0200 Subject: [PATCH 03/19] GROOVY-12023: add tests to ensure PIC functionality and fix one existing test since the fallback logic changed --- .../v8/IndyClassLoaderLeakTest.groovy | 63 +++++++++++++++ .../v8/IndyInterfaceCallSiteTargetTest.groovy | 78 ++++++------------- .../vmplugin/v8/IndyInterfacePicTest.groovy | 72 +++++++++++++++++ 3 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy create mode 100644 src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy new file mode 100644 index 00000000000..35c81eff3dc --- /dev/null +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy @@ -0,0 +1,63 @@ +package org.codehaus.groovy.vmplugin.v8 + +import org.junit.jupiter.api.Test +import java.lang.invoke.MethodType +import static org.junit.jupiter.api.Assertions.* + +final class IndyClassLoaderLeakTest { + + @Test + void testMruLeakAwareness() { + MethodType type = MethodType.methodType(Object, Object) + CacheableCallSite callSite = new CacheableCallSite(type, java.lang.invoke.MethodHandles.lookup()) + + // 1. Same ClassLoader (Safe) + def sameLoaderObj = new Object() + Object key1 = IndyInterface.receiverCacheKey(sameLoaderObj) + // Simulate a successful lookup that calls updateMRU + updateMRU(callSite, key1, sameLoaderObj.class, this.class) + assertNotNull(callSite.mruEntry, "MRU should be populated for same loader") + + // 2. Different (Child) ClassLoader (Unsafe) + def gcl = new GroovyClassLoader() + def childClass = gcl.parseClass("class Child { def foo() {} }") + def childObj = childClass.newInstance() + + // Reset MRU + callSite.mruEntry = null + + Object key2 = IndyInterface.receiverCacheKey(childObj) + updateMRU(callSite, key2, childObj.class, this.class) + assertNull(callSite.mruEntry, "MRU should NOT be populated for child loader to avoid leaks") + } + + @Test + void testIdentityKeyIsolation() { + def gcl1 = new GroovyClassLoader() + def gcl2 = new GroovyClassLoader() + + String script = "class Target {}" + Class class1 = gcl1.parseClass(script) + Class class2 = gcl2.parseClass(script) + + assertNotSame(class1, class2) + assertEquals(class1.name, class2.name) + + Object key1 = IndyInterface.receiverCacheKey(class1) + Object key2 = IndyInterface.receiverCacheKey(class2) + + assertNotSame(key1, key2, "Classes from different loaders must have distinct cache keys") + } + + private static void updateMRU(CacheableCallSite callSite, Object key, Class targetClass, Class sender) { + // We use a dummy wrapper for testing + def wrapper = new MethodHandleWrapper( + java.lang.invoke.MethodHandles.constant(Object, "test"), + java.lang.invoke.MethodHandles.constant(Object, "test"), + new org.codehaus.groovy.reflection.CachedMethod(targetClass.getDeclaredMethods().length > 0 ? targetClass.getDeclaredMethods()[0] : Object.class.getMethod("toString")), + true + ) + callSite.updateMRU(key, wrapper, sender) + } + +} diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index 9f9226dee24..b9a760cd636 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -25,8 +25,6 @@ import org.junit.jupiter.api.Test import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType -import java.lang.reflect.Field -import java.lang.reflect.Method import java.lang.reflect.Modifier import java.util.concurrent.atomic.AtomicInteger @@ -96,7 +94,7 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[] - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho', @@ -124,16 +122,16 @@ final class IndyInterfaceCallSiteTargetTest { ) cacheWrapper(callSite, receiver, wrapper) - primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD')) + primeLatestHitCount(callSite, receiver, wrapper, IndyInterface.INDY_OPTIMIZE_THRESHOLD) - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'foo', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args ) assertSame(wrapper.cachedMethodHandle, methodHandle) - assertSame(wrapper.targetMethodHandle, callSite.target) + assertNotSame(callSite.defaultTarget, callSite.target) assertEquals(0L, wrapper.latestHitCount) } @@ -153,7 +151,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, receiver, wrapper) - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'foo', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args @@ -169,7 +167,7 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] args = [null] as Object[] - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'foo', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -191,7 +189,7 @@ final class IndyInterfaceCallSiteTargetTest { registry.setMetaClass((Object) PerInstanceMetaClassStaticTarget, emc) try { 2.times { - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, PerInstanceMetaClassStaticTarget, 'ping', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -219,7 +217,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, receiver, wrapper) callSite.target = wrapper.targetMethodHandle - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'foo', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args @@ -241,7 +239,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, ClassA, wrapper) - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, ClassA, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -257,7 +255,7 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] argsA = [ClassA] as Object[] - MethodHandle handleA = invokeFromCacheHandle( + MethodHandle handleA = IndyInterface.fromCacheHandle( callSite, ClassA, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA @@ -265,7 +263,7 @@ final class IndyInterfaceCallSiteTargetTest { assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[])) Object[] argsB = [ClassB] as Object[] - MethodHandle handleB = invokeFromCacheHandle( + MethodHandle handleB = IndyInterface.fromCacheHandle( callSite, ClassB, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB @@ -285,7 +283,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, ClassA, wrapper) Object[] args = [ClassA] as Object[] - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, ClassA, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -304,7 +302,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, ClassA, wrapper) Object[] args = [ClassA] as Object[] - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, ClassA, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -321,7 +319,7 @@ final class IndyInterfaceCallSiteTargetTest { def receiver = new InstanceStaticCallTarget() Object[] args = [receiver, 'abc'] as Object[] - MethodHandle selectedHandle = invokeSelectMethodHandle( + MethodHandle selectedHandle = IndyInterface.selectMethodHandle( callSite, InstanceStaticCallTarget, 'valueOf', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -332,7 +330,7 @@ final class IndyInterfaceCallSiteTargetTest { assertTrue(Modifier.isStatic(cachedWrapper.method.modifiers)) assertSame(callSite.defaultTarget, callSite.target) - MethodHandle cachedHandle = invokeFromCacheHandle( + MethodHandle cachedHandle = IndyInterface.fromCacheHandle( callSite, InstanceStaticCallTarget, 'valueOf', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args @@ -349,7 +347,7 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] argsA = [ClassA] as Object[] - MethodHandle handleA = invokeSelectMethodHandle( + MethodHandle handleA = IndyInterface.selectMethodHandle( callSite, ClassA, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA @@ -357,7 +355,7 @@ final class IndyInterfaceCallSiteTargetTest { assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[])) Object[] argsB = [ClassB] as Object[] - MethodHandle handleB = invokeSelectMethodHandle( + MethodHandle handleB = IndyInterface.selectMethodHandle( callSite, ClassB, 'bar', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB @@ -378,7 +376,7 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[] - MethodHandle methodHandle = invokeSelectMethodHandle( + MethodHandle methodHandle = IndyInterface.selectMethodHandle( callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, 1, args @@ -416,7 +414,7 @@ final class IndyInterfaceCallSiteTargetTest { private static void primeLatestHitCount(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper, long value) { assertSame(wrapper, callSite.getAndPut(IndyInterface.receiverCacheKey(receiver), { wrapper }, IndyInterfaceCallSiteTargetTest)) - latestHitCountField().get(wrapper).set(value) + wrapper.@latestHitCount.set(value) } private static void assertFallbackCutoffLeavesDefaultTarget(boolean startAwayFromDefaultTarget) { @@ -430,13 +428,13 @@ final class IndyInterfaceCallSiteTargetTest { ) cacheWrapper(callSite, receiver, wrapper) - primeLatestHitCount(callSite, receiver, wrapper, readIndyLong('INDY_OPTIMIZE_THRESHOLD')) - callSite.fallbackRound.set(readIndyLong('INDY_FALLBACK_CUTOFF') + 1L) + primeLatestHitCount(callSite, receiver, wrapper, IndyInterface.INDY_OPTIMIZE_THRESHOLD) + callSite.fallbackRound.set(IndyInterface.INDY_FALLBACK_CUTOFF + 1L) if (startAwayFromDefaultTarget) { callSite.target = targetHandle(type, 'non-default-target') } - MethodHandle methodHandle = invokeFromCacheHandle( + MethodHandle methodHandle = IndyInterface.fromCacheHandle( callSite, IndyInterfaceCallSiteTargetTest, 'foo', IndyInterface.CallType.METHOD.getOrderNumber(), Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args @@ -463,37 +461,5 @@ final class IndyInterfaceCallSiteTargetTest { return receiver.getClass().name } - private static long readIndyLong(String fieldName) { - Field field = IndyInterface.getDeclaredField(fieldName) - field.accessible = true - field.getLong(null) - } - - private static Field latestHitCountField() { - Field field = MethodHandleWrapper.getDeclaredField('latestHitCount') - field.accessible = true - field - } - - private static MethodHandle invokeFromCacheHandle(CacheableCallSite callSite, Class sender, String methodName, - int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) { - Method method = IndyInterface.getDeclaredMethod('fromCacheHandle', - CacheableCallSite, Class, String, Integer.TYPE, Boolean, Boolean, Boolean, Object, Object[] - ) - method.accessible = true - return (MethodHandle) method.invoke(null, - callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments - ) - } - private static MethodHandle invokeSelectMethodHandle(CacheableCallSite callSite, Class sender, String methodName, - int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) { - Method method = IndyInterface.getDeclaredMethod('selectMethodHandle', - CacheableCallSite, Class, String, Integer.TYPE, Boolean, Boolean, Boolean, Object, Object[] - ) - method.accessible = true - return (MethodHandle) method.invoke(null, - callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments - ) - } } diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy new file mode 100644 index 00000000000..1f6715259d1 --- /dev/null +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy @@ -0,0 +1,72 @@ +package org.codehaus.groovy.vmplugin.v8 + +import org.junit.jupiter.api.Test +import java.lang.invoke.MethodType +import static org.junit.jupiter.api.Assertions.* + +final class IndyInterfacePicTest { + + static class Receiver1 { String foo() { "r1" } } + static class Receiver2 { String foo() { "r2" } } + static class Receiver3 { String foo() { "r3" } } + static class Receiver4 { String foo() { "r4" } } + static class Receiver5 { String foo() { "r5" } } + + @Test + void testPicChainGrowthAndLimit() { + MethodType type = MethodType.methodType(Object, Object) + // Use bootstrap for proper initialization + CacheableCallSite callSite = (CacheableCallSite) IndyInterface.bootstrap( + java.lang.invoke.MethodHandles.lookup(), "invoke", type, "foo", 0) + + // Initial state + assertEquals(0, callSite.getPicCount()) + assertNull(callSite.getPicChain()) + + def receivers = [new Receiver1(), new Receiver2(), new Receiver3(), new Receiver4(), new Receiver5()] + int picLimit = IndyInterface.INDY_PIC_SIZE + int optimizeThreshold = (int) IndyInterface.INDY_OPTIMIZE_THRESHOLD + + receivers.eachWithIndex { receiver, i -> + Object[] args = [receiver] as Object[] + + // Trigger method selection and optimization + // We need to exceed INDY_OPTIMIZE_THRESHOLD + (optimizeThreshold + 10).times { + callSite.getTarget().invokeWithArguments(args) + } + + if (i < picLimit) { + assertEquals(i + 1, callSite.getPicCount(), "PIC should grow for receiver ${i+1}") + assertNotNull(callSite.getPicChain()) + } else { + assertEquals(picLimit, callSite.getPicCount(), "PIC should stop growing at limit") + } + } + } + + @Test + void testPicResetOnMetaClassChange() { + MethodType type = MethodType.methodType(Object, Object) + CacheableCallSite callSite = (CacheableCallSite) IndyInterface.bootstrap( + java.lang.invoke.MethodHandles.lookup(), "invoke", type, "foo", 0) + + def receiver = new Receiver1() + Object[] args = [receiver] as Object[] + + int optimizeThreshold = (int) IndyInterface.INDY_OPTIMIZE_THRESHOLD + + // Fill PIC + (optimizeThreshold + 10).times { + callSite.getTarget().invokeWithArguments(args) + } + assertTrue(callSite.getPicCount() > 0) + + // Trigger global invalidation (reset) + callSite.resetFallbackCount() + + assertEquals(0, callSite.getPicCount()) + assertNull(callSite.getPicChain()) + } + +} From b48a20670be45316886edd564428e3f33e9ee9a9 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Wed, 20 May 2026 11:05:07 +0200 Subject: [PATCH 04/19] GROOVY-12023: adding documentation created by AI --- ARCHITECTURE.md | 20 +++++++++++++++ .../groovy/vmplugin/v8/CacheableCallSite.java | 25 ++++++++++++++++++- .../groovy/vmplugin/v8/IndyInterface.java | 19 +++++++++++--- .../groovy/vmplugin/v8/package-info.java | 10 +++++++- 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a6534b926af..4fea0153980 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -90,6 +90,26 @@ The phase enum is the right anchor for any documentation that talks about "when X happens during compilation". Quoting the phase names verbatim keeps the reference precise; paraphrasing tends to drift. +## Runtime: invokedynamic (Indy) + +Since Groovy 2.0, dynamic method dispatch can be performed using the `invokedynamic` +instruction. The core of this implementation lives in `org.codehaus.groovy.vmplugin.v8`. + +| Class | Role | +|---|---| +| `IndyInterface` | Bootstrap methods and optimization lifecycle management. | +| `CacheableCallSite` | The stateful call site holding the PIC chain, MRU entry, and LRU cache. | +| `Selector` | Logic for finding the target method/property and constructing the guarded `MethodHandle`. | +| `MethodHandleWrapper` | Combines a `MethodHandle` with metadata like hit counts and target description. | + +### Caching Hierarchy +To maximize performance, `CacheableCallSite` uses three levels of caching: +1. **PIC Chain (Level 1)**: A bounded chain of guarded handles in the call-site target (JIT-optimized). +2. **MRU Entry (Level 2)**: A lock-free volatile field for the most recent hit shape. +3. **LRU Cache (Level 3)**: A synchronized, soft-referenced map for megamorphic fallback. + +Detailed technical documentation of this hierarchy can be found in the Javadoc of `CacheableCallSite`. + ### Parser (phase 2) - Grammar lives in `src/antlr/GroovyLexer.g4` and 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 87ba5573f10..bd8309b5c17 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -37,7 +37,30 @@ import java.util.logging.Logger; /** - * Represents a cacheable call site, which can reduce the cost of resolving methods + * Represents a cacheable call site, which manages a multi-level caching hierarchy for dynamic method dispatch. + *

+ * To minimize the overhead of dynamic method selection and invocation, this class maintains three levels of caching: + *

    + *
  1. Level 1: Polymorphic Inline Cache (PIC) Chain: + * A site-local, bounded chain of guarded method handles (default size 4) stored directly in the {@link #getTarget() target}. + * This is the fastest path, allowing the JVM's JIT compiler to inline calls for the hottest receiver shapes. + * It is managed via {@link #getPicChain()} and updated by {@code IndyInterface.optimizeCallSite}. + *
  2. + *
  3. Level 2: Most Recently Used (MRU) Entry: + * A {@code volatile} field {@link #mruEntry} that stores a single {@link MethodHandleWrapper} for the most recently successful hit. + * Accessed via {@link #get(Object)}, it provides a lock-free path for monomorphic or low-polymorphic call sites + * that fall through the PIC chain. It uses identity-based keys to avoid allocations. + *
  4. + *
  5. Level 3: Least Recently Used (LRU) Cache: + * A synchronized {@link LinkedHashMap} {@link #lruCache} (default size 8) that stores {@link SoftReference}s to + * {@link MethodHandleWrapper}s. This serves as the megamorphic fallback, preventing full re-selection + * for shapes that have been seen before but are not currently in the PIC or MRU. + *
  6. + *
+ *

+ * Leak-Awareness: To prevent permanent ClassLoader leaks, Level 2 (MRU) uses strong references only when + * the target class belongs to a safe ClassLoader (same or parent). Level 3 (LRU) always uses {@link SoftReference}s + * to allow the JVM to reclaim Metaspace under memory pressure. * * @since 3.0.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 6943533faa3..bb952b10360 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -40,10 +40,21 @@ /** * Bytecode level interface for bootstrap methods used by invokedynamic. - * This class provides a logging ability by using the boolean system property - * groovy.indy.logging. Other than that this class contains the - * interfacing methods with bytecode for invokedynamic as well as some helper - * methods and classes. + *

+ * This class provides the core logic for the {@code invokedynamic} (Indy) support in Groovy. + * It handles the bootstrap process, method selection via {@link Selector}, and the + * optimization lifecycle of {@link CacheableCallSite}. + *

+ * Optimization Lifecycle: + *

    + *
  1. Bootstrap: The JVM calls one of the bootstrap methods (e.g., {@code bootstrap}) when an {@code invokedynamic} instruction is first encountered.
  2. + *
  3. Initial Linkage: The call site is initialized with a fallback target (adapter pointing to {@link #fromCacheHandle}).
  4. + *
  5. Execution & Selection: On first execution, {@code fromCacheHandle} uses a {@link Selector} to find the target method and create a guarded {@link java.lang.invoke.MethodHandle}.
  6. + *
  7. Promotion & PIC: After reaching {@link #INDY_OPTIMIZE_THRESHOLD} hits for a stable shape, {@link #optimizeCallSite} promotes the handle into a + * Polymorphic Inline Cache (PIC) chain directly in the call site target for maximum JIT optimization.
  8. + *
+ *

+ * Logging can be enabled using the system property {@code groovy.indy.logging=true}. */ public class IndyInterface { private static final long INDY_OPTIMIZE_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.optimize.threshold", 1_000L); diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/package-info.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/package-info.java index e2ade9541b4..5c78d5a1603 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/package-info.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/package-info.java @@ -18,6 +18,14 @@ */ /** - * Java 8 VM plugin. Compatibility layer for Java 8 features (lambdas, streams). + * Java 8 VM plugin. Compatibility layer for Java 8 features and core implementation of {@code invokedynamic} (Indy) support. + *

+ * This package contains the runtime infrastructure for Groovy's dynamic method dispatch using the {@code invokedynamic} + * instruction. Key components include: + *

    + *
  • {@link org.codehaus.groovy.vmplugin.v8.IndyInterface}: The entry point for bootstrap methods and call-site optimization logic.
  • + *
  • {@link org.codehaus.groovy.vmplugin.v8.CacheableCallSite}: Manages the multi-level caching hierarchy (PIC, MRU, LRU) to minimize dispatch overhead.
  • + *
  • {@link org.codehaus.groovy.vmplugin.v8.Selector}: Responsible for dynamic method selection, handle creation, and guard generation.
  • + *
*/ package org.codehaus.groovy.vmplugin.v8; From 7cbbf97ae48bceca7c0041f59fffa5fb3b798474 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Thu, 21 May 2026 06:30:57 +0200 Subject: [PATCH 05/19] GROOVY-12023: fix multithreading --- gradle/verification-metadata.xml | 613 ++++++++++++++++++ .../groovy/vmplugin/v8/CacheableCallSite.java | 110 +++- .../groovy/vmplugin/v8/IndyInterface.java | 40 +- .../v8/IndyInterfaceCallSiteTargetTest.groovy | 10 + 4 files changed, 744 insertions(+), 29 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 70aa232ea03..e7ec0175284 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -258,31 +258,37 @@ + + + + + + @@ -293,6 +299,7 @@ + @@ -303,6 +310,7 @@ + @@ -313,11 +321,13 @@ + + @@ -336,23 +346,27 @@ + + + + @@ -361,34 +375,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -399,6 +456,7 @@ + @@ -409,6 +467,7 @@ + @@ -419,6 +478,7 @@ + @@ -428,6 +488,12 @@ + + + + + + @@ -435,24 +501,29 @@ + + + + + @@ -463,6 +534,7 @@ + @@ -473,11 +545,13 @@ + + @@ -488,6 +562,7 @@ + @@ -498,24 +573,28 @@ + + + + @@ -526,6 +605,7 @@ + @@ -546,18 +626,21 @@ + + + @@ -568,12 +651,14 @@ + + @@ -586,6 +671,7 @@ + @@ -593,6 +679,7 @@ + @@ -613,16 +700,19 @@ + + + @@ -638,6 +728,7 @@ + @@ -653,11 +744,13 @@ + + @@ -668,6 +761,7 @@ + @@ -695,6 +789,7 @@ + @@ -705,6 +800,7 @@ + @@ -718,6 +814,12 @@ + + + + + + @@ -726,22 +828,26 @@ + + + + @@ -752,44 +858,52 @@ + + + + + + + + @@ -800,6 +914,7 @@ + @@ -810,38 +925,45 @@ + + + + + + + @@ -853,12 +975,14 @@ + + @@ -879,6 +1003,7 @@ + @@ -890,6 +1015,7 @@ + @@ -912,6 +1038,7 @@ + @@ -922,18 +1049,21 @@ + + + @@ -954,46 +1084,60 @@ + + + + + + + + + + + + + + @@ -1004,30 +1148,46 @@ + + + + + + + + + + + + + + + + @@ -1040,6 +1200,7 @@ + @@ -1052,12 +1213,14 @@ + + @@ -1070,6 +1233,7 @@ + @@ -1082,12 +1246,14 @@ + + @@ -1108,28 +1274,34 @@ + + + + + + @@ -1140,18 +1312,21 @@ + + + @@ -1168,38 +1343,45 @@ + + + + + + + @@ -1217,42 +1399,50 @@ + + + + + + + + @@ -1269,58 +1459,69 @@ + + + + + + + + + + + @@ -1331,46 +1532,55 @@ + + + + + + + + + @@ -1383,6 +1593,7 @@ + @@ -1393,11 +1604,13 @@ + + @@ -1406,29 +1619,40 @@ + + + + + + + + + + + @@ -1439,42 +1663,50 @@ + + + + + + + + @@ -1496,17 +1728,20 @@ + + + @@ -1525,6 +1760,12 @@ + + + + + + @@ -1540,53 +1781,69 @@ + + + + + + + + + + + + + + + + @@ -1602,16 +1859,19 @@ + + + @@ -1627,11 +1887,13 @@ + + @@ -1647,6 +1909,7 @@ + @@ -1662,11 +1925,13 @@ + + @@ -1682,11 +1947,13 @@ + + @@ -1702,21 +1969,25 @@ + + + + @@ -1732,11 +2003,13 @@ + + @@ -1752,11 +2025,13 @@ + + @@ -1772,6 +2047,7 @@ + @@ -1787,31 +2063,37 @@ + + + + + + @@ -1822,11 +2104,13 @@ + + @@ -1843,6 +2127,7 @@ + @@ -1851,11 +2136,23 @@ + + + + + + + + + + + + @@ -1863,6 +2160,7 @@ + @@ -1871,6 +2169,12 @@ + + + + + + @@ -1878,6 +2182,7 @@ + @@ -1886,6 +2191,12 @@ + + + + + + @@ -1893,6 +2204,7 @@ + @@ -1901,21 +2213,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1923,6 +2259,7 @@ + @@ -1931,6 +2268,12 @@ + + + + + + @@ -1940,11 +2283,13 @@ + + @@ -1965,6 +2310,7 @@ + @@ -1985,18 +2331,21 @@ + + + @@ -2116,10 +2465,13 @@ + + + @@ -2135,6 +2487,7 @@ + @@ -2152,22 +2505,26 @@ + + + + @@ -2178,27 +2535,32 @@ + + + + + @@ -2209,22 +2571,26 @@ + + + + @@ -2235,29 +2601,34 @@ + + + + + @@ -2275,32 +2646,38 @@ + + + + + + @@ -2311,47 +2688,56 @@ + + + + + + + + + @@ -2367,6 +2753,7 @@ + @@ -2382,6 +2769,7 @@ + @@ -2398,17 +2786,20 @@ + + + @@ -2419,38 +2810,45 @@ + + + + + + + @@ -2461,32 +2859,38 @@ + + + + + + @@ -2502,31 +2906,37 @@ + + + + + + @@ -2535,11 +2945,13 @@ + + @@ -2558,6 +2970,12 @@ + + + + + + @@ -2573,6 +2991,12 @@ + + + + + + @@ -2588,6 +3012,12 @@ + + + + + + @@ -2598,6 +3028,12 @@ + + + + + + @@ -2613,6 +3049,12 @@ + + + + + + @@ -2628,6 +3070,12 @@ + + + + + + @@ -2638,6 +3086,12 @@ + + + + + + @@ -2653,6 +3107,12 @@ + + + + + + @@ -2668,6 +3128,12 @@ + + + + + + @@ -2693,8 +3159,15 @@ + + + + + + + @@ -2757,6 +3230,7 @@ + @@ -2767,6 +3241,7 @@ + @@ -2777,12 +3252,14 @@ + + @@ -2798,11 +3275,13 @@ + + @@ -2813,11 +3292,13 @@ + + @@ -2828,11 +3309,13 @@ + + @@ -2843,11 +3326,13 @@ + + @@ -2858,11 +3343,13 @@ + + @@ -2873,11 +3360,13 @@ + + @@ -2888,11 +3377,13 @@ + + @@ -2920,23 +3411,27 @@ + + + + @@ -2955,91 +3450,109 @@ + + + + + + + + + + + + + + + + + + @@ -3050,16 +3563,25 @@ + + + + + + + + + @@ -3070,14 +3592,22 @@ + + + + + + + + @@ -3085,6 +3615,7 @@ + @@ -3095,6 +3626,7 @@ + @@ -3103,6 +3635,12 @@ + + + + + + @@ -3110,16 +3648,19 @@ + + + @@ -3128,6 +3669,12 @@ + + + + + + @@ -3135,16 +3682,19 @@ + + + @@ -3153,6 +3703,12 @@ + + + + + + @@ -3160,6 +3716,7 @@ + @@ -3170,6 +3727,7 @@ + @@ -3181,33 +3739,39 @@ + + + + + + @@ -3218,6 +3782,7 @@ + @@ -3236,6 +3801,7 @@ + @@ -3251,18 +3817,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -3281,6 +3871,7 @@ + @@ -3296,23 +3887,27 @@ + + + + @@ -3324,14 +3919,17 @@ + + + @@ -3344,6 +3942,7 @@ + @@ -3357,26 +3956,31 @@ + + + + + @@ -3412,21 +4016,25 @@ + + + + @@ -3442,12 +4050,14 @@ + + @@ -3459,17 +4069,20 @@ + + + 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 bd8309b5c17..57d726e34fd 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -18,23 +18,24 @@ */ package org.codehaus.groovy.vmplugin.v8; -import org.apache.groovy.util.SystemUtil; -import org.codehaus.groovy.runtime.DefaultGroovyMethods; -import org.codehaus.groovy.runtime.memoize.MemoizeCache; - import java.io.Serial; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.invoke.MutableCallSite; import java.lang.ref.SoftReference; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.groovy.util.SystemUtil; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.memoize.MemoizeCache; /** * Represents a cacheable call site, which manages a multi-level caching hierarchy for dynamic method dispatch. @@ -69,14 +70,39 @@ public class CacheableCallSite extends MutableCallSite { private static final float LOAD_FACTOR = 0.75f; private static final int INITIAL_CAPACITY = (int) Math.ceil(CACHE_SIZE / LOAD_FACTOR) + 1; private final MethodHandles.Lookup lookup; + /** + * Stores the most recently accessed entry. + *

+ * Concurrency: Marked as {@code volatile} to ensure thread-safe publication of the entry + * across different threads, allowing {@link #get(Object)} to remain lock-free. + */ private volatile MRUEntry mruEntry; private final AtomicLong fallbackCount = new AtomicLong(); private final AtomicLong fallbackRound = new AtomicLong(); private MethodHandle defaultTarget; private MethodHandle fallbackTarget; + /** + * The direct target of the call site before global guards are applied. + *

+ * Concurrency: {@code volatile} ensures that updates to the PIC chain are immediately + * visible to all threads during high-speed dispatch. + */ private volatile MethodHandle picChain; - private Object[] picKeys; - private int picCount; + + /** + * Keys corresponding to the handles in the {@link #picChain}. + *

+ * Concurrency: {@code final} for safe visibility during concurrent lookups. + */ + private final Object[] picKeys; + + /** + * The number of active entries in the PIC. + *

+ * Concurrency: {@code volatile} for safe visibility. Modifications are further + * protected by {@code synchronized} blocks to ensure atomicity. + */ + private volatile int picCount; private final Map> lruCache = new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { @Serial private static final long serialVersionUID = 7785958879964294463L; @@ -101,6 +127,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { */ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { super(type); + this.picKeys = new Object[IndyInterface.INDY_PIC_SIZE]; this.lookup = lookup; } @@ -120,6 +147,9 @@ public MethodHandleWrapper get(Object key) { /** * Returns a cached method-handle wrapper for the receiver class, computing and storing it if needed. + *

+ * Concurrency: Synchronizes on {@link #lruCache} to protect the non-thread-safe + * {@link LinkedHashMap} and to ensure that a missing entry is only computed once. * * @param key the receiver cache key * @param valueProvider the provider used to compute a missing entry @@ -174,6 +204,8 @@ private static boolean isSafeLoader(ClassLoader callerLoader, ClassLoader target /** * Stores a method-handle wrapper under the supplied cache key. + *

+ * Concurrency: Synchronizes on {@link #lruCache} for thread-safe access to the underlying map. * * @param key the receiver cache key * @param mhw the wrapper to cache @@ -190,15 +222,63 @@ public MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { } } - public void recordInPic(Object key, int maxPicSize) { - if (picKeys == null) picKeys = new Object[maxPicSize]; + /** + * Promotes a new receiver shape to the PIC if it is not already present and space is available. + *

+ * Concurrency: Synchronizes on {@code this} to atomically manage the + * promotion of receiver shapes into the PIC chain. This prevents multiple threads + * from corrupting the chain metadata or redundant JIT invalidations. + * + * @param key the receiver cache key + * @param updater the callback used to build the new PIC link + */ + public void maybeUpdatePic(Object key, UnaryOperator updater) { + synchronized (this) { + for (int i = 0; i < picCount; i++) { + if (picKeys[i] == key) return; + } + if (picCount < picKeys.length) { + MethodHandle currentChain = picChain != null ? picChain : defaultTarget; + MethodHandle newChain = updater.apply(currentChain); + if (newChain != null) { + picChain = newChain; + picKeys[picCount++] = key; + } + } + } + } + + /** + * Resets the PIC and targets to the default state if not already reset. + *

+ * Concurrency: Synchronizes on {@code this} to safely reset the + * target and PIC metadata when the site becomes too megamorphic. + */ + public void resetPicAndTarget() { + synchronized (this) { + if (getTarget() != defaultTarget) { + setTarget(defaultTarget); + resetFallbackCount(); + } + } + } + + public synchronized void recordInPic(Object key) { if (picCount < picKeys.length) { picKeys[picCount++] = key; } } - public boolean picIncludes(Object key) { - if (picKeys == null) return false; + /** + * Checks if a receiver shape is already present in the PIC. + *

+ * Concurrency: Marked as {@code synchronized} to prevent reading inconsistent state + * while the PIC is being updated. + * + * @param key the receiver cache key + * @return {@code true} if the key is in the PIC + */ + public synchronized boolean picIncludes(Object key) { for (int i = 0; i < picCount; i++) { if (picKeys[i] == key) return true; } @@ -213,7 +293,7 @@ public void setPicChain(MethodHandle picChain) { this.picChain = picChain; } - public int getPicCount() { + public synchronized int getPicCount() { return picCount; } @@ -245,12 +325,16 @@ public long incrementFallbackCount() { /** * Resets the fallback count and advances the fallback round marker. + *

+ * Concurrency: Marked as {@code synchronized} to atomically clear all PIC-related + * state, ensuring threads see a consistent "empty" state. */ - public void resetFallbackCount() { + public synchronized void resetFallbackCount() { fallbackCount.set(0); + fallbackRound.incrementAndGet(); picCount = 0; picChain = null; - picKeys = null; + Arrays.fill(picKeys, null); } /** 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 bb952b10360..982b088e803 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -60,7 +60,7 @@ 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); - private static final int INDY_PIC_SIZE = SystemUtil.getIntegerSafe("groovy.indy.pic.size", 4); + static final int INDY_PIC_SIZE = SystemUtil.getIntegerSafe("groovy.indy.pic.size", 4); /** * Flags for method and property calls. @@ -201,8 +201,11 @@ public int getOrderNumber() { /** * Shared switch point invalidated when metaclass state changes. + *

+ * Concurrency: {@code volatile} ensures that global invalidations are immediately + * visible to all threads across the JVM, causing them to fall back from JIT-optimized handles. */ - protected static SwitchPoint switchPoint = new SwitchPoint(); + protected static volatile SwitchPoint switchPoint = new SwitchPoint(); static { GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints()); @@ -210,6 +213,10 @@ public int getOrderNumber() { /** * Callback for constant metaclass update change + *

+ * Concurrency: Synchronizes on {@code IndyInterface.class} to atomically replace + * the global switch point and invalidate the old one, preventing race conditions during + * simultaneous MetaClass changes. */ protected static void invalidateSwitchPoints() { if (LOG_ENABLED) { @@ -426,24 +433,17 @@ private static void optimizeCallSite(CacheableCallSite callSite, Class sender } } else { Object receiverKey = receiverCacheKey(arguments[0]); - if (!callSite.picIncludes(receiverKey) && callSite.getPicCount() < INDY_PIC_SIZE) { + callSite.maybeUpdatePic(receiverKey, picChain -> { Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); selector.skipSwitchPoint = true; - MethodHandle picChain = callSite.getPicChain(); - if (picChain == null) picChain = callSite.getDefaultTarget(); selector.fallback = picChain; selector.setCallSiteTarget(); - - MethodHandle newChain = selector.handle; - callSite.setPicChain(newChain); - // wrap with top-level SwitchPoint guard - MethodHandle target = switchPoint.guardWithTest(newChain, callSite.getDefaultTarget()); + MethodHandle target = switchPoint.guardWithTest(selector.handle, callSite.getDefaultTarget()); callSite.setTarget(target); - callSite.recordInPic(receiverKey, INDY_PIC_SIZE); - if (LOG_ENABLED) LOG.info("call site target updated with PIC link, pic size: " + callSite.getPicCount()); - } + return selector.handle; + }); } mhw.resetLatestHitCount(); } @@ -467,9 +467,17 @@ private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class MethodHandle defaultTarget = callSite.getDefaultTarget(); long fallbackCount = callSite.incrementFallbackCount(); if ((fallbackCount > INDY_FALLBACK_THRESHOLD) && (callSite.getTarget() != defaultTarget)) { - callSite.setTarget(defaultTarget); - if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation"); - callSite.resetFallbackCount(); + /** + * Concurrency: Synchronizes on the {@code callSite} to safely reset the + * target and PIC metadata when the site becomes too megamorphic. + */ + synchronized (callSite) { + if (callSite.getTarget() != defaultTarget) { + callSite.setTarget(defaultTarget); + if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation"); + callSite.resetFallbackCount(); + } + } } if (callSite.getTarget() == defaultTarget) { diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index b9a760cd636..36559583080 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -141,6 +141,16 @@ final class IndyInterfaceCallSiteTargetTest { assertFallbackCutoffLeavesDefaultTarget(false) } + @Test + void testResetFallbackCountAdvancesRound() { + CacheableCallSite callSite = newCallSite(MethodType.methodType(Object, Object)) + assertEquals(0L, callSite.fallbackRound.get()) + callSite.resetFallbackCount() + assertEquals(1L, callSite.fallbackRound.get()) + callSite.resetFallbackCount() + assertEquals(2L, callSite.fallbackRound.get()) + } + @Test void testFromCacheHandleSkipsTargetChangesWhenCachedWrapperCannotSetTarget() { MethodType type = MethodType.methodType(Object, IndyInterfaceCallSiteTargetTest) From cb74b15efd1209417fe4300ab35abdb4822db18a Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Thu, 21 May 2026 09:27:31 +0200 Subject: [PATCH 06/19] GROOVY-12023: further fixes for multi threading and fixing some minor issues --- .../groovy/vmplugin/v8/CacheableCallSite.java | 135 +++++++++++------- .../groovy/vmplugin/v8/IndyInterface.java | 35 ++--- .../vmplugin/v8/MethodHandleWrapper.java | 23 +-- .../v8/IndyInterfaceCallSiteTargetTest.groovy | 5 +- 4 files changed, 111 insertions(+), 87 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 57d726e34fd..191ec1ca718 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -23,12 +23,12 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.invoke.MutableCallSite; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; import java.util.function.UnaryOperator; import java.util.logging.Level; @@ -66,6 +66,7 @@ * @since 3.0.0 */ public class CacheableCallSite extends MutableCallSite { + private static final Logger LOGGER = Logger.getLogger(CacheableCallSite.class.getName()); private static final int CACHE_SIZE = SystemUtil.getIntegerSafe("groovy.indy.callsite.cache.size", 8); private static final float LOAD_FACTOR = 0.75f; private static final int INITIAL_CAPACITY = (int) Math.ceil(CACHE_SIZE / LOAD_FACTOR) + 1; @@ -87,6 +88,7 @@ public class CacheableCallSite extends MutableCallSite { * Concurrency: {@code volatile} ensures that updates to the PIC chain are immediately * visible to all threads during high-speed dispatch. */ + @SuppressWarnings("java:S3077") private volatile MethodHandle picChain; /** @@ -104,7 +106,7 @@ public class CacheableCallSite extends MutableCallSite { */ private volatile int picCount; private final Map> lruCache = - new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { + new LinkedHashMap<>(INITIAL_CAPACITY, LOAD_FACTOR, true) { @Serial private static final long serialVersionUID = 7785958879964294463L; /** @@ -123,7 +125,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { * Creates a cacheable call site for the supplied type and lookup context. * * @param type the call-site type - * @param lookup the lookup used to unreflect targets + * @param lookup the lookup used to un-reflect targets */ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { super(type); @@ -137,7 +139,7 @@ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { * @param key the receiver cache key * @return the cached wrapper, or {@code null} if not found or not MRU */ - public MethodHandleWrapper get(Object key) { + MethodHandleWrapper get(Object key) { MRUEntry entry = mruEntry; if (entry != null && entry.key == key) { return entry.wrapper; @@ -156,19 +158,21 @@ public MethodHandleWrapper get(Object key) { * @param sender the caller class * @return the cached or newly created wrapper */ - public MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider valueProvider, Class sender) { + MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider valueProvider, Class sender) { MethodHandleWrapper result = null; SoftReference resultSoftReference; synchronized (lruCache) { resultSoftReference = lruCache.get(key); if (null != resultSoftReference) { result = resultSoftReference.get(); - if (null == result) removeAllStaleEntriesOfLruCache(); + if (null == result) { + lruCache.remove(key); + } } if (null == result) { result = valueProvider.provide(key); - resultSoftReference = new SoftReference<>(result); + resultSoftReference = new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache); lruCache.put(key, resultSoftReference); } } @@ -211,13 +215,15 @@ private static boolean isSafeLoader(ClassLoader callerLoader, ClassLoader target * @param mhw the wrapper to cache * @return the previously cached wrapper, or {@code null} if none existed */ - public MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { + MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { synchronized (lruCache) { - final SoftReference methodHandleWrapperSoftReference; - methodHandleWrapperSoftReference = lruCache.put(key, new SoftReference<>(mhw)); + final SoftReference methodHandleWrapperSoftReference = + lruCache.put(key, new SoftReferenceWithKey(key, mhw, REFERENCE_QUEUE, lruCache)); if (null == methodHandleWrapperSoftReference) return null; final MethodHandleWrapper methodHandleWrapper = methodHandleWrapperSoftReference.get(); - if (null == methodHandleWrapper) removeAllStaleEntriesOfLruCache(); + if (null == methodHandleWrapper) { + lruCache.remove(key); + } return methodHandleWrapper; } } @@ -248,55 +254,32 @@ public void maybeUpdatePic(Object key, UnaryOperator updater) { } } - /** - * Resets the PIC and targets to the default state if not already reset. - *

- * Concurrency: Synchronizes on {@code this} to safely reset the - * target and PIC metadata when the site becomes too megamorphic. - */ - public void resetPicAndTarget() { - synchronized (this) { - if (getTarget() != defaultTarget) { - setTarget(defaultTarget); - resetFallbackCount(); - } - } - } - - public synchronized void recordInPic(Object key) { - if (picCount < picKeys.length) { - picKeys[picCount++] = key; - } - } - /** * Checks if a receiver shape is already present in the PIC. *

- * Concurrency: Marked as {@code synchronized} to prevent reading inconsistent state - * while the PIC is being updated. + * Concurrency: Lock-free read of the PIC metadata. The volatile read of {@link #picCount} + * ensures visibility of prior writes to {@link #picKeys} by the same or another thread. * * @param key the receiver cache key * @return {@code true} if the key is in the PIC */ - public synchronized boolean picIncludes(Object key) { - for (int i = 0; i < picCount; i++) { - if (picKeys[i] == key) return true; + public boolean picInsertIfMissing(Object key) { + int count = picCount; + for (int i = 0; i < count; i++) { + if (picKeys[i] == key) return false; } - return false; + return true; } public MethodHandle getPicChain() { return picChain; } - public void setPicChain(MethodHandle picChain) { - this.picChain = picChain; - } - - public synchronized int getPicCount() { + public int getPicCount() { return picCount; } + @javax.annotation.concurrent.Immutable private static final class MRUEntry { final Object key; final MethodHandleWrapper wrapper; @@ -306,12 +289,50 @@ private static final class MRUEntry { } } - private void removeAllStaleEntriesOfLruCache() { - CACHE_CLEANER_QUEUE.offer(() -> { - synchronized (lruCache) { - lruCache.values().removeIf(v -> null == v.get()); + private static class SoftReferenceWithKey extends SoftReference { + private final Object key; + private final Map> cache; + + SoftReferenceWithKey(Object key, MethodHandleWrapper referent, ReferenceQueue q, Map> cache) { + super(referent, q); + this.key = key; + this.cache = cache; + } + + void clean() { + synchronized (cache) { + if (cache.get(key) == this) { + cache.remove(key); + } + } + } + } + + /** + * Atomically resets the call site to the default target when the fallback count + * exceeds the given threshold. This provides a concurrency-safe, double-checked + * locking reset for mega-morphic call sites. + *

+ * Concurrency: Synchronizes on {@code this} to atomically reset + * the target and associated PIC metadata, preventing redundant resets + * when multiple threads detect a high fallback count simultaneously. + * + * @param defaultTarget the default target handle to restore + * @param threshold the fallback count threshold that triggers a reset + * @param fallbackCount the current fallback count + * @return {@code true} if the target was reset to the default + */ + public boolean tryResetToDefaultTarget(MethodHandle defaultTarget, long threshold, long fallbackCount) { + if (fallbackCount > threshold && getTarget() != defaultTarget) { + synchronized (this) { + if (getTarget() != defaultTarget) { + setTarget(defaultTarget); + resetFallbackCount(); + return true; + } } - }); + } + return false; } /** @@ -391,16 +412,22 @@ public MethodHandles.Lookup getLookup() { return lookup; } - private static final BlockingQueue CACHE_CLEANER_QUEUE = new LinkedBlockingQueue<>(); + private static final ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>(); static { Thread cacheCleaner = new Thread(() -> { while (true) { try { - CACHE_CLEANER_QUEUE.take().run(); - } catch (Throwable ignore) { + Reference ref = REFERENCE_QUEUE.remove(); + if (ref instanceof SoftReferenceWithKey sRef) { + sRef.clean(); + } + } catch (@SuppressWarnings("java:S1181") Throwable throwable) { + if (throwable instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); - if (logger.isLoggable(Level.FINEST)) { - logger.finest(DefaultGroovyMethods.asString(ignore)); + if (LOGGER.isLoggable(Level.FINEST)) { + logger.finest(DefaultGroovyMethods.asString(throwable)); } } } 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 982b088e803..bb7f1823835 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -18,6 +18,7 @@ */ package org.codehaus.groovy.vmplugin.v8; +import edu.umd.cs.findbugs.annotations.NonNull; import groovy.lang.GroovySystem; import org.apache.groovy.util.SystemUtil; import org.codehaus.groovy.GroovyBugError; @@ -205,6 +206,7 @@ public int getOrderNumber() { * Concurrency: {@code volatile} ensures that global invalidations are immediately * visible to all threads across the JVM, causing them to fall back from JIT-optimized handles. */ + @SuppressWarnings("java:S3077") protected static volatile SwitchPoint switchPoint = new SwitchPoint(); static { @@ -371,9 +373,9 @@ public static Object fromCache(CacheableCallSite callSite, Class sender, Stri } private static final Object NULL_KEY = new Object(); - private static final ClassValue STATIC_KEYS = new ClassValue() { + private static final ClassValue STATIC_KEYS = new ClassValue<>() { @Override - protected Object computeValue(Class type) { + protected Object computeValue(@NonNull Class type) { return new Object(); } }; @@ -388,16 +390,14 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class MethodHandleWrapper mhw = callSite.get(receiverKey); if (mhw != null) { mhw.incrementLatestHitCount(); - if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle())) { - if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, mhw); - } + if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { + optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); } return mhw.getCachedMethodHandle(); } FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); - mhw = callSite.getAndPut(receiverKey, (theKey) -> { + mhw = callSite.getAndPut(receiverKey, theKey -> { MethodHandleWrapper fallback = fallbackSupplier.get(); if (fallback.isCanSetTarget()) return fallback; return NULL_METHOD_HANDLE_WRAPPER; @@ -418,21 +418,20 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class } } - if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD) { - optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, mhw); + if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { + optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); } } return mhw.getCachedMethodHandle(); } - private static void optimizeCallSite(CacheableCallSite callSite, Class sender, String methodName, int callID, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments, MethodHandleWrapper mhw) { + private static void optimizeCallSite(CacheableCallSite callSite, Class sender, String methodName, int callID, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments, Object receiverKey, MethodHandleWrapper mhw) { if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { if (callSite.getTarget() != callSite.getDefaultTarget()) { callSite.setTarget(callSite.getDefaultTarget()); } } else { - Object receiverKey = receiverCacheKey(arguments[0]); callSite.maybeUpdatePic(receiverKey, picChain -> { Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); selector.skipSwitchPoint = true; @@ -466,18 +465,8 @@ private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class MethodHandle defaultTarget = callSite.getDefaultTarget(); long fallbackCount = callSite.incrementFallbackCount(); - if ((fallbackCount > INDY_FALLBACK_THRESHOLD) && (callSite.getTarget() != defaultTarget)) { - /** - * Concurrency: Synchronizes on the {@code callSite} to safely reset the - * target and PIC metadata when the site becomes too megamorphic. - */ - synchronized (callSite) { - if (callSite.getTarget() != defaultTarget) { - callSite.setTarget(defaultTarget); - if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation"); - callSite.resetFallbackCount(); - } - } + if (callSite.tryResetToDefaultTarget(defaultTarget, INDY_FALLBACK_THRESHOLD, fallbackCount)) { + if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation"); } if (callSite.getTarget() == defaultTarget) { diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java index 71c3cb5dbb8..34b59ac0617 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java @@ -21,7 +21,7 @@ import groovy.lang.MetaMethod; import java.lang.invoke.MethodHandle; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; /** * Wrap method handles @@ -33,7 +33,7 @@ class MethodHandleWrapper { private final MethodHandle targetMethodHandle; private final MetaMethod method; private final boolean canSetTarget; - private final AtomicLong latestHitCount = new AtomicLong(0); + private final LongAdder latestHitCount = new LongAdder(); /** * Creates a wrapper for the cached and relink targets of a meta method. @@ -88,18 +88,25 @@ public boolean isCanSetTarget() { /** * Increments the hit count for the latest inline-cache hit. - * - * @return the updated hit count */ - public long incrementLatestHitCount() { - return latestHitCount.incrementAndGet(); + public void incrementLatestHitCount() { + latestHitCount.increment(); } /** * Resets the latest-hit counter. */ public void resetLatestHitCount() { - latestHitCount.set(0); + latestHitCount.reset(); + } + + /** + * Adds the specified value to the latest-hit counter. + * + * @param value the value to add + */ + public void addLatestHitCount(long value) { + latestHitCount.add(value); } /** @@ -108,7 +115,7 @@ public void resetLatestHitCount() { * @return the current latest-hit counter */ public long getLatestHitCount() { - return latestHitCount.get(); + return latestHitCount.sum(); } /** diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index 36559583080..b2f5f80e4bf 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -424,7 +424,8 @@ final class IndyInterfaceCallSiteTargetTest { private static void primeLatestHitCount(CacheableCallSite callSite, Object receiver, MethodHandleWrapper wrapper, long value) { assertSame(wrapper, callSite.getAndPut(IndyInterface.receiverCacheKey(receiver), { wrapper }, IndyInterfaceCallSiteTargetTest)) - wrapper.@latestHitCount.set(value) + wrapper.resetLatestHitCount() + wrapper.addLatestHitCount(value) } private static void assertFallbackCutoffLeavesDefaultTarget(boolean startAwayFromDefaultTarget) { @@ -452,7 +453,7 @@ final class IndyInterfaceCallSiteTargetTest { assertSame(wrapper.cachedMethodHandle, methodHandle) assertSame(callSite.defaultTarget, callSite.target) - assertEquals(0L, wrapper.latestHitCount) + assertEquals(0L, wrapper.getLatestHitCount()) } private static MethodHandleWrapper requireCachedWrapper(CacheableCallSite callSite, Object receiver) { From 83d1cf4579af58cb496ed11ad51caf9f1372de71 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Thu, 21 May 2026 11:55:32 +0200 Subject: [PATCH 07/19] move heavy method selection code out of synchronized block --- .../groovy/vmplugin/v8/CacheableCallSite.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 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 191ec1ca718..cf9e63b11a7 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -160,25 +160,41 @@ MethodHandleWrapper get(Object key) { */ MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider valueProvider, Class sender) { MethodHandleWrapper result = null; - SoftReference resultSoftReference; + + // First check under lock (fast path — already in cache) synchronized (lruCache) { - resultSoftReference = lruCache.get(key); + SoftReference resultSoftReference = lruCache.get(key); if (null != resultSoftReference) { result = resultSoftReference.get(); if (null == result) { lruCache.remove(key); } } + } - if (null == result) { - result = valueProvider.provide(key); - resultSoftReference = new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache); - lruCache.put(key, resultSoftReference); + // Compute outside lock if not found (expensive operation — method selection) + if (null == result) { + result = valueProvider.provide(key); + + // Second check under lock — another thread may have stored it in the meantime + synchronized (lruCache) { + SoftReference existingRef = lruCache.get(key); + if (existingRef != null) { + MethodHandleWrapper existing = existingRef.get(); + if (existing != null) { + // Another thread already computed and stored; use theirs + result = existing; + } else { + // Reference was cleared; replace it + lruCache.put(key, new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache)); + } + } else { + lruCache.put(key, new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache)); + } } } updateMRU(key, result, sender); - return result; } From 916340f21cb5992fbf4c210e8fb2cba853375d12 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Thu, 21 May 2026 12:04:00 +0200 Subject: [PATCH 08/19] GROOVY-12023: avoid overhead through stream generation --- .../codehaus/groovy/vmplugin/v8/Selector.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) 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 44b9ee61c45..945ca570378 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -63,10 +63,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.Objects; import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.ARRAYLIST_CONSTRUCTOR; import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.BEAN_CONSTRUCTOR_PROPERTY_SETTER; @@ -1109,7 +1107,7 @@ public void setGuards(Object receiver) { // guards for receiver and parameter Class[] pt = handle.type().parameterArray(); - if (Arrays.stream(args).anyMatch(Objects::isNull)) { + if (anyArgIsNull(args)) { for (int i = 0; i < args.length; i++) { MethodHandle test; var arg = args[i]; @@ -1126,9 +1124,9 @@ public void setGuards(Object receiver) { test = MethodHandles.dropArguments(test, 0, drops); handle = MethodHandles.guardWithTest(test, handle, fallback); } - } else if (Arrays.stream(pt).anyMatch(nonFinalOrNullUnsafe)) { + } else if (anyTypeIsNonFinalOrNullUnsafe(pt, nonFinalOrNullUnsafe)) { MethodHandle test = SAME_CLASSES - .bindTo(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)) + .bindTo(getArgClasses(args)) .asCollector(Object[].class, pt.length) .asType(MethodType.methodType(boolean.class, pt)); handle = MethodHandles.guardWithTest(test, handle, fallback); @@ -1275,4 +1273,35 @@ private static Class getThisType(Class sender) { } return sender; } + + /** + * Returns {@code true} if any element in the array is {@code null}. + */ + private static boolean anyArgIsNull(Object[] args) { + for (Object arg : args) { + if (arg == null) return true; + } + return false; + } + + /** + * Returns {@code true} if any type in the array satisfies the predicate. + */ + private static boolean anyTypeIsNonFinalOrNullUnsafe(Class[] pt, java.util.function.Predicate> predicate) { + for (Class t : pt) { + if (predicate.test(t)) return true; + } + return false; + } + + /** + * Returns an array of runtime classes for the given arguments. + */ + private static Class[] getArgClasses(Object[] args) { + Class[] classes = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + classes[i] = args[i].getClass(); + } + return classes; + } } From 6122b9b4ef8fdc246516b870ad3f4f16d47c6dc1 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Thu, 21 May 2026 22:30:58 +0200 Subject: [PATCH 09/19] GROOVY-12023: remove exception handler where not required --- .../groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java | 2 +- src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java index b99a3951599..1a78ac99e9a 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java @@ -90,7 +90,7 @@ public class IndyGuardsFiltersAndSignatures { HAS_CATEGORY_IN_CURRENT_THREAD_GUARD = LOOKUP.findStatic (GroovyCategorySupport.class, "hasCategoryInCurrentThread", MethodType.methodType(boolean.class)); GROOVY_OBJECT_INVOKER = LOOKUP.findStatic (IndyGuardsFiltersAndSignatures.class, "invokeGroovyObjectInvoker", MethodType.methodType(Object.class, MissingMethodException.class, Object.class, String.class, Object[].class)); GROOVY_OBJECT_GET_PROPERTY = LOOKUP.findVirtual(GroovyObject.class, "getProperty", MethodType.methodType(Object.class, String.class)); - META_METHOD_INVOKER = LOOKUP.findVirtual(MetaMethod.class, "doMethodInvoke", MethodType.methodType(Object.class, Object.class, Object[].class)); + META_METHOD_INVOKER = LOOKUP.findVirtual(MetaMethod.class, "invoke", MethodType.methodType(Object.class, Object.class, Object[].class)); META_CLASS_INVOKE_METHOD = LOOKUP.findVirtual(MetaClass.class, "invokeMethod", MethodType.methodType(Object.class, Class.class, Object.class, String.class, Object[].class, boolean.class, boolean.class)); META_CLASS_INVOKE_STATIC_METHOD = LOOKUP.findVirtual(MetaObjectProtocol.class, "invokeStaticMethod", MethodType.methodType(Object.class, Object.class, String.class, Object[].class)); BEAN_CONSTRUCTOR_PROPERTY_SETTER = LOOKUP.findStatic (IndyGuardsFiltersAndSignatures.class, "setBeanProperties", MethodType.methodType(Object.class, MetaClass.class, Object.class, Map.class)); 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 945ca570378..0e310b906db 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -814,6 +814,7 @@ public void setHandleForMetaMethod() { } catch (ReflectiveOperationException e) { throw new GroovyBugError(e); } + catchException = false; if (isStaticCategoryTypeMethod) { handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY); handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0)); @@ -828,6 +829,7 @@ public void setHandleForMetaMethod() { // generic meta method invocation path handle = META_METHOD_INVOKER; handle = handle.bindTo(method); + catchException = false; if (spread) { args = originalArguments; skipSpreadCollector = true; From f314dc8f3f06f62de82ca9b95acdd88bad196e03 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Sat, 23 May 2026 07:36:37 +0200 Subject: [PATCH 10/19] GROOVY-12023: optimize DGM handling by skipping their wrapper structure and directly invoke the target --- .../reflection/GeneratedMetaMethod.java | 14 +++++ .../codehaus/groovy/tools/DgmConverter.java | 61 +++++++++++++++++++ .../v8/IndyGuardsFiltersAndSignatures.java | 2 +- .../codehaus/groovy/vmplugin/v8/Selector.java | 5 ++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java index e8253c3bf05..2774c6e0f4b 100644 --- a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java +++ b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.Serial; import java.io.Serializable; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -70,6 +71,19 @@ public CachedClass getDeclaringClass() { return declaringClass; } + /** + * Returns a {@link MethodHandle} pointing directly to the underlying target method, + * or {@code null} if not available. + *

+ * Generated DGM adapter classes override this to provide a pre-computed handle that + * avoids the boxing overhead of {@link #invoke(Object, Object[])}. + * + * @return a method handle for direct invocation, or {@code null} + */ + public MethodHandle getTargetMethodHandle() { + return null; + } + public static class Proxy extends GeneratedMetaMethod { private volatile MetaMethod proxy; private final String className; diff --git a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java index 59639388eed..a2c1fa428b4 100644 --- a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java +++ b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java @@ -41,11 +41,14 @@ import static org.objectweb.asm.Opcodes.AALOAD; import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; import static org.objectweb.asm.Opcodes.ACONST_NULL; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ARETURN; import static org.objectweb.asm.Opcodes.ASTORE; +import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GOTO; import static org.objectweb.asm.Opcodes.ICONST_0; import static org.objectweb.asm.Opcodes.ICONST_1; @@ -55,6 +58,7 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.IRETURN; +import static org.objectweb.asm.Opcodes.PUTSTATIC; import static org.objectweb.asm.Opcodes.RETURN; /** @@ -117,12 +121,16 @@ public static void main(String[] args) throws IOException { final String methodDescriptor = BytecodeHelper.getMethodDescriptor(returnType, method.getNativeParameterTypes()); + createTargetMethodHandleField(cw, method, className); + createInvokeMethod(method, cw, returnType, methodDescriptor); createDoMethodInvokeMethod(method, cw, className, returnType, methodDescriptor); createIsValidMethodMethod(method, cw, className); + createGetTargetMethodHandleMethod(cw, className); + cw.visitEnd(); final byte[] bytes = cw.toByteArray(); @@ -272,4 +280,57 @@ protected static void loadParameters(CachedMethod method, int argumentIndex, Met BytecodeHelper.doCast(mv, type); } } + + private static void createTargetMethodHandleField(ClassWriter cw, CachedMethod method, String className) { + // private static final java.lang.invoke.MethodHandle TARGET; + cw.visitField(ACC_PRIVATE + ACC_STATIC + ACC_FINAL, + "TARGET", "Ljava/lang/invoke/MethodHandle;", null, null).visitEnd(); + + // static initializer + MethodVisitor mv = cw.visitMethod(ACC_STATIC, "", "()V", null, null); + mv.visitCode(); + + // Lookup lookup = java.lang.invoke.MethodHandles.lookup(); + mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup", + "()Ljava/lang/invoke/MethodHandles$Lookup;", false); + mv.visitVarInsn(ASTORE, 0); + + // Class ownerClass = .class; + String ownerInternal = BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()); + mv.visitLdcInsn(org.objectweb.asm.Type.getObjectType(ownerInternal)); + mv.visitVarInsn(ASTORE, 1); + + // String methodName = "" + mv.visitLdcInsn(method.getName()); + mv.visitVarInsn(ASTORE, 2); + + // MethodType methodType = MethodType.methodType(, , , ...) + mv.visitLdcInsn(org.objectweb.asm.Type.getMethodType(method.getDescriptor())); + mv.visitVarInsn(ASTORE, 3); + + // TARGET = lookup.findStatic(ownerClass, methodName, methodType); + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ALOAD, 1); + mv.visitVarInsn(ALOAD, 2); + mv.visitVarInsn(ALOAD, 3); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false); + mv.visitFieldInsn(PUTSTATIC, className, "TARGET", "Ljava/lang/invoke/MethodHandle;"); + + mv.visitInsn(RETURN); + mv.visitMaxs(4, 4); + mv.visitEnd(); + } + + private static void createGetTargetMethodHandleMethod(ClassWriter cw, String className) { + MethodVisitor mv; + // public MethodHandle getTargetMethodHandle() { return TARGET; } + mv = cw.visitMethod(ACC_PUBLIC, "getTargetMethodHandle", + "()Ljava/lang/invoke/MethodHandle;", null, null); + mv.visitCode(); + mv.visitFieldInsn(GETSTATIC, className, "TARGET", "Ljava/lang/invoke/MethodHandle;"); + mv.visitInsn(ARETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } } diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java index 1a78ac99e9a..b99a3951599 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java @@ -90,7 +90,7 @@ public class IndyGuardsFiltersAndSignatures { HAS_CATEGORY_IN_CURRENT_THREAD_GUARD = LOOKUP.findStatic (GroovyCategorySupport.class, "hasCategoryInCurrentThread", MethodType.methodType(boolean.class)); GROOVY_OBJECT_INVOKER = LOOKUP.findStatic (IndyGuardsFiltersAndSignatures.class, "invokeGroovyObjectInvoker", MethodType.methodType(Object.class, MissingMethodException.class, Object.class, String.class, Object[].class)); GROOVY_OBJECT_GET_PROPERTY = LOOKUP.findVirtual(GroovyObject.class, "getProperty", MethodType.methodType(Object.class, String.class)); - META_METHOD_INVOKER = LOOKUP.findVirtual(MetaMethod.class, "invoke", MethodType.methodType(Object.class, Object.class, Object[].class)); + META_METHOD_INVOKER = LOOKUP.findVirtual(MetaMethod.class, "doMethodInvoke", MethodType.methodType(Object.class, Object.class, Object[].class)); META_CLASS_INVOKE_METHOD = LOOKUP.findVirtual(MetaClass.class, "invokeMethod", MethodType.methodType(Object.class, Class.class, Object.class, String.class, Object[].class, boolean.class, boolean.class)); META_CLASS_INVOKE_STATIC_METHOD = LOOKUP.findVirtual(MetaObjectProtocol.class, "invokeStaticMethod", MethodType.methodType(Object.class, Object.class, String.class, Object[].class)); BEAN_CONSTRUCTOR_PROPERTY_SETTER = LOOKUP.findStatic (IndyGuardsFiltersAndSignatures.class, "setBeanProperties", MethodType.methodType(Object.class, MetaClass.class, Object.class, Map.class)); 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 0e310b906db..af7d93a4da9 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -824,6 +824,11 @@ public void setHandleForMetaMethod() { // Object.class handles both cases at once handle = MethodHandles.dropArguments(handle, 0, Object.class); } + } else if (method instanceof GeneratedMetaMethod gMethod) { + if (LOG_ENABLED) LOG.info("meta method is generated helper"); + handle = gMethod.getTargetMethodHandle(); + isVargs = gMethod.isVargsMethod(); + catchException = false; } else if (method != null) { if (LOG_ENABLED) LOG.info("meta method is dgm helper"); // generic meta method invocation path From 3b211adc53db833c9572fa6de3a4d34732b7ed85 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Sat, 23 May 2026 08:37:32 +0200 Subject: [PATCH 11/19] GROOVY-12023: fix ScriptToTreeNodeAdapterTest failures - revert CachedMethod catchException=false only --- src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 af7d93a4da9..afe5e6c2e61 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -783,7 +783,7 @@ public void setHandleForMetaMethod() { boolean isCategoryTypeMethod = (metaMethod instanceof NewInstanceMetaMethod); if (LOG_ENABLED) LOG.info("meta method is category type method: " + isCategoryTypeMethod); boolean isStaticCategoryTypeMethod = (metaMethod instanceof NewStaticMetaMethod); - if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isCategoryTypeMethod); + if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isStaticCategoryTypeMethod); if (metaMethod instanceof ReflectionMetaMethod) { if (LOG_ENABLED) LOG.info("meta method is reflective method"); @@ -792,6 +792,7 @@ public void setHandleForMetaMethod() { if (metaMethod instanceof CachedMethod cm) { isVargs = metaMethod.isVargsMethod(); + catchException = false; VMPlugin vmplugin = VMPluginFactory.getPlugin(); cm = (CachedMethod) vmplugin.transformMetaMethod(mc, cm, sender); try { @@ -810,11 +811,11 @@ public void setHandleForMetaMethod() { handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader()); } else { handle = unreflect(cm.getCachedMethod()); + catchException = !isCategoryTypeMethod && !isStaticCategoryTypeMethod; } } catch (ReflectiveOperationException e) { throw new GroovyBugError(e); } - catchException = false; if (isStaticCategoryTypeMethod) { handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY); handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0)); @@ -834,7 +835,6 @@ public void setHandleForMetaMethod() { // generic meta method invocation path handle = META_METHOD_INVOKER; handle = handle.bindTo(method); - catchException = false; if (spread) { args = originalArguments; skipSpreadCollector = true; From e12ad162685fd759a3ea7feb687d723409cf058b Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Sat, 23 May 2026 09:46:12 +0200 Subject: [PATCH 12/19] GROOVY-12023: restrict CachedClass path to catch exception only if method target is instance of GroovyObject --- .../codehaus/groovy/vmplugin/v8/Selector.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 afe5e6c2e61..29fe4fc8759 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,16 @@ import groovy.lang.MissingMethodException; import groovy.lang.ProxyMetaClass; import groovy.transform.Internal; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import org.codehaus.groovy.GroovyBugError; import org.codehaus.groovy.reflection.CachedField; import org.codehaus.groovy.reflection.CachedMethod; @@ -55,17 +65,6 @@ import org.codehaus.groovy.vmplugin.VMPlugin; import org.codehaus.groovy.vmplugin.VMPluginFactory; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; - import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.ARRAYLIST_CONSTRUCTOR; import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.BEAN_CONSTRUCTOR_PROPERTY_SETTER; import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.CLASS_FOR_NAME; @@ -812,6 +811,7 @@ public void setHandleForMetaMethod() { } else { handle = unreflect(cm.getCachedMethod()); catchException = !isCategoryTypeMethod && !isStaticCategoryTypeMethod; + if (!catchException) catchException = metaMethod.getDeclaringClass().isAssignableFrom(GroovyObject.class); } } catch (ReflectiveOperationException e) { throw new GroovyBugError(e); From c6429353c41ea22736734ea5ef60a3062d929bea Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Sat, 23 May 2026 11:13:24 +0200 Subject: [PATCH 13/19] GROOVY-12023: narrow exception handling to cater only for GroovyObject MOP met --- .../codehaus/groovy/vmplugin/v8/Selector.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) 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 29fe4fc8759..0cb0aa75c6e 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -42,6 +42,7 @@ import java.util.Collection; import java.util.HashSet; import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.reflection.CachedClass; import org.codehaus.groovy.reflection.CachedField; import org.codehaus.groovy.reflection.CachedMethod; import org.codehaus.groovy.reflection.ClassInfo; @@ -811,7 +812,9 @@ public void setHandleForMetaMethod() { } else { handle = unreflect(cm.getCachedMethod()); catchException = !isCategoryTypeMethod && !isStaticCategoryTypeMethod; - if (!catchException) catchException = metaMethod.getDeclaringClass().isAssignableFrom(GroovyObject.class); + if (catchException && metaMethod.getDeclaringClass().isAssignableFrom(GroovyObject.class)) { + catchException = invokesMOPMethod(name, handle, metaMethod.getDeclaringClass()); + } } } catch (ReflectiveOperationException e) { throw new GroovyBugError(e); @@ -847,6 +850,30 @@ public void setHandleForMetaMethod() { } } + private boolean invokesMOPMethod(String name, MethodHandle handle, CachedClass declaringClass) { + // here we already know the target class is an instance of GroovyObject + // MOP methods can use exceptions to control the MOP; thus we have to catch the exception in those cases + if (name.equals("invokeMethod")) { + if (handle.type().parameterCount() != 3) return false; + if (handle.type().parameterType(1) != String.class) return false; + return handle.type().parameterType(2) == Object.class; + } + if (name.equals("getProperty")) { + if (handle.type().parameterCount() != 2) return false; + return handle.type().parameterType(1) == String.class; + /*if (handle.type().parameterType(1) != String.class) { + throw new GroovyRuntimeException("getProperty method had parameter type " + handle.type() + " on " + declaringClass.getTheClass() + " is a GroovyObject " + declaringClass.isAssignableFrom(GroovyObject.class)); + } + //return true;*/ + } + if (name.equals("setProperty")) { + if (handle.type().parameterCount() != 3) return false; + if (handle.type().parameterType(1) != String.class) return false; + return handle.type().parameterType(2) == Object.class; + } + return false; + } + /** * Unreflects a cached reflective method against the call-site lookup. * From 164311f6c7bfb5ac27d4f4da1ec93b29baf3924a Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Sat, 23 May 2026 11:27:28 +0200 Subject: [PATCH 14/19] GROOVY-12023: adding missing licenses to new tests --- .../vmplugin/v8/IndyClassLoaderLeakTest.groovy | 18 ++++++++++++++++++ .../vmplugin/v8/IndyInterfacePicTest.groovy | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy index 35c81eff3dc..c9be7bcb1be 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy @@ -1,3 +1,21 @@ +/* + * 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.codehaus.groovy.vmplugin.v8 import org.junit.jupiter.api.Test diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy index 1f6715259d1..7eb84657cd1 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfacePicTest.groovy @@ -1,3 +1,21 @@ +/* + * 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.codehaus.groovy.vmplugin.v8 import org.junit.jupiter.api.Test From 2ad2cc5b0ee5963de32528948e0100e6122f287b Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Mon, 25 May 2026 13:33:15 +0200 Subject: [PATCH 15/19] GROOVY-12023: improving inlining for DTT operations --- .../DefaultTypeTransformation.java | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java index 611e01c07d3..a482d21b7d9 100644 --- a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java +++ b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java @@ -96,6 +96,9 @@ public class DefaultTypeTransformation { * @throws GroovyCastException if the object cannot be converted to a number */ public static byte byteUnbox(final Object value) { + if (value instanceof Byte b) { + return b; + } Number n = castToNumber(value, byte.class); return n.byteValue(); } @@ -111,9 +114,11 @@ public static byte byteUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to char */ public static char charUnbox(final Object value) { + if (value instanceof Character c) { + return c; + } if (value == null) return '\u0000'; // GROOVY-11371 - var ch = ShortTypeHandling.castToChar(value); - return ch; + return ShortTypeHandling.castToChar(value); } /** @@ -126,6 +131,9 @@ public static char charUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to a number */ public static short shortUnbox(final Object value) { + if (value instanceof Short s) { + return s; + } Number n = castToNumber(value, short.class); return n.shortValue(); } @@ -140,6 +148,9 @@ public static short shortUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to a number */ public static int intUnbox(final Object value) { + if (value instanceof Integer i) { + return i; + } Number n = castToNumber(value, int.class); return n.intValue(); } @@ -153,6 +164,9 @@ public static int intUnbox(final Object value) { * @return the boolean value */ public static boolean booleanUnbox(final Object value) { + if (value instanceof Boolean b) { + return b; + } return castToBoolean(value); } @@ -166,6 +180,9 @@ public static boolean booleanUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to a number */ public static long longUnbox(final Object value) { + if (value instanceof Long l) { + return l; + } Number n = castToNumber(value, long.class); return n.longValue(); } @@ -181,6 +198,9 @@ public static long longUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to a number */ public static float floatUnbox(final Object value) { + if (value instanceof Float f) { + return f; + } if (value == null) return Float.NaN; // GROOVY-11371 Number n = castToNumber(value, float.class); return n.floatValue(); @@ -197,6 +217,9 @@ public static float floatUnbox(final Object value) { * @throws GroovyCastException if the object cannot be converted to a number */ public static double doubleUnbox(final Object value) { + if (value instanceof Double d) { + return d; + } if (value == null) return Double.NaN; // GROOVY-11371 Number n = castToNumber(value, double.class); return n.doubleValue(); @@ -333,11 +356,15 @@ public static Number castToNumber(Object object) { * @throws GroovyCastException if the object cannot be converted to a number */ public static Number castToNumber(Object object, Class type) { - if (object instanceof Number) { - return (Number) object; + if (object instanceof Number number) { + return number; } - if (object instanceof Character) { - char c = (Character) object; + return castToNumberFallback(object, type); + } + + private static Number castToNumberFallback(Object object, Class type) { + if (object instanceof Character cObj) { + char c = cObj; return (int) c; } if (object instanceof GString) { @@ -370,6 +397,10 @@ public static boolean castToBoolean(Object object) { return (Boolean) object; } + return castToBooleanFallback(object); + } + + private static boolean castToBooleanFallback(Object object) { // if the object isn't null and no Boolean, try to call an asBoolean() method on the object return (Boolean) InvokerHelper.invokeMethod(object, "asBoolean", InvokerHelper.EMPTY_ARGS); } From 6df1d265a2f326fb40986f4d9f29a8c71dccb20e Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Mon, 25 May 2026 17:06:53 +0200 Subject: [PATCH 16/19] GROOVY-12023: fixing wrong isAssignableFrom check --- .../java/org/codehaus/groovy/vmplugin/v8/Selector.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 0cb0aa75c6e..0f5517265c9 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -42,7 +42,6 @@ import java.util.Collection; import java.util.HashSet; import org.codehaus.groovy.GroovyBugError; -import org.codehaus.groovy.reflection.CachedClass; import org.codehaus.groovy.reflection.CachedField; import org.codehaus.groovy.reflection.CachedMethod; import org.codehaus.groovy.reflection.ClassInfo; @@ -812,8 +811,8 @@ public void setHandleForMetaMethod() { } else { handle = unreflect(cm.getCachedMethod()); catchException = !isCategoryTypeMethod && !isStaticCategoryTypeMethod; - if (catchException && metaMethod.getDeclaringClass().isAssignableFrom(GroovyObject.class)) { - catchException = invokesMOPMethod(name, handle, metaMethod.getDeclaringClass()); + if (catchException && GroovyObject.class.isAssignableFrom(declaringClass)) { + catchException = invokesMOPMethod(name, handle); } } } catch (ReflectiveOperationException e) { @@ -850,7 +849,7 @@ public void setHandleForMetaMethod() { } } - private boolean invokesMOPMethod(String name, MethodHandle handle, CachedClass declaringClass) { + private boolean invokesMOPMethod(String name, MethodHandle handle) { // here we already know the target class is an instance of GroovyObject // MOP methods can use exceptions to control the MOP; thus we have to catch the exception in those cases if (name.equals("invokeMethod")) { From 7747e969bdde2201384066152392c4aec8ada893 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Mon, 25 May 2026 18:11:22 +0200 Subject: [PATCH 17/19] fixing switchpoint related bug in PIC logic --- .../groovy/vmplugin/v8/CacheableCallSite.java | 39 ++++++++++++++++--- .../groovy/vmplugin/v8/IndyInterface.java | 6 ++- .../vmplugin/v8/MethodHandleWrapper.java | 18 +++++++-- .../codehaus/groovy/vmplugin/v8/Selector.java | 12 ++++-- .../perf/grails/DynamicDispatchBench.groovy | 2 - 5 files changed, 61 insertions(+), 16 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 cf9e63b11a7..6f9e65a1e2d 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -90,6 +90,7 @@ public class CacheableCallSite extends MutableCallSite { */ @SuppressWarnings("java:S3077") private volatile MethodHandle picChain; + private volatile java.lang.invoke.SwitchPoint picSwitchPoint; /** * Keys corresponding to the handles in the {@link #picChain}. @@ -142,7 +143,11 @@ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { MethodHandleWrapper get(Object key) { MRUEntry entry = mruEntry; if (entry != null && entry.key == key) { - return entry.wrapper; + MethodHandleWrapper mhw = entry.wrapper; + if (mhw.getSwitchPoint() == IndyInterface.switchPoint) { + return mhw; + } + mruEntry = null; } return null; } @@ -166,8 +171,9 @@ MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider resultSoftReference = lruCache.get(key); if (null != resultSoftReference) { result = resultSoftReference.get(); - if (null == result) { + if (null == result || result.getSwitchPoint() != IndyInterface.switchPoint) { lruCache.remove(key); + result = null; } } } @@ -181,11 +187,11 @@ MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider existingRef = lruCache.get(key); if (existingRef != null) { MethodHandleWrapper existing = existingRef.get(); - if (existing != null) { + if (existing != null && existing.getSwitchPoint() == IndyInterface.switchPoint) { // Another thread already computed and stored; use theirs result = existing; } else { - // Reference was cleared; replace it + // Reference was cleared or stale; replace it lruCache.put(key, new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache)); } } else { @@ -254,8 +260,13 @@ MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { * @param key the receiver cache key * @param updater the callback used to build the new PIC link */ - public void maybeUpdatePic(Object key, UnaryOperator updater) { + public void maybeUpdatePic(Object key, java.util.function.UnaryOperator updater) { synchronized (this) { + var currentSwitchPoint = IndyInterface.switchPoint; + if (picSwitchPoint != currentSwitchPoint) { + clearPic(); + picSwitchPoint = currentSwitchPoint; + } for (int i = 0; i < picCount; i++) { if (picKeys[i] == key) return; } @@ -280,6 +291,9 @@ public void maybeUpdatePic(Object key, UnaryOperator updater) { * @return {@code true} if the key is in the PIC */ public boolean picInsertIfMissing(Object key) { + if (picSwitchPoint != IndyInterface.switchPoint) { + return true; + } int count = picCount; for (int i = 0; i < count; i++) { if (picKeys[i] == key) return false; @@ -291,6 +305,17 @@ public MethodHandle getPicChain() { return picChain; } + public void clearPic() { + synchronized (this) { + picChain = null; + picSwitchPoint = null; + picCount = 0; + for (int i = 0; i < picKeys.length; i++) { + picKeys[i] = null; + } + } + } + public int getPicCount() { return picCount; } @@ -428,6 +453,10 @@ public MethodHandles.Lookup getLookup() { return lookup; } + @Override public void setTarget(MethodHandle newTarget) { + super.setTarget(newTarget); + } + private static final ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>(); static { Thread cacheCleaner = new Thread(() -> { 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 bb7f1823835..381e419478e 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -388,7 +388,7 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class Object receiverKey = receiverCacheKey(receiver); MethodHandleWrapper mhw = callSite.get(receiverKey); - if (mhw != null) { + if (mhw != null && mhw.getSwitchPoint() == switchPoint) { mhw.incrementLatestHitCount(); if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); @@ -403,9 +403,10 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class return NULL_METHOD_HANDLE_WRAPPER; }, sender); - if (mhw == NULL_METHOD_HANDLE_WRAPPER) { + if (mhw == NULL_METHOD_HANDLE_WRAPPER || mhw.getSwitchPoint() != switchPoint) { // The PIC stores a sentinel to remember "do not relink this receiver shape"; // execution still needs a real handle for the current invocation. + // OR the cached handle is stale. mhw = fallbackSupplier.get(); } @@ -498,6 +499,7 @@ private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)), selector.handle, selector.method, + switchPoint, selector.cache ); } diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java index 34b59ac0617..9cbcfa23e27 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/MethodHandleWrapper.java @@ -19,8 +19,8 @@ package org.codehaus.groovy.vmplugin.v8; import groovy.lang.MetaMethod; - import java.lang.invoke.MethodHandle; +import java.lang.invoke.SwitchPoint; import java.util.concurrent.atomic.LongAdder; /** @@ -32,6 +32,7 @@ class MethodHandleWrapper { private final MethodHandle cachedMethodHandle; private final MethodHandle targetMethodHandle; private final MetaMethod method; + private final SwitchPoint switchPoint; private final boolean canSetTarget; private final LongAdder latestHitCount = new LongAdder(); @@ -41,12 +42,14 @@ class MethodHandleWrapper { * @param cachedMethodHandle the cached invocation handle * @param targetMethodHandle the relink target handle * @param method the associated meta method + * @param switchPoint the switch point associated with this handle * @param canSetTarget whether the call site target may be updated to this handle */ - public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, boolean canSetTarget) { + public MethodHandleWrapper(MethodHandle cachedMethodHandle, MethodHandle targetMethodHandle, MetaMethod method, SwitchPoint switchPoint, boolean canSetTarget) { this.cachedMethodHandle = cachedMethodHandle; this.targetMethodHandle = targetMethodHandle; this.method = method; + this.switchPoint = switchPoint; this.canSetTarget = canSetTarget; } @@ -118,6 +121,15 @@ public long getLatestHitCount() { return latestHitCount.sum(); } + /** + * Returns the switch point associated with this wrapper. + * + * @return the associated switch point + */ + public SwitchPoint getSwitchPoint() { + return switchPoint; + } + /** * Returns the sentinel wrapper used when no cacheable handle is available. * @@ -134,7 +146,7 @@ private static class NullMethodHandleWrapper extends MethodHandleWrapper { public static final NullMethodHandleWrapper INSTANCE = new NullMethodHandleWrapper(); private NullMethodHandleWrapper() { - super(null, null, null, false); + super(null, null, null, null, false); } } } 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 0f5517265c9..fa1573f5ea2 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -51,6 +51,7 @@ import org.codehaus.groovy.runtime.GeneratedClosure; import org.codehaus.groovy.runtime.GroovyCategorySupport; import org.codehaus.groovy.runtime.GroovyCategorySupport.CategoryMethod; +import org.codehaus.groovy.runtime.HandleMetaClass; import org.codehaus.groovy.runtime.MetaClassHelper; import org.codehaus.groovy.runtime.NullObject; import org.codehaus.groovy.runtime.dgmimpl.NumberNumberMetaMethod; @@ -827,16 +828,16 @@ public void setHandleForMetaMethod() { // Object.class handles both cases at once handle = MethodHandles.dropArguments(handle, 0, Object.class); } - } else if (method instanceof GeneratedMetaMethod gMethod) { + } else if (metaMethod instanceof GeneratedMetaMethod gMethod) { if (LOG_ENABLED) LOG.info("meta method is generated helper"); handle = gMethod.getTargetMethodHandle(); isVargs = gMethod.isVargsMethod(); catchException = false; - } else if (method != null) { + } else if (metaMethod != null) { if (LOG_ENABLED) LOG.info("meta method is dgm helper"); // generic meta method invocation path handle = META_METHOD_INVOKER; - handle = handle.bindTo(method); + handle = handle.bindTo(metaMethod); if (spread) { args = originalArguments; skipSpreadCollector = true; @@ -1242,7 +1243,10 @@ public void setCallSiteTarget() { /** * @return {@code mc} if {@code ClosureMetaClass}, {@code ExpandoMetaClass} or not {@code ProxyMetaClass}; otherwise null */ - private static MetaClassImpl getMetaClassImpl(final MetaClass mc, final boolean includeEMC) { + private static MetaClassImpl getMetaClassImpl(MetaClass mc, final boolean includeEMC) { + if (mc instanceof HandleMetaClass hmc) { + mc = hmc.getMetaClass(); + } boolean valid = (mc.getClass() == ClosureMetaClass.class) || (includeEMC && mc instanceof ExpandoMetaClass) || (mc instanceof MetaClassImpl && !(mc instanceof ExpandoMetaClass || mc instanceof ProxyMetaClass)); // GROOVY-11813 diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/DynamicDispatchBench.groovy b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/DynamicDispatchBench.groovy index a994cbdaeb7..03924c227e7 100644 --- a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/DynamicDispatchBench.groovy +++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/DynamicDispatchBench.groovy @@ -18,13 +18,11 @@ */ package org.apache.groovy.perf.grails -import groovy.lang.GroovySystem import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.TimeUnit - /** * Tests the performance of Groovy's dynamic method dispatch mechanisms: * {@code methodMissing}, {@code propertyMissing}, {@code invokeMethod}, From ebab001bafeaf010cb81b9a353a0fab41eaae4be Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Mon, 25 May 2026 23:22:25 +0200 Subject: [PATCH 18/19] GROOVY-12023: fix double switchpoint usage --- .../org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java | 4 ++-- .../java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java | 2 +- .../groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy | 1 + .../groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy | 2 +- 4 files changed, 5 insertions(+), 4 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 6f9e65a1e2d..3879a080e33 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -144,7 +144,7 @@ MethodHandleWrapper get(Object key) { MRUEntry entry = mruEntry; if (entry != null && entry.key == key) { MethodHandleWrapper mhw = entry.wrapper; - if (mhw.getSwitchPoint() == IndyInterface.switchPoint) { + if (mhw == MethodHandleWrapper.getNullMethodHandleWrapper() || mhw.getSwitchPoint() == IndyInterface.switchPoint) { return mhw; } mruEntry = null; @@ -171,7 +171,7 @@ MethodHandleWrapper getAndPut(Object key, MemoizeCache.ValueProvider resultSoftReference = lruCache.get(key); if (null != resultSoftReference) { result = resultSoftReference.get(); - if (null == result || result.getSwitchPoint() != IndyInterface.switchPoint) { + if (null == result || (result != MethodHandleWrapper.getNullMethodHandleWrapper() && result.getSwitchPoint() != IndyInterface.switchPoint)) { lruCache.remove(key); result = null; } 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 381e419478e..50daf394d8b 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -388,7 +388,7 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class Object receiverKey = receiverCacheKey(receiver); MethodHandleWrapper mhw = callSite.get(receiverKey); - if (mhw != null && mhw.getSwitchPoint() == switchPoint) { + if (mhw != null && (mhw == NULL_METHOD_HANDLE_WRAPPER || mhw.getSwitchPoint() == switchPoint)) { mhw.incrementLatestHitCount(); if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy index c9be7bcb1be..8892c40b1d1 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy @@ -73,6 +73,7 @@ final class IndyClassLoaderLeakTest { java.lang.invoke.MethodHandles.constant(Object, "test"), java.lang.invoke.MethodHandles.constant(Object, "test"), new org.codehaus.groovy.reflection.CachedMethod(targetClass.getDeclaredMethods().length > 0 ? targetClass.getDeclaredMethods()[0] : Object.class.getMethod("toString")), + IndyInterface.switchPoint, true ) callSite.updateMRU(key, wrapper, sender) diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index b2f5f80e4bf..d265f70a27d 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -407,7 +407,7 @@ final class IndyInterfaceCallSiteTargetTest { } private static MethodHandleWrapper newCachedWrapper(MethodType type, Object cachedValue, Object targetValue, MetaMethod method, boolean canSetTarget) { - new MethodHandleWrapper(cachedHandle(cachedValue), targetHandle(type, targetValue), method, canSetTarget) + new MethodHandleWrapper(cachedHandle(cachedValue), targetHandle(type, targetValue), method, IndyInterface.switchPoint, canSetTarget) } private static MethodHandle cachedHandle(Object value) { From da154cf773eeaef55a10cb6c644fa135e833b661 Mon Sep 17 00:00:00 2001 From: Jochen Theodorou Date: Wed, 27 May 2026 10:20:58 +0200 Subject: [PATCH 19/19] GROOVY-12023: Move call-site flags into CacheableCallSite and fix spread-call test - Moved callType, safe, thisCall, spreadCall from method parameters into CacheableCallSite fields, removed spread field from Selector - Updated IndyInterface bootstrap, Selector constructors, and callers to read flags from the call site - Fixed IndyInterfaceCallSiteTargetTest to match new API (fromCacheHandle returns MethodHandle, not result) - Updated spread-call tests to verify handle type instead of invoking MethodHandle methods from Groovy (JDK 17+ blocks Lookup.unreflect on MethodHandle) - Cleaned up unused imports and dead code - update VArgs test to test spread cases better --- .../DefaultTypeTransformation.java | 2 +- .../codehaus/groovy/tools/DgmConverter.java | 16 +-- .../groovy/vmplugin/v8/CacheableCallSite.java | 27 +++-- .../groovy/vmplugin/v8/IndyInterface.java | 78 +++++------- .../codehaus/groovy/vmplugin/v8/Selector.java | 107 ++++++----------- src/test/groovy/groovy/VArgsTest.groovy | 18 +++ .../v8/IndyClassLoaderLeakTest.groovy | 12 +- .../v8/IndyInterfaceCallSiteTargetTest.groovy | 111 ++++++------------ .../v8/IndyInterfaceDeprecatedTest.groovy | 4 +- 9 files changed, 154 insertions(+), 221 deletions(-) diff --git a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java index a482d21b7d9..10d589b37ca 100644 --- a/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java +++ b/src/main/java/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java @@ -362,7 +362,7 @@ public static Number castToNumber(Object object, Class type) { return castToNumberFallback(object, type); } - private static Number castToNumberFallback(Object object, Class type) { + private static Number castToNumberFallback(Object object, Class type) { if (object instanceof Character cObj) { char c = cObj; return (int) c; diff --git a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java index a2c1fa428b4..73b4bffff13 100644 --- a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java +++ b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java @@ -68,6 +68,8 @@ public class DgmConverter { private static final System.Logger LOGGER = System.getLogger(DgmConverter.class.getName()); + private static final String TARGET = "TARGET"; + private static final String METHOD_HANDLE_CLASS_NAME = "Ljava/lang/invoke/MethodHandle;"; /** * Generates DGM adapter classes into the target directory. @@ -282,20 +284,20 @@ protected static void loadParameters(CachedMethod method, int argumentIndex, Met } private static void createTargetMethodHandleField(ClassWriter cw, CachedMethod method, String className) { - // private static final java.lang.invoke.MethodHandle TARGET; + // private static final java.lang.invoke.MethodHandle TARGET cw.visitField(ACC_PRIVATE + ACC_STATIC + ACC_FINAL, - "TARGET", "Ljava/lang/invoke/MethodHandle;", null, null).visitEnd(); + TARGET, METHOD_HANDLE_CLASS_NAME, null, null).visitEnd(); // static initializer MethodVisitor mv = cw.visitMethod(ACC_STATIC, "", "()V", null, null); mv.visitCode(); - // Lookup lookup = java.lang.invoke.MethodHandles.lookup(); + // Lookup lookup = java.lang.invoke.MethodHandles.lookup() mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup", "()Ljava/lang/invoke/MethodHandles$Lookup;", false); mv.visitVarInsn(ASTORE, 0); - // Class ownerClass = .class; + // Class ownerClass = .class String ownerInternal = BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()); mv.visitLdcInsn(org.objectweb.asm.Type.getObjectType(ownerInternal)); mv.visitVarInsn(ASTORE, 1); @@ -308,14 +310,14 @@ private static void createTargetMethodHandleField(ClassWriter cw, CachedMethod m mv.visitLdcInsn(org.objectweb.asm.Type.getMethodType(method.getDescriptor())); mv.visitVarInsn(ASTORE, 3); - // TARGET = lookup.findStatic(ownerClass, methodName, methodType); + // TARGET = lookup.findStatic(ownerClass, methodName, methodType) mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 1); mv.visitVarInsn(ALOAD, 2); mv.visitVarInsn(ALOAD, 3); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false); - mv.visitFieldInsn(PUTSTATIC, className, "TARGET", "Ljava/lang/invoke/MethodHandle;"); + mv.visitFieldInsn(PUTSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME); mv.visitInsn(RETURN); mv.visitMaxs(4, 4); @@ -328,7 +330,7 @@ private static void createGetTargetMethodHandleMethod(ClassWriter cw, String cla mv = cw.visitMethod(ACC_PUBLIC, "getTargetMethodHandle", "()Ljava/lang/invoke/MethodHandle;", null, null); mv.visitCode(); - mv.visitFieldInsn(GETSTATIC, className, "TARGET", "Ljava/lang/invoke/MethodHandle;"); + mv.visitFieldInsn(GETSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME); mv.visitInsn(ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); 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 3879a080e33..ff9c66cc0d1 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -30,7 +30,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.UnaryOperator; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.groovy.util.SystemUtil; @@ -71,6 +70,13 @@ public class CacheableCallSite extends MutableCallSite { private static final float LOAD_FACTOR = 0.75f; private static final int INITIAL_CAPACITY = (int) Math.ceil(CACHE_SIZE / LOAD_FACTOR) + 1; private final MethodHandles.Lookup lookup; + final IndyInterface.CallType callType; + final boolean safe; + /** + * Indicates whether the invocation is a {@code this} call. + */ + final boolean thisCall; + final boolean spreadCall; /** * Stores the most recently accessed entry. *

@@ -90,6 +96,7 @@ public class CacheableCallSite extends MutableCallSite { */ @SuppressWarnings("java:S3077") private volatile MethodHandle picChain; + @SuppressWarnings("java:S3077") private volatile java.lang.invoke.SwitchPoint picSwitchPoint; /** @@ -125,11 +132,19 @@ protected boolean removeEldestEntry(Map.Entry eldest) { /** * Creates a cacheable call site for the supplied type and lookup context. * - * @param type the call-site type - * @param lookup the lookup used to un-reflect targets + * @param type the call-site type + * @param lookup the lookup used to un-reflect targets + * @param callType + * @param safe + * @param thisCall + * @param spreadCall */ - public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) { + CacheableCallSite(MethodType type, MethodHandles.Lookup lookup, IndyInterface.CallType callType, boolean safe, boolean thisCall, boolean spreadCall) { super(type); + this.callType = callType; + this.safe = safe; + this.thisCall = thisCall; + this.spreadCall = spreadCall; this.picKeys = new Object[IndyInterface.INDY_PIC_SIZE]; this.lookup = lookup; } @@ -453,10 +468,6 @@ public MethodHandles.Lookup getLookup() { return lookup; } - @Override public void setTarget(MethodHandle newTarget) { - super.setTarget(newTarget); - } - private static final ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>(); static { Thread cacheCleaner = new Thread(() -> { 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 50daf394d8b..b8ca57ebb93 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java @@ -20,16 +20,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import groovy.lang.GroovySystem; -import org.apache.groovy.util.SystemUtil; -import org.codehaus.groovy.GroovyBugError; -import org.codehaus.groovy.runtime.GeneratedClosure; - import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.invoke.MutableCallSite; import java.lang.invoke.SwitchPoint; import java.lang.reflect.Modifier; import java.util.Map; @@ -38,6 +33,9 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.groovy.util.SystemUtil; +import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.runtime.GeneratedClosure; /** * Bytecode level interface for bootstrap methods used by invokedynamic. @@ -66,7 +64,7 @@ public class IndyInterface { /** * Flags for method and property calls. */ - public static final int SAFE_NAVIGATION=1, THIS_CALL=2, GROOVY_OBJECT=4, IMPLICIT_THIS=8, SPREAD_CALL=16, UNCACHED_CALL=32; + public static final int SAFE_NAVIGATION=1, THIS_CALL=2, GROOVY_OBJECT=4, IMPLICIT_THIS=8, SPREAD_CALL=16; private static final MethodHandleWrapper NULL_METHOD_HANDLE_WRAPPER = MethodHandleWrapper.getNullMethodHandleWrapper(); @@ -191,7 +189,7 @@ public int getOrderNumber() { static { try { - MethodType mt = MethodType.methodType(MethodHandle.class, CacheableCallSite.class, Class.class, String.class, int.class, Boolean.class, Boolean.class, Boolean.class, Object.class, Object[].class); + MethodType mt = MethodType.methodType(MethodHandle.class, CacheableCallSite.class, Class.class, String.class, Object[].class); FROM_CACHE_HANDLE_METHOD = LOOKUP.findStatic(IndyInterface.class, "fromCacheHandle", mt); SELECT_METHOD_HANDLE_METHOD = LOOKUP.findStatic(IndyInterface.class, "selectMethodHandle", mt); @@ -250,7 +248,6 @@ public static CallSite bootstrap(final MethodHandles.Lookup caller, final String CallType ct = CallType.fromCallSiteName(callType); if (null == ct) throw new GroovyBugError("Unknown call type: " + callType); - int callID = ct.getOrderNumber(); boolean safe = (flags & SAFE_NAVIGATION) != 0; boolean thisCall = (flags & THIS_CALL ) != 0; boolean spreadCall = (flags & SPREAD_CALL ) != 0; @@ -258,7 +255,7 @@ public static CallSite bootstrap(final MethodHandles.Lookup caller, final String // first produce a dummy call site, since indy doesn't give the runtime types; // the site then changes to the target when INDY_OPTIMIZE_THRESHOLD is reached // that does the method selection including the direct call to the real method - var mc = new CacheableCallSite(type, caller); + var mc = new CacheableCallSite(type, caller, ct, safe, thisCall, spreadCall); Class sender = caller.lookupClass(); if (thisCall) { while (GeneratedClosure.class.isAssignableFrom(sender)) { @@ -266,10 +263,10 @@ public static CallSite bootstrap(final MethodHandles.Lookup caller, final String } } // make an adapter for method selection, i.e. get cached method handle (fast path) or fall back - MethodHandle mh = makeBootHandle(mc, sender, name, callID, type, safe, thisCall, spreadCall, FROM_CACHE_HANDLE_METHOD); + MethodHandle mh = makeBootHandle(mc, sender, name, FROM_CACHE_HANDLE_METHOD); mc.setTarget(mh); mc.setDefaultTarget(mh); - mc.setFallbackTarget(makeFallBack(mc, sender, name, callID, type, safe, thisCall, spreadCall)); + mc.setFallbackTarget(makeFallBack(mc, sender, name)); return mc; } @@ -277,24 +274,18 @@ public static CallSite bootstrap(final MethodHandles.Lookup caller, final String /** * Makes a fallback method for an invalidated method selection. */ - protected static MethodHandle makeFallBack(MutableCallSite mc, Class sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall) { - return makeBootHandle(mc, sender, name, callID, type, safeNavigation, thisCall, spreadCall, SELECT_METHOD_HANDLE_METHOD); + protected static MethodHandle makeFallBack(CacheableCallSite mc, Class sender, String name) { + return makeBootHandle(mc, sender, name, SELECT_METHOD_HANDLE_METHOD); } - private static MethodHandle makeBootHandle(MutableCallSite mc, Class sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall, MethodHandle fromCacheOrSelectMethod) { - final Object dummyReceiver = 1; + private static MethodHandle makeBootHandle(CacheableCallSite mc, Class sender, String name, MethodHandle fromCacheOrSelectMethod) { // Step 1: bind site-constant arguments MethodHandle boundHandle = MethodHandles.insertArguments( fromCacheOrSelectMethod, 0, // insert start index mc, sender, - name, - callID, - safeNavigation, - thisCall, - spreadCall, - dummyReceiver + name ); // boundHandle: (Object receiver, Object[] arguments) → MethodHandle @@ -306,7 +297,7 @@ private static MethodHandle makeBootHandle(MutableCallSite mc, Class sender, // bootHandle: (Object receiver, Object[] arguments) → Object // Step 3: adapt to call site type: collect all arguments into Object[] and then asType - bootHandle = bootHandle.asCollector(Object[].class, type.parameterCount()).asType(type); + bootHandle = bootHandle.asCollector(Object[].class, mc.type().parameterCount()).asType(mc.type()); return bootHandle; } @@ -315,11 +306,6 @@ private static class FallbackSupplier { private final CacheableCallSite callSite; private final Class sender; private final String methodName; - private final int callID; - private final Boolean safeNavigation; - private final Boolean thisCall; - private final Boolean spreadCall; - private final Object dummyReceiver; private final Object[] arguments; private MethodHandleWrapper result; @@ -329,22 +315,12 @@ private static class FallbackSupplier { * @param callSite the current call site * @param sender the sending class * @param methodName the method name - * @param callID the call-type id - * @param safeNavigation whether safe navigation is enabled - * @param thisCall whether the invocation is a {@code this} call - * @param spreadCall whether spread-call semantics are active - * @param dummyReceiver the synthetic receiver placeholder * @param arguments the invocation arguments */ - FallbackSupplier(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) { + FallbackSupplier(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { this.callSite = callSite; this.sender = sender; this.methodName = methodName; - this.callID = callID; - this.safeNavigation = safeNavigation; - this.thisCall = thisCall; - this.spreadCall = spreadCall; - this.dummyReceiver = dummyReceiver; this.arguments = arguments; } @@ -355,7 +331,7 @@ private static class FallbackSupplier { */ MethodHandleWrapper get() { if (null == result) { - result = fallback(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + result = fallback(callSite, sender, methodName, arguments); } return result; @@ -368,7 +344,7 @@ MethodHandleWrapper get() { */ @Deprecated public static Object fromCache(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { - MethodHandle mh = fromCacheHandle(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + MethodHandle mh = fromCacheHandle(callSite, sender, methodName, arguments); return mh.invokeExact(arguments); } @@ -383,7 +359,7 @@ protected Object computeValue(@NonNull Class type) { /** * Get the cached methodHandle. if the related methodHandle is not found in the inline cache, cache and return it. */ - private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { + private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) throws Throwable { Object receiver = arguments[0]; Object receiverKey = receiverCacheKey(receiver); @@ -391,12 +367,12 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class if (mhw != null && (mhw == NULL_METHOD_HANDLE_WRAPPER || mhw.getSwitchPoint() == switchPoint)) { mhw.incrementLatestHitCount(); if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { - optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); + optimizeCallSite(callSite, sender, methodName, arguments, receiverKey, mhw); } return mhw.getCachedMethodHandle(); } - FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, arguments); mhw = callSite.getAndPut(receiverKey, theKey -> { MethodHandleWrapper fallback = fallbackSupplier.get(); if (fallback.isCanSetTarget()) return fallback; @@ -420,21 +396,21 @@ private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class } if (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD && callSite.picInsertIfMissing(receiverKey)) { - optimizeCallSite(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments, receiverKey, mhw); + optimizeCallSite(callSite, sender, methodName, arguments, receiverKey, mhw); } } return mhw.getCachedMethodHandle(); } - private static void optimizeCallSite(CacheableCallSite callSite, Class sender, String methodName, int callID, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments, Object receiverKey, MethodHandleWrapper mhw) { + private static void optimizeCallSite(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments, Object receiverKey, MethodHandleWrapper mhw) { if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) { if (callSite.getTarget() != callSite.getDefaultTarget()) { callSite.setTarget(callSite.getDefaultTarget()); } } else { callSite.maybeUpdatePic(receiverKey, picChain -> { - Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); + Selector selector = Selector.getSelector(callSite, sender, methodName, arguments); selector.skipSwitchPoint = true; selector.fallback = picChain; selector.setCallSiteTarget(); @@ -454,15 +430,15 @@ private static void optimizeCallSite(CacheableCallSite callSite, Class sender */ @Deprecated public static Object selectMethod(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { - MethodHandle mh = selectMethodHandle(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + MethodHandle mh = selectMethodHandle(callSite, sender, methodName, arguments); return mh.invokeExact(arguments); } /** * Core method for indy method selection using runtime types. */ - private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { - MethodHandleWrapper mhw = fallback(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments); + private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) throws Throwable { + MethodHandleWrapper mhw = fallback(callSite, sender, methodName, arguments); MethodHandle defaultTarget = callSite.getDefaultTarget(); long fallbackCount = callSite.incrementFallbackCount(); @@ -491,8 +467,8 @@ static Object receiverCacheKey(Object receiver) { return receiver.getClass(); } - private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) { - Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); + private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { + Selector selector = Selector.getSelector(callSite, sender, methodName, arguments); selector.setCallSiteTarget(); return new MethodHandleWrapper( 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 fa1573f5ea2..8223be7d43e 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java @@ -142,15 +142,11 @@ public abstract class Selector { /** * Flags tracking safe navigation and spread-call semantics. */ - public boolean safeNavigation, safeNavigationOrig, spread; + public boolean safeNavOnNull; /** * Indicates whether spread-collector adaptation should be skipped. */ public boolean skipSpreadCollector; - /** - * Indicates whether the invocation is a {@code this} call. - */ - public boolean thisCall; /** * Class used as the selection base for metaclass lookups. */ @@ -167,10 +163,6 @@ public abstract class Selector { * Custom fallback method handle to use during re-linking or PIC building. */ public MethodHandle fallback; - /** - * Call-site category associated with this selector. - */ - public CallType callType; public static MethodHandle maybeWrapWithExceptionHandler(MethodHandle handle, boolean catchException) { if (handle == null || !catchException) return handle; @@ -183,24 +175,17 @@ public static MethodHandle maybeWrapWithExceptionHandler(MethodHandle handle, bo } } - /** - * Cache values for read-only access - */ - private static final CallType[] CALL_TYPE_VALUES = CallType.values(); - /** * Returns a Selector or throws a GroovyBugError. */ - public static Selector getSelector(CacheableCallSite callSite, Class sender, String methodName, int callID, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments) { - CallType callType = CALL_TYPE_VALUES[callID]; - return switch (callType) { - case INIT -> new InitSelector(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); - case METHOD -> new MethodSelector(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); - case GET -> new PropertySelector(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); + public static Selector getSelector(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { + return switch (callSite.callType) { + case INIT -> new InitSelector(callSite, sender, methodName, arguments); + case METHOD -> new MethodSelector(callSite, sender, methodName, arguments); + case GET -> new PropertySelector(callSite, sender, methodName, arguments); case SET -> throw new GroovyBugError("your call tried to do a property set, which is not supported."); case CAST -> new CastSelector(callSite, sender, methodName, arguments); - case INTERFACE -> new InterfaceSelector(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); - default -> throw new GroovyBugError("unexpected call type"); + case INTERFACE -> new InterfaceSelector(callSite, sender, methodName, arguments); }; } @@ -237,7 +222,7 @@ private static class CastSelector extends MethodSelector { * @param args the invocation arguments */ CastSelector(final CacheableCallSite callSite, final Class sender, final String spec, final Object[] args) { - super(callSite, sender, spec, CallType.CAST, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, args); + super(callSite, sender, spec, args); this.staticSourceType = callSite.type().parameterType(0); this.staticTargetType = callSite.type().returnType(); } @@ -356,14 +341,10 @@ private static class PropertySelector extends MethodSelector { * @param callSite the call site being linked * @param sender the sending class * @param propertyName the property name - * @param callType the call-site category - * @param safeNavigation whether safe navigation is enabled - * @param thisCall whether the invocation is a {@code this} call - * @param spreadCall whether spread-call semantics are active * @param arguments the invocation arguments */ - public PropertySelector(CacheableCallSite callSite, Class sender, String propertyName, CallType callType, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments) { - super(callSite, sender, propertyName, callType, safeNavigation, thisCall, spreadCall, arguments); + public PropertySelector(CacheableCallSite callSite, Class sender, String propertyName, Object[] arguments) { + super(callSite, sender, propertyName, arguments); } /** @@ -410,15 +391,15 @@ public void chooseMeta(MetaClassImpl mci) { if (LOG_ENABLED) LOG.info("selectionBase set to " + selectionBase); var mp = mci.getEffectiveGetMetaProperty(selectionBase, receiver, name, false); - if (mp instanceof MethodMetaProperty) { - method = ((MethodMetaProperty) mp).getMetaMethod(); + if (mp instanceof MethodMetaProperty mmp) { + method = mmp.getMetaMethod(); insertName = true; // pass "name" field as argument - } else if (mp instanceof CachedField && !mp.isStatic()) { + } else if (mp instanceof CachedField cf && !mp.isStatic()) { try { // GROOVY-9144, GROOVY-9596: get lookup for sender and unreflect before forcing access @SuppressWarnings("removal") MethodHandles.Lookup lookup = ((Java8) VMPluginFactory.getPlugin()).newLookup(sender); - handle = ((CachedField) mp).asAccessMethod(lookup); + handle = cf.asAccessMethod(lookup); } catch (IllegalAccessException e) { throw new GroovyBugError(e); } @@ -470,14 +451,10 @@ private static class InitSelector extends MethodSelector { * @param callSite the call site being linked * @param sender the sending class * @param methodName the constructor pseudo-name - * @param callType the call-site category - * @param safeNavigation whether safe navigation is enabled - * @param thisCall whether the invocation is a {@code this} call - * @param spreadCall whether spread-call semantics are active * @param arguments the invocation arguments */ - public InitSelector(CacheableCallSite callSite, Class sender, String methodName, CallType callType, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments) { - super(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); + public InitSelector(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { + super(callSite, sender, methodName, arguments); } /** @@ -596,14 +573,10 @@ private static class InterfaceSelector extends MethodSelector { * @param callSite the call site being linked * @param sender the sending class * @param methodName the method name - * @param callType the call-site category - * @param safeNavigation whether safe navigation is enabled - * @param thisCall whether the invocation is a {@code this} call - * @param spreadCall whether spread-call semantics are active * @param arguments the invocation arguments */ - public InterfaceSelector(CacheableCallSite callSite, Class sender, String methodName, CallType callType, boolean safeNavigation, boolean thisCall, boolean spreadCall, Object[] arguments) { - super(callSite, sender, methodName, callType, safeNavigation, thisCall, spreadCall, arguments); + public InterfaceSelector(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { + super(callSite, sender, methodName, arguments); } /** @@ -660,36 +633,28 @@ private static class MethodSelector extends Selector { * @param callSite the call site being linked * @param sender the sending class * @param methodName the method name - * @param callType the call-site category - * @param safeNavigation whether safe navigation is enabled - * @param thisCall whether the invocation is a {@code this} call - * @param spreadCall whether spread-call semantics are active * @param arguments the invocation arguments */ - public MethodSelector(CacheableCallSite callSite, Class sender, String methodName, CallType callType, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object[] arguments) { - this.callType = callType; + public MethodSelector(CacheableCallSite callSite, Class sender, String methodName, Object[] arguments) { this.targetType = callSite.type(); this.name = methodName; this.originalArguments = arguments; - this.args = spread(arguments, spreadCall); + this.args = spread(arguments, callSite.spreadCall); this.callSite = callSite; this.sender = sender; - this.safeNavigationOrig = safeNavigation; - this.safeNavigation = safeNavigation && arguments[0] == null; - this.thisCall = thisCall; - this.spread = spreadCall; - this.cache = !spreadCall; + this.safeNavOnNull = callSite.safe && arguments[0] == null; + this.cache = !callSite.spreadCall; if (LOG_ENABLED) { StringBuilder msg = new StringBuilder("----------------------------------------------------" + "\n\t\tinvocation of method '" + methodName + "'" + - "\n\t\tinvocation type: " + callType + + "\n\t\tinvocation type: " + callSite.callType + "\n\t\tsender: " + sender + "\n\t\ttargetType: " + targetType + - "\n\t\tsafe navigation: " + safeNavigation + - "\n\t\tthisCall: " + thisCall + - "\n\t\tspreadCall: " + spreadCall + + "\n\t\tsafe navigation: " + safeNavOnNull + + "\n\t\tthisCall: " + callSite.thisCall + + "\n\t\tspreadCall: " + callSite.spreadCall + "\n\t\twith " + arguments.length + " arguments"); for (int i = 0; i < arguments.length; i++) { msg.append("\n\t\t\targument[").append(i).append("] = "); @@ -710,7 +675,7 @@ public MethodSelector(CacheableCallSite callSite, Class sender, String method * return the constant. */ public boolean setNullForSafeNavigation() { - if (!safeNavigation) return false; + if (!safeNavOnNull) return false; handle = MethodHandles.dropArguments(NULL_REF, 0, targetType.parameterArray()); if (LOG_ENABLED) LOG.info("set null returning handle for safe navigation"); return true; @@ -838,7 +803,7 @@ public void setHandleForMetaMethod() { // generic meta method invocation path handle = META_METHOD_INVOKER; handle = handle.bindTo(metaMethod); - if (spread) { + if (callSite.spreadCall) { args = originalArguments; skipSpreadCollector = true; } else { @@ -861,10 +826,6 @@ private boolean invokesMOPMethod(String name, MethodHandle handle) { if (name.equals("getProperty")) { if (handle.type().parameterCount() != 2) return false; return handle.type().parameterType(1) == String.class; - /*if (handle.type().parameterType(1) != String.class) { - throw new GroovyRuntimeException("getProperty method had parameter type " + handle.type() + " on " + declaringClass.getTheClass() + " is a GroovyObject " + declaringClass.isAssignableFrom(GroovyObject.class)); - } - //return true;*/ } if (name.equals("setProperty")) { if (handle.type().parameterCount() != 3) return false; @@ -940,7 +901,7 @@ public void setMetaClassCallHandleIfNeeded(boolean standardMetaClass) { } } handle = MethodHandles.insertArguments(handle, 1, name); - if (!spread) handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1); + if (!callSite.spreadCall) handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1); if (LOG_ENABLED) LOG.info("bind method name and create collector for arguments"); } @@ -975,7 +936,7 @@ public void correctParameterLength() { Class[] params = handle.type().parameterArray(); if (currentType != null) params = currentType.parameterArray(); if (!isVargs) { - if (!(spread && useMetaClass) && params.length == 2 && args.length == 1) { + if (!(callSite.spreadCall && useMetaClass) && params.length == 2 && args.length == 1) { handle = MethodHandles.insertArguments(handle, 1, SINGLE_NULL_ARRAY); } return; @@ -1075,7 +1036,7 @@ public void correctNullReceiver() { * Adapts the handle for spread-call argument collection when needed. */ public void correctSpreading() { - if (spread && !useMetaClass && !skipSpreadCollector) { + if (callSite.spreadCall && !useMetaClass && !skipSpreadCollector) { handle = handle.asSpreader(Object[].class, args.length - 1); } } @@ -1165,7 +1126,7 @@ public void setGuards(Object receiver) { .asType(MethodType.methodType(boolean.class, pt)); handle = MethodHandles.guardWithTest(test, handle, fallback); if (LOG_ENABLED) LOG.info("added same-class argument check"); - } else if (safeNavigationOrig) { // GROOVY-11126 + } else if (callSite.safe) { // GROOVY-11126 MethodHandle test = NON_NULL.asType(MethodType.methodType(boolean.class, pt[0])); handle = MethodHandles.guardWithTest(test, handle, fallback); if (LOG_ENABLED) LOG.info("added null receiver check"); @@ -1186,7 +1147,7 @@ public void doCallSiteTargetSet() { */ public void setSelectionBase() { Class sender = getThisType(this.sender); - if (thisCall || sender.isInstance(args[0])) { // GROOVY-2433 + if (callSite.thisCall || sender.isInstance(args[0])) { // GROOVY-2433 selectionBase = sender; } else { selectionBase = mc.getTheClass(); @@ -1218,7 +1179,7 @@ public void setCallSiteTarget() { if (!setNullForSafeNavigation() && !setInterceptor()) { getMetaClass(); setSelectionBase(); - MetaClassImpl mci = getMetaClassImpl(mc, callType != CallType.GET); + MetaClassImpl mci = getMetaClassImpl(mc, callSite.callType != CallType.GET); chooseMeta(mci); setHandleForMetaMethod(); setMetaClassCallHandleIfNeeded(mci != null); diff --git a/src/test/groovy/groovy/VArgsTest.groovy b/src/test/groovy/groovy/VArgsTest.groovy index 6250af10242..38bf367eb79 100644 --- a/src/test/groovy/groovy/VArgsTest.groovy +++ b/src/test/groovy/groovy/VArgsTest.groovy @@ -34,6 +34,7 @@ final class VArgsTest { assert intMethod(1,1) == 2 assert intMethod(1,1,1) == 13 assert intMethod([1,2,2,2] as int[]) == 14 + assert intMethod(*[1,2]) == 2 } def doubleMethod(double[] doubles) {20+doubles.length} @@ -46,6 +47,7 @@ final class VArgsTest { assert doubleMethod(1.0G,1.0G) == 22 assert doubleMethod(1.0G,1.0G,1.0G) == 23 assert doubleMethod([1,2,2,2] as BigDecimal[]) == 24 + assert doubleMethod(*[1.0G, 2.0G]) == 22 // with double assert doubleMethod() == 20 @@ -53,6 +55,7 @@ final class VArgsTest { assert doubleMethod(1.0d,1.0d) == 22 assert doubleMethod(1.0d,1.0d,1.0d) == 23 assert doubleMethod([1,2,2,2] as double[]) == 24 + assert doubleMethod(*[1.0d, 2.0d]) == 22 } // test vargs with one fixed argument for primitives @@ -65,12 +68,18 @@ final class VArgsTest { assert doubleMethod2(1.0G,1.0G) == 32 assert doubleMethod2(1.0G,1.0G,1.0G) == 33 assert doubleMethod2(1.0G, [1,2,2,2] as BigDecimal[]) == 35 + assert doubleMethod2(1.0G, *[1.0G]) == 32 + assert doubleMethod2(*[1.0G, 1.0G]) == 32 + assert doubleMethod2(*[1.0G]) == 31 // with double assert doubleMethod2(1.0d) == 31 assert doubleMethod2(1.0d,1.0d) == 32 assert doubleMethod2(1.0d,1.0d,1.0d) == 33 assert doubleMethod2(1.0d,[1,2,2,2] as double[]) == 35 + assert doubleMethod2(1.0d,*[1.0d]) == 32 + assert doubleMethod2(*[1.0d, 1.0d]) == 32 + assert doubleMethod2(*[1.0d]) == 31 } def objectMethod() {0} @@ -85,6 +94,7 @@ final class VArgsTest { assert objectMethod(1,1) == 2 assert objectMethod(1,1,1) == 13 assert objectMethod([1,2,2,2] as Object[]) == 14 + assert objectMethod(*[1,2,2,2]) == 14 } @Test @@ -103,6 +113,7 @@ final class VArgsTest { assert gstringMethod(gstring) == 1 assert gstringMethod(gstring,gstring,gstring) == 3 assert gstringMethod([gstring] as GString[]) == 1 + assert gstringMethod(*[gstring]) == 1 } def stringMethod(String[] strings) {strings.length} @@ -115,6 +126,7 @@ final class VArgsTest { assert stringMethod(gstring) == 1 assert stringMethod(gstring,gstring,gstring) == 3 assert stringMethod([gstring] as GString[]) == 1 + assert stringMethod(*[gstring]) == 1 assert stringMethod() == 0 assert stringMethod("a") == 1 assert stringMethod("a","a","a") == 3 @@ -129,6 +141,9 @@ final class VArgsTest { @Test void testOverloadedMethod1() { assert overloadedMethod1() == 2 + assert overloadedMethod1(*[]) == 2 + assert overloadedMethod1("s") == 1 + assert overloadedMethod1(*["s"]) == 1 } def overloadedMethod2(x,y) {1} @@ -137,7 +152,10 @@ final class VArgsTest { @Test void testOverloadedMethod2() { assert overloadedMethod2(null) == 2 + assert overloadedMethod2(*[1]) == 2 assert overloadedMethod2("foo") == 2 + assert overloadedMethod2(1,2) == 1 + assert overloadedMethod2(*[1,2]) == 1 } def normalVargsMethod(Object[] a) {a.length} diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy index 8892c40b1d1..f397cb0364a 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyClassLoaderLeakTest.groovy @@ -18,8 +18,12 @@ */ package org.codehaus.groovy.vmplugin.v8 +import org.codehaus.groovy.reflection.CachedMethod import org.junit.jupiter.api.Test + +import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType + import static org.junit.jupiter.api.Assertions.* final class IndyClassLoaderLeakTest { @@ -27,7 +31,7 @@ final class IndyClassLoaderLeakTest { @Test void testMruLeakAwareness() { MethodType type = MethodType.methodType(Object, Object) - CacheableCallSite callSite = new CacheableCallSite(type, java.lang.invoke.MethodHandles.lookup()) + CacheableCallSite callSite = new CacheableCallSite(type, MethodHandles.lookup(), IndyInterface.CallType.METHOD, false, false, false) // 1. Same ClassLoader (Safe) def sameLoaderObj = new Object() @@ -70,9 +74,9 @@ final class IndyClassLoaderLeakTest { private static void updateMRU(CacheableCallSite callSite, Object key, Class targetClass, Class sender) { // We use a dummy wrapper for testing def wrapper = new MethodHandleWrapper( - java.lang.invoke.MethodHandles.constant(Object, "test"), - java.lang.invoke.MethodHandles.constant(Object, "test"), - new org.codehaus.groovy.reflection.CachedMethod(targetClass.getDeclaredMethods().length > 0 ? targetClass.getDeclaredMethods()[0] : Object.class.getMethod("toString")), + MethodHandles.constant(Object, "test"), + MethodHandles.constant(Object, "test"), + new CachedMethod(targetClass.getDeclaredMethods().length > 0 ? targetClass.getDeclaredMethods()[0] : Object.class.getMethod("toString")), IndyInterface.switchPoint, true ) diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy index d265f70a27d..3be292dc680 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceCallSiteTargetTest.groovy @@ -72,41 +72,28 @@ final class IndyInterfaceCallSiteTargetTest { CacheableCallSite callSite = newCallSite(type) Object[] args = [IndyInterfaceCallSiteTargetTest] as Object[] - Object result = IndyInterface.fromCache( - callSite, - IndyInterfaceCallSiteTargetTest, - 'staticFoo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, - Boolean.FALSE, - Boolean.FALSE, - 1, - args + MethodHandle methodHandle = IndyInterface.fromCacheHandle( + callSite, IndyInterfaceCallSiteTargetTest, 'staticFoo', args ) - assertEquals(staticFoo(), result) + // fromCacheHandle returns a MethodHandle, not the result value + assertEquals(MethodType.methodType(Object, Object[]), methodHandle.type()) assertNotSame(callSite.defaultTarget, callSite.target) } @Test void testFromCacheHandleKeepsDefaultTargetForSpreadCall() { MethodType type = MethodType.methodType(Object, Class, Object[]) - CacheableCallSite callSite = newCallSite(type) + CacheableCallSite callSite = newCallSite(type, spreadCall:true) Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[] MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, - IndyInterfaceCallSiteTargetTest, - 'staticEcho', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, - Boolean.FALSE, - Boolean.TRUE, - 1, - args + callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho', args ) - assertEquals(staticEcho('bar'), methodHandle.invokeWithArguments([args] as Object[])) + // Cannot invoke MethodHandle methods from Groovy (JDK 17+ blocks unreflect on MethodHandle). + // Instead verify the handle type and the call site state. + assertEquals(MethodType.methodType(Object, Object[]), methodHandle.type()) assertSame(callSite.defaultTarget, callSite.target) } @@ -125,9 +112,7 @@ final class IndyInterfaceCallSiteTargetTest { primeLatestHitCount(callSite, receiver, wrapper, IndyInterface.INDY_OPTIMIZE_THRESHOLD) MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'foo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'foo', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -162,9 +147,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, receiver, wrapper) MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'foo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'foo', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -174,13 +157,11 @@ final class IndyInterfaceCallSiteTargetTest { @Test void testFromCacheHandleReturnsNullHandleForSafeNavigationReceiver() { MethodType type = MethodType.methodType(Object, Object) - CacheableCallSite callSite = newCallSite(type) + CacheableCallSite callSite = newCallSite(type, safe:true) Object[] args = [null] as Object[] MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'foo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.TRUE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'foo', args ) assertEquals(null, methodHandle.invokeWithArguments([args] as Object[])) @@ -200,9 +181,7 @@ final class IndyInterfaceCallSiteTargetTest { try { 2.times { MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, PerInstanceMetaClassStaticTarget, 'ping', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, PerInstanceMetaClassStaticTarget, 'ping', args ) assertEquals(PerInstanceMetaClassStaticTarget.ping(), methodHandle.invokeWithArguments([args] as Object[])) @@ -228,9 +207,7 @@ final class IndyInterfaceCallSiteTargetTest { callSite.target = wrapper.targetMethodHandle MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'foo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'foo', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -250,9 +227,7 @@ final class IndyInterfaceCallSiteTargetTest { cacheWrapper(callSite, ClassA, wrapper) MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, ClassA, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, ClassA, 'bar', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -266,17 +241,13 @@ final class IndyInterfaceCallSiteTargetTest { Object[] argsA = [ClassA] as Object[] MethodHandle handleA = IndyInterface.fromCacheHandle( - callSite, ClassA, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA + callSite, ClassA, 'bar', argsA ) assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[])) Object[] argsB = [ClassB] as Object[] MethodHandle handleB = IndyInterface.fromCacheHandle( - callSite, ClassB, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB + callSite, ClassB, 'bar', argsB ) assertEquals(ClassB.bar(), handleB.invokeWithArguments([argsB] as Object[])) } @@ -294,9 +265,7 @@ final class IndyInterfaceCallSiteTargetTest { Object[] args = [ClassA] as Object[] MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, ClassA, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, ClassA, 'bar', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -313,9 +282,7 @@ final class IndyInterfaceCallSiteTargetTest { Object[] args = [ClassA] as Object[] MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, ClassA, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, ClassA, 'bar', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) @@ -330,9 +297,7 @@ final class IndyInterfaceCallSiteTargetTest { Object[] args = [receiver, 'abc'] as Object[] MethodHandle selectedHandle = IndyInterface.selectMethodHandle( - callSite, InstanceStaticCallTarget, 'valueOf', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, InstanceStaticCallTarget, 'valueOf', args ) assertEquals(InstanceStaticCallTarget.valueOf('abc'), selectedHandle.invokeWithArguments([args] as Object[])) @@ -341,9 +306,7 @@ final class IndyInterfaceCallSiteTargetTest { assertSame(callSite.defaultTarget, callSite.target) MethodHandle cachedHandle = IndyInterface.fromCacheHandle( - callSite, InstanceStaticCallTarget, 'valueOf', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, args + callSite, InstanceStaticCallTarget, 'valueOf', args ) assertSame(cachedWrapper.cachedMethodHandle, cachedHandle) @@ -358,17 +321,13 @@ final class IndyInterfaceCallSiteTargetTest { Object[] argsA = [ClassA] as Object[] MethodHandle handleA = IndyInterface.selectMethodHandle( - callSite, ClassA, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsA + callSite, ClassA, 'bar', argsA ) assertEquals(ClassA.bar(), handleA.invokeWithArguments([argsA] as Object[])) Object[] argsB = [ClassB] as Object[] MethodHandle handleB = IndyInterface.selectMethodHandle( - callSite, ClassB, 'bar', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, 1, argsB + callSite, ClassB, 'bar', argsB ) assertEquals(ClassB.bar(), handleB.invokeWithArguments([argsB] as Object[])) @@ -383,22 +342,26 @@ final class IndyInterfaceCallSiteTargetTest { @Test void testSelectMethodHandleStoresSentinelForUncacheableSpreadCall() { MethodType type = MethodType.methodType(Object, Class, Object[]) - CacheableCallSite callSite = newCallSite(type) + CacheableCallSite callSite = newCallSite(type, spreadCall:true) Object[] args = [IndyInterfaceCallSiteTargetTest, ['bar'] as Object[]] as Object[] MethodHandle methodHandle = IndyInterface.selectMethodHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'staticEcho', args ) - assertEquals(staticEcho('bar'), methodHandle.invokeWithArguments([args] as Object[])) + // Cannot invoke MethodHandle methods from Groovy (JDK 17+ blocks unreflect on MethodHandle). + // Instead verify the handle type and the call site state. + assertEquals(MethodType.methodType(Object, Object[]), methodHandle.type()) assertSame(MethodHandleWrapper.getNullMethodHandleWrapper(), requireCachedWrapper(callSite, IndyInterfaceCallSiteTargetTest)) assertSame(callSite.defaultTarget, callSite.target) } - private static CacheableCallSite newCallSite(MethodType type) { - CacheableCallSite callSite = new CacheableCallSite(type, MethodHandles.lookup()) + private static CacheableCallSite newCallSite(Map options=[:], MethodType type) { + CacheableCallSite callSite = new CacheableCallSite(type, MethodHandles.lookup(), + IndyInterface.CallType.METHOD, + options.getOrDefault("safe", false), + options.getOrDefault("thisCall", false), + options.getOrDefault("spreadCall", false)) MethodHandle dummyTarget = targetHandle(type, null) callSite.target = dummyTarget callSite.defaultTarget = dummyTarget @@ -446,9 +409,7 @@ final class IndyInterfaceCallSiteTargetTest { } MethodHandle methodHandle = IndyInterface.fromCacheHandle( - callSite, IndyInterfaceCallSiteTargetTest, 'foo', - IndyInterface.CallType.METHOD.getOrderNumber(), - Boolean.FALSE, Boolean.TRUE, Boolean.FALSE, 1, args + callSite, IndyInterfaceCallSiteTargetTest, 'foo', args ) assertSame(wrapper.cachedMethodHandle, methodHandle) diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy index d7a50dec726..0c647effb97 100644 --- a/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/vmplugin/v8/IndyInterfaceDeprecatedTest.groovy @@ -36,7 +36,7 @@ final class IndyInterfaceDeprecatedTest { // Prepare call site type: (IndyInterfaceDeprecatedTest) -> Object MethodHandles.Lookup lookup = MethodHandles.lookup() MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest) - CacheableCallSite callSite = new CacheableCallSite(type, lookup) + CacheableCallSite callSite = new CacheableCallSite(type, lookup, IndyInterface.CallType.METHOD, false, true, false) // Provide non-null default/fallback targets (needed for guards in Selector) def dummyTarget = MethodHandles.dropArguments( @@ -72,7 +72,7 @@ final class IndyInterfaceDeprecatedTest { // Prepare call site type: (IndyInterfaceDeprecatedTest) -> Object MethodHandles.Lookup lookup = MethodHandles.lookup() MethodType type = MethodType.methodType(Object, IndyInterfaceDeprecatedTest) - CacheableCallSite callSite = new CacheableCallSite(type, lookup) + CacheableCallSite callSite = new CacheableCallSite(type, lookup, IndyInterface.CallType.METHOD, false, true, false) // Provide non-null default/fallback targets (needed for guards in Selector) def dummyTarget = MethodHandles.dropArguments(