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 }
