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
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ class JarJarTask extends DefaultTask {

final protected String projectName = project.name

@InputFiles
@Classpath
@Input
@Optional
List<String> untouchedFiles = []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ public void resetFallbackCount() {
fallbackRound.incrementAndGet();
}

/**
* Clear the LRU cache and reset fallback count.
* Called when metaclass changes to ensure stale method handles are discarded.
*/
public void clearCache() {
synchronized (lruCache) {
lruCache.clear();
}
latestHitMethodHandleWrapperSoftReference = null;
resetFallbackCount();
}

public AtomicLong getFallbackRound() {
return fallbackRound;
}
Expand Down
45 changes: 43 additions & 2 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
import java.lang.invoke.MethodType;
import java.lang.invoke.MutableCallSite;
import java.lang.invoke.SwitchPoint;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Level;
Expand All @@ -51,6 +54,11 @@ 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);
/**
* Initial capacity for the call site registry used to track all active call sites
* for cache invalidation when metaclass changes occur.
*/
private static final int INDY_CALLSITE_INITIAL_CAPACITY = SystemUtil.getIntegerSafe("groovy.indy.callsite.initial.capacity", 1024);

/**
* flags for method and property calls
Expand Down Expand Up @@ -181,24 +189,54 @@ public int getOrderNumber() {
}

protected static SwitchPoint switchPoint = new SwitchPoint();

/**
* Weak set of all CacheableCallSites. Used to invalidate caches when metaclass changes.
* Uses WeakReferences so call sites can be garbage collected when no longer referenced.
*/
private static final Set<WeakReference<CacheableCallSite>> ALL_CALL_SITES = ConcurrentHashMap.newKeySet(INDY_CALLSITE_INITIAL_CAPACITY);

static {
GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints());
}

/**
* Register a call site for cache invalidation when metaclass changes.
*/
static void registerCallSite(CacheableCallSite callSite) {
ALL_CALL_SITES.add(new WeakReference<>(callSite));
}

/**
* Callback for constant metaclass update change
* Callback for constant metaclass update change.
* Invalidates all call site caches to ensure metaclass changes are visible.
*/
protected static void invalidateSwitchPoints() {
if (LOG_ENABLED) {
LOG.info("invalidating switch point");
LOG.info("invalidating switch point and call site caches");
}

synchronized (IndyInterface.class) {
SwitchPoint old = switchPoint;
switchPoint = new SwitchPoint();
SwitchPoint.invalidateAll(new SwitchPoint[]{old});
}

// Invalidate all call site caches and reset targets to default (cache lookup).
ALL_CALL_SITES.removeIf(ref -> {
CacheableCallSite cs = ref.get();
if (cs == null) {
return true; // Remove garbage collected references
}
// Reset target to default (fromCache) so next call goes through cache lookup
MethodHandle defaultTarget = cs.getDefaultTarget();
if (defaultTarget != null && cs.getTarget() != defaultTarget) {
cs.setTarget(defaultTarget);
}
// Clear the cache so stale method handles are discarded
cs.clearCache();
return false;
});
}

/**
Expand Down Expand Up @@ -247,6 +285,9 @@ private static CallSite realBootstrap(MethodHandles.Lookup caller, String name,
mc.setTarget(mh);
mc.setDefaultTarget(mh);
mc.setFallbackTarget(makeFallBack(mc, sender, name, callID, type, safe, thisCall, spreadCall));

// Register for cache invalidation on metaclass changes
registerCallSite(mc);

return mc;
}
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import groovy.lang.MissingMethodException;
import groovy.lang.ProxyMetaClass;
import groovy.transform.Internal;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.reflection.CachedField;
import org.codehaus.groovy.reflection.CachedMethod;
Expand Down Expand Up @@ -122,6 +123,18 @@ public abstract class Selector {
* Cache values for read-only access
*/
private static final CallType[] CALL_TYPE_VALUES = CallType.values();

/**
* Controls whether the global SwitchPoint guard is applied to method handles.
* When {@code false} (default), the SwitchPoint guard is NOT applied, which improves
* performance when metaclass changes occur by avoiding global invalidation of all call sites.
* Instead, call sites are invalidated individually via {@link IndyInterface#invalidateSwitchPoints()}.
* <p>
* Set {@code groovy.indy.switchpoint.guard=true} <strong>only</strong> for specific
* debugging or backward-compatibility scenarios where strict metaclass change
* detection is required, and not for general production use.
*/
private static final boolean INDY_SWITCHPOINT_GUARD = SystemUtil.getBooleanSafe("groovy.indy.switchpoint.guard");

/**
* Returns the Selector
Expand Down Expand Up @@ -959,9 +972,11 @@ 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");
// Apply global switchpoint guard if enabled (disabled by default for performance)
if (INDY_SWITCHPOINT_GUARD) {
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
Loading