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/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 b6a9a4622c2..191ec1ca718 100644 --- a/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java +++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java @@ -18,41 +18,95 @@ */ 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.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; 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 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 */ 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; private final MethodHandles.Lookup lookup; - private volatile SoftReference latestHitMethodHandleWrapperSoftReference = null; + /** + * 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; - private final Map> lruCache = - new LinkedHashMap>(INITIAL_CAPACITY, LOAD_FACTOR, true) { + /** + * 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. + */ + @SuppressWarnings("java:S3077") + private volatile MethodHandle picChain; + + /** + * 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; /** @@ -71,74 +125,214 @@ 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); + this.picKeys = new Object[IndyInterface.INDY_PIC_SIZE]; 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 + */ + 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. + *

+ * 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 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) { + 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) { + lruCache.remove(key); + } } if (null == result) { - result = valueProvider.provide(className); - resultSoftReference = new SoftReference<>(result); - lruCache.put(className, resultSoftReference); + result = valueProvider.provide(key); + resultSoftReference = new SoftReferenceWithKey(key, result, REFERENCE_QUEUE, lruCache); + 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. + *

+ * Concurrency: Synchronizes on {@link #lruCache} for thread-safe access to the underlying map. * - * @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) { + MethodHandleWrapper put(Object key, MethodHandleWrapper mhw) { synchronized (lruCache) { - final SoftReference methodHandleWrapperSoftReference; - methodHandleWrapperSoftReference = lruCache.put(name, 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; } } - private void removeAllStaleEntriesOfLruCache() { - CACHE_CLEANER_QUEUE.offer(() -> { - synchronized (lruCache) { - lruCache.values().removeIf(v -> null == v.get()); + /** + * 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; + } + } + } + } + + /** + * Checks if a receiver shape is already present in the PIC. + *

+ * 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 boolean picInsertIfMissing(Object key) { + int count = picCount; + for (int i = 0; i < count; i++) { + if (picKeys[i] == key) return false; + } + return true; + } + + public MethodHandle getPicChain() { + return picChain; + } + + public int getPicCount() { + return picCount; + } + + @javax.annotation.concurrent.Immutable + private static final class MRUEntry { + final Object key; + final MethodHandleWrapper wrapper; + MRUEntry(Object key, MethodHandleWrapper wrapper) { + this.key = key; + this.wrapper = wrapper; + } + } + + 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; } /** @@ -152,10 +346,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; + Arrays.fill(picKeys, null); } /** @@ -212,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 c0889923370..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; @@ -40,15 +41,27 @@ /** * 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); 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); + static final int INDY_PIC_SIZE = SystemUtil.getIntegerSafe("groovy.indy.pic.size", 4); /** * Flags for method and property calls. @@ -56,7 +69,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. @@ -190,8 +202,12 @@ 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(); + @SuppressWarnings("java:S3077") + protected static volatile SwitchPoint switchPoint = new SwitchPoint(); static { GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints()); @@ -199,6 +215,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) { @@ -352,18 +372,36 @@ 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(@NonNull 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()) && 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 -> { 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"; @@ -380,26 +418,35 @@ 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(); + 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, 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.skipSwitchPoint = true; + selector.fallback = picChain; + selector.setCallSiteTarget(); + // wrap with top-level SwitchPoint guard + MethodHandle target = switchPoint.guardWithTest(selector.handle, callSite.getDefaultTarget()); + callSite.setTarget(target); + if (LOG_ENABLED) LOG.info("call site target updated with PIC link, pic size: " + callSite.getPicCount()); + return selector.handle; + }); + } + mhw.resetLatestHitCount(); + } + /** * Core method for indy method selection using runtime types. * @deprecated Use the new bootHandle-based approach instead. @@ -418,10 +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)) { - callSite.setTarget(defaultTarget); + if (callSite.tryResetToDefaultTarget(defaultTarget, INDY_FALLBACK_THRESHOLD, fallbackCount)) { if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation"); - callSite.resetFallbackCount(); } if (callSite.getTarget() == defaultTarget) { @@ -438,14 +483,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/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/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()) 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; 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 2a499a222aa..b2f5f80e4bf 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) } @@ -143,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) @@ -153,7 +161,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 +177,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 +199,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 +227,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 +249,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 +265,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 +273,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 +293,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 +312,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 +329,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 +340,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 +357,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 +365,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 +386,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 @@ -411,12 +419,13 @@ 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 })) - latestHitCountField().get(wrapper).set(value) + assertSame(wrapper, callSite.getAndPut(IndyInterface.receiverCacheKey(receiver), { wrapper }, IndyInterfaceCallSiteTargetTest)) + wrapper.resetLatestHitCount() + wrapper.addLatestHitCount(value) } private static void assertFallbackCutoffLeavesDefaultTarget(boolean startAwayFromDefaultTarget) { @@ -430,13 +439,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 @@ -444,15 +453,15 @@ 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) { 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 } @@ -463,37 +472,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()) + } + +}