This is an automated email from the ASF dual-hosted git repository. blackdrag pushed a commit to branch feature/Cache_changes in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 539e4a47a201ac7e7986f01fe05985edb9d092d9 Author: Jochen Theodorou <[email protected]> AuthorDate: Wed May 20 10:41:44 2026 +0200 GROOVY-12023: add tests to ensure PIC functionality and fix one existing test since the fallback logic changed --- .../vmplugin/v8/IndyClassLoaderLeakTest.groovy | 63 +++++++++++++++++ .../v8/IndyInterfaceCallSiteTargetTest.groovy | 78 ++++++---------------- .../groovy/vmplugin/v8/IndyInterfacePicTest.groovy | 72 ++++++++++++++++++++ 3 files changed, 157 insertions(+), 56 deletions(-) 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 0000000000..35c81eff3d --- /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 9f9226dee2..b9a760cd63 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) + [email protected](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 0000000000..1f6715259d --- /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()) + } + +}
