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 254c08cacfd4fe0ddfffc55416268e34614cc764
Author: Jochen Theodorou <[email protected]>
AuthorDate: Wed May 20 02:10:02 2026 +0200

    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 ++++++++++++++++------
 .../codehaus/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 b6a9a4622c..ff57d8b979 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<MethodHandleWrapper> 
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<String, SoftReference<MethodHandleWrapper>> lruCache =
-            new LinkedHashMap<String, 
SoftReference<MethodHandleWrapper>>(INITIAL_CAPACITY, LOAD_FACTOR, true) {
+    private final Map<Object, SoftReference<MethodHandleWrapper>> lruCache =
+            new LinkedHashMap<Object, 
SoftReference<MethodHandleWrapper>>(INITIAL_CAPACITY, LOAD_FACTOR, true) {
                 @Serial private static final long serialVersionUID = 
7785958879964294463L;
 
                 /**
@@ -78,54 +78,85 @@ public class CacheableCallSite extends MutableCallSite {
         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<? super String, ? extends MethodHandleWrapper> 
valueProvider) {
+    public MethodHandleWrapper getAndPut(Object key, 
MemoizeCache.ValueProvider<? super Object, ? extends MethodHandleWrapper> 
valueProvider, Class<?> sender) {
         MethodHandleWrapper result = null;
         SoftReference<MethodHandleWrapper> 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<MethodHandleWrapper> 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<MethodHandleWrapper> 
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 class CacheableCallSite extends MutableCallSite {
         }
     }
 
+    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 c088992337..91ef98d680 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 class IndyInterface {
         return mh.invokeExact(arguments);
     }
 
+    private static final Object NULL_KEY = new Object();
+    private static final ClassValue<Object> STATIC_KEYS = new 
ClassValue<Object>() {
+        @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 @@ public class IndyInterface {
             }
 
             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 @@ public class IndyInterface {
 
     /**
      * 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 2a499a222a..9f9226dee2 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
     }

Reply via email to