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())
+    }
+
+}

Reply via email to