Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
613 changes: 613 additions & 0 deletions gradle/verification-metadata.xml

Large diffs are not rendered by default.

294 changes: 250 additions & 44 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/CacheableCallSite.java

Large diffs are not rendered by default.

110 changes: 76 additions & 34 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,23 +41,34 @@

/**
* 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.
* <p>
* 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}.
* <p>
* <b>Optimization Lifecycle:</b>
* <ol>
* <li><b>Bootstrap:</b> The JVM calls one of the bootstrap methods (e.g., {@code bootstrap}) when an {@code invokedynamic} instruction is first encountered.</li>
* <li><b>Initial Linkage:</b> The call site is initialized with a fallback target (adapter pointing to {@link #fromCacheHandle}).</li>
* <li><b>Execution & Selection:</b> On first execution, {@code fromCacheHandle} uses a {@link Selector} to find the target method and create a guarded {@link java.lang.invoke.MethodHandle}.</li>
* <li><b>Promotion & PIC:</b> 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.</li>
* </ol>
* <p>
* 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.
*/
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.
Expand Down Expand Up @@ -190,15 +202,23 @@

/**
* Shared switch point invalidated when metaclass state changes.
* <p>
* <b>Concurrency:</b> {@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());
}

/**
* Callback for constant metaclass update change
* <p>
* <b>Concurrency:</b> 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) {
Expand Down Expand Up @@ -352,18 +372,36 @@
return mh.invokeExact(arguments);
}

private static final Object NULL_KEY = new Object();
private static final ClassValue<Object> 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";
Expand All @@ -380,26 +418,35 @@
}
}

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) {

Check warning on line 429 in src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Method has 10 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ5JkRRL7b0f9vdM1ub_&open=AZ5JkRRL7b0f9vdM1ub_&pullRequest=2549
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.
Expand All @@ -418,10 +465,8 @@

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");

Check warning on line 469 in src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this if statement with the enclosing one.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ5JkRRL7b0f9vdM1ub-&open=AZ5JkRRL7b0f9vdM1ub-&pullRequest=2549
callSite.resetFallbackCount();
}

if (callSite.getTarget() == defaultTarget) {
Expand All @@ -438,14 +483,11 @@

/**
* 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -108,7 +115,7 @@ public void resetLatestHitCount() {
* @return the current latest-hit counter
*/
public long getLatestHitCount() {
return latestHitCount.get();
return latestHitCount.sum();
}

/**
Expand Down
40 changes: 26 additions & 14 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,30 @@
* 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;

Check warning on line 167 in src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make skipSwitchPoint a static final constant or non-public and provide accessors if needed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ5JkRQz7b0f9vdM1ub8&open=AZ5JkRQz7b0f9vdM1ub8&pullRequest=2549
/**
* Custom fallback method handle to use during re-linking or PIC building.
*/
public MethodHandle fallback;

Check warning on line 171 in src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make fallback a static final constant or non-public and provide accessors if needed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ5JkRQz7b0f9vdM1ub9&open=AZ5JkRQz7b0f9vdM1ub9&pullRequest=2549
/**
* 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
*/
Expand Down Expand Up @@ -1033,17 +1052,8 @@
* 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");
}

/**
Expand All @@ -1052,7 +1062,7 @@
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) {
Expand Down Expand Up @@ -1087,8 +1097,10 @@
}

// 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<Class<?>> nonFinalOrNullUnsafe = (t) -> {
return !Modifier.isFinal(t.getModifiers())
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/codehaus/groovy/vmplugin/v8/package-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* This package contains the runtime infrastructure for Groovy's dynamic method dispatch using the {@code invokedynamic}
* instruction. Key components include:
* <ul>
* <li>{@link org.codehaus.groovy.vmplugin.v8.IndyInterface}: The entry point for bootstrap methods and call-site optimization logic.</li>
* <li>{@link org.codehaus.groovy.vmplugin.v8.CacheableCallSite}: Manages the multi-level caching hierarchy (PIC, MRU, LRU) to minimize dispatch overhead.</li>
* <li>{@link org.codehaus.groovy.vmplugin.v8.Selector}: Responsible for dynamic method selection, handle creation, and guard generation.</li>
* </ul>
*/
package org.codehaus.groovy.vmplugin.v8;
Loading
Loading