Skip to content
Draft
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
92 changes: 92 additions & 0 deletions src/main/java/org/apache/groovy/runtime/indy/AotDispatch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.groovy.runtime.indy;

import java.lang.invoke.SwitchPoint;
import java.util.concurrent.atomic.AtomicLong;

/**
* Ahead-of-time link mode for indy dispatch (spike).
* <p>
* GraalVM native image supports every {@code java.lang.invoke} building block Groovy's indy
* runtime uses <em>except</em> retargeting an existing call site: both
* {@code MutableCallSite.setTarget} and {@code SwitchPoint.invalidateAll} fail with
* {@code Unsupported method java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal}.
* In Groovy's design those two primitives only ever install or invalidate <em>caches</em> —
* the dispatch semantics live entirely in method selection — so under AOT the runtime links
* every site once to its cache-consulting default path ({@code ConstantCallSite}) and carries
* freshness in data instead:
* <ul>
* <li>a global {@linkplain #stamp() invalidation stamp}, bumped wherever the JVM path would
* invalidate SwitchPoints;</li>
* <li>a stamp captured per cached {@code MethodHandleWrapper} at selection time and compared
* on every PIC hit — a mismatch is treated as a cache miss and re-selects.</li>
* </ul>
* The JVM path is untouched: sites link mutable exactly as before, and the stamp is written
* but never read. Coarser than the scoped SwitchPoint invalidation of GROOVY-12191 (any
* invalidation flushes every AOT PIC entry on next hit), which is safe — staleness is
* impossible, over-invalidation just re-selects.
*
* @since 6.0.0
*/
public final class AotDispatch {

/**
* Diagnostic knob: forces AOT link mode on a regular JVM so the whole mode can be
* exercised by ordinary tests without a native build.
*/
public static final String FORCE_PROPERTY = "groovy.indy.aot.link";

private static final AtomicLong STAMP = new AtomicLong();

private AotDispatch() {
}

/**
* Whether sites should link in AOT mode. Evaluated per call and never cached in a static:
* under native image this class may be initialized at image build time, where
* {@code org.graalvm.nativeimage.imagecode} reports {@code buildtime} — caching would bake
* the wrong answer into the image heap. Callers are all link-time or invalidation-time
* (cold); per-invocation code reads the site-local flag captured at link time instead.
*/
public static boolean isAotLinkRequested() {
return "runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))
|| Boolean.getBoolean(FORCE_PROPERTY);
}

/** The current global invalidation stamp. */
public static long stamp() {
return STAMP.get();
}

/**
* Invalidates the given switch points, AOT-safely: the global stamp is always advanced
* (so AOT-linked sites observe the change on their next PIC hit), and the actual
* {@link SwitchPoint#invalidateAll} — which native image cannot execute — runs only
* outside AOT mode. All indy invalidation funnels through here.
*
* @param switchPoints the points to invalidate; may be empty
*/
public static void invalidateAll(final SwitchPoint[] switchPoints) {
STAMP.incrementAndGet();
if (!isAotLinkRequested()) {
SwitchPoint.invalidateAll(switchPoints);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ private static void invalidateBatch(final List<SwitchPoint> batch) {
if (batch.isEmpty()) {
return;
}
SwitchPoint.invalidateAll(batch.toArray(EMPTY_SWITCH_POINTS));
// AOT-safe: advances the AotDispatch stamp; the real invalidateAll runs only on a JVM
AotDispatch.invalidateAll(batch.toArray(EMPTY_SWITCH_POINTS));
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ public static void invalidateIfLive(final SwitchPoint sp) {
synchronized (SINGLE_INVALIDATE_LOCK) {
SINGLE_INVALIDATE_BUF[0] = sp;
try {
SwitchPoint.invalidateAll(SINGLE_INVALIDATE_BUF);
// AOT-safe: stamp always advances; real invalidateAll only on a JVM
AotDispatch.invalidateAll(SINGLE_INVALIDATE_BUF);
} finally {
SINGLE_INVALIDATE_BUF[0] = null;
}
Expand Down
72 changes: 67 additions & 5 deletions src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ protected interface UseExistingReference {
// off the emitted-bytecode surface.
private static final String DISPATCHERS_GETTER = "$getPackedDispatchers$";
private static final String DISPATCHERS_GETTER_DESC = "()Ljava/lang/Object;";
// The factory emitted into the hosting class that adapts its three tables to their functional
// interfaces through bytecode-level LambdaMetafactory sites (see writeDispatchersFactory).
private static final String DISPATCHERS_FACTORY = "$packedDispatchersFactory$";
private static final String BUNDLE_TYPE = "org/codehaus/groovy/runtime/GeneratedDispatcher$Bundle";
private static final String DISPATCHER_TYPE = "org/codehaus/groovy/runtime/GeneratedDispatcher";
private static final String ARITY1_TYPE = "org/codehaus/groovy/runtime/GeneratedDispatcher$Arity1";
private static final String ARITY2_TYPE = "org/codehaus/groovy/runtime/GeneratedDispatcher$Arity2";
private static final Handle LMF_BOOTSTRAP = new Handle(
H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;"
+ "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)"
+ "Ljava/lang/invoke/CallSite;",
false);
// Max tableswitch cases per dispatch method (power of two: the two-level entry method selects a
// chunk with a shift); sized so a full chunk stays well under the JIT's 325-byte inlining budget.
private static final int DISPATCH_CHUNK = 8;
Expand Down Expand Up @@ -1082,20 +1095,28 @@ public void writePackedDispatcher() {
org.objectweb.asm.ClassVisitor cv = controller.getClassVisitor();

// the accessor: return INDY packedDispatchers()Object — IndyInterface.packedDispatchers
// (delegating to GeneratedDispatcher.bootstrap) links the class's three dispatch tables
// (through LambdaMetafactory, with this class's lookup) once, on first adapter creation,
// and every later call returns the constant bundle, so the accessor is also the cache
// (delegating to GeneratedDispatcher.bootstrap) invokes this class's emitted factory
// once, on first adapter creation, and every later call returns the constant bundle,
// so the accessor is also the cache
MethodVisitor mv = cv.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, DISPATCHERS_GETTER, DISPATCHERS_GETTER_DESC, null, null);
mv.visitCode();
// The bundle is built by a factory emitted into this class (see writeDispatchersFactory)
// and reached as a constant bootstrap argument, so the bootstrap needs neither a runtime
// Lookup.findStatic nor a programmatic LambdaMetafactory call — both of which fail under
// GraalVM native image (GROOVY-12227).
Handle bootstrap = new Handle(
H_INVOKESTATIC, INDY_INTERFACE_TYPE, "packedDispatchers",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;"
+ "Ljava/lang/invoke/MethodHandle;)Ljava/lang/invoke/CallSite;",
false);
mv.visitInvokeDynamicInsn("packedDispatchers", DISPATCHERS_GETTER_DESC, bootstrap);
mv.visitInvokeDynamicInsn("packedDispatchers", DISPATCHERS_GETTER_DESC, bootstrap,
new Handle(H_INVOKESTATIC, internal, DISPATCHERS_FACTORY, DISPATCHERS_GETTER_DESC, false));
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();

writeDispatchersFactory(cv, internal);

// the array-free per-arity tables, over the targets whose captured-plus-argument count
// matches (membership is sparse over the id space, so cases use lookupswitch); routing is
// the adapter's responsibility, so any other id landing here is a compiler bug
Expand Down Expand Up @@ -1199,6 +1220,47 @@ private static void writeDispatchSwitch(final MethodVisitor mv, final String int
* switches over its id-range's members (at most {@code DISPATCH_CHUNK}, since a range spans
* {@code DISPATCH_CHUNK} consecutive ids).
*/
/**
* Emits the hosting class's dispatcher factory: three <em>bytecode-level</em>
* {@code LambdaMetafactory} sites adapting its private static tables to their functional
* interfaces, wrapped in one {@code Bundle}.
* <p>
* Emitting the linkage here rather than calling {@code LambdaMetafactory} programmatically
* from the bootstrap matters twice over. The sites are ordinary {@code invokedynamic}, so
* GraalVM native image pre-processes them at build time — no class is defined at run time,
* and the JVM path is unchanged (the VM spins the same hidden class when it links the site).
* And because the factory lives in the hosting class, its method references reach that
* class's own private tables directly, so no {@code Lookup.findStatic} — and hence no
* per-class reflection metadata — is needed either (GROOVY-12227).
*/
private static void writeDispatchersFactory(final org.objectweb.asm.ClassVisitor cv, final String internal) {
MethodVisitor mv = cv.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, DISPATCHERS_FACTORY, DISPATCHERS_GETTER_DESC, null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, BUNDLE_TYPE);
mv.visitInsn(DUP);
emitLambda(mv, internal, "dispatch", DISPATCHER_TYPE, DISPATCH_METHOD, DISPATCH_DESC);
emitLambda(mv, internal, "dispatch1", ARITY1_TYPE, DISPATCH1_METHOD, DISPATCH1_DESC);
emitLambda(mv, internal, "dispatch2", ARITY2_TYPE, DISPATCH2_METHOD, DISPATCH2_DESC);
mv.visitMethodInsn(INVOKESPECIAL, BUNDLE_TYPE, "<init>",
"(L" + DISPATCHER_TYPE + ";L" + ARITY1_TYPE + ";L" + ARITY2_TYPE + ";)V", false);
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}

/**
* Emits one {@code invokedynamic} adapting {@code tableMethod} to the single abstract method
* {@code samName} of {@code ifaceType}. The table's descriptor is both the erased and the
* instantiated signature, so the metafactory inserts no adaptation.
*/
private static void emitLambda(final MethodVisitor mv, final String internal, final String samName,
final String ifaceType, final String tableMethod, final String tableDesc) {
mv.visitInvokeDynamicInsn(samName, "()L" + ifaceType + ";", LMF_BOOTSTRAP,
org.objectweb.asm.Type.getMethodType(tableDesc),
new Handle(H_INVOKESTATIC, internal, tableMethod, tableDesc, false),
org.objectweb.asm.Type.getMethodType(tableDesc));
}

private static void writeArityTable(final org.objectweb.asm.ClassVisitor cv, final String internal,
final ClassNode enclosing, final List<MethodNode> targets, final int paramCount,
final String tableMethod, final String tableDesc) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/codehaus/groovy/reflection/ClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ public void invalidateIndySwitchPoint() {
List<SwitchPoint> batch = new ArrayList<>(2);
collectLiveIndySwitchPoints(batch);
if (!batch.isEmpty()) {
SwitchPoint.invalidateAll(batch.toArray(new SwitchPoint[0]));
// AOT-safe: stamp always advances; real invalidateAll only on a JVM
org.apache.groovy.runtime.indy.AotDispatch.invalidateAll(batch.toArray(new SwitchPoint[0]));
}
}

Expand Down
41 changes: 35 additions & 6 deletions src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;


/**
* A per-class table of compiler-generated dispatch targets, reached by a compact
* integer id instead of a {@link java.lang.invoke.MethodHandle}.
Expand Down Expand Up @@ -115,7 +117,11 @@ final class Bundle {
final Arity1 arity1;
final Arity2 arity2;

Bundle(final GeneratedDispatcher dispatcher, final Arity1 arity1, final Arity2 arity2) {
/**
* Public because the hosting class's compiler-emitted factory constructs it directly
* (see {@code ClosureWriter#writeDispatchersFactory}); not API for hand-written code.
*/
public Bundle(final GeneratedDispatcher dispatcher, final Arity1 arity1, final Arity2 arity2) {
this.dispatcher = dispatcher;
this.arity1 = arity1;
this.arity2 = arity2;
Expand Down Expand Up @@ -150,11 +156,12 @@ static Class<?>[] paramTypes(final MethodHandles.Lookup caller, final String nam
}

/**
* Invokedynamic bootstrap for the hosting class's dispatcher accessor: adapts the class's
* three private static dispatch tables to their functional interfaces (one hidden class
* each, via {@code LambdaMetafactory} with the caller's full-privilege lookup) and returns
* them as one constant {@link Bundle}. Linked once per class, on first adapter creation.
* Emitted bytecode reaches this through
* Legacy invokedynamic bootstrap for the dispatcher accessor, kept for class files emitted
* by earlier 6.0 pre-releases: adapts the class's three private static dispatch tables to
* their functional interfaces (one hidden class each, via {@code LambdaMetafactory} with the
* caller's full-privilege lookup) and returns them as one constant {@link Bundle}. Current
* class files link through the one-{@code MethodHandle} overload instead. Emitted bytecode
* reaches this through
* {@code org.codehaus.groovy.vmplugin.v8.IndyInterface#packedDispatchers} — the central
* bytecode-facing bootstrap surface — which delegates here.
*
Expand Down Expand Up @@ -182,4 +189,26 @@ static CallSite bootstrap(final MethodHandles.Lookup caller, final String name,
twoType, caller.findStatic(host, TABLE2_METHOD, twoType), twoType).getTarget().invokeExact();
return new ConstantCallSite(MethodHandles.constant(type.returnType(), new Bundle(dispatcher, arity1, arity2)));
}

/**
* Invokedynamic bootstrap for the dispatcher accessor: the hosting class supplies (as a
* constant bootstrap argument) a compiler-emitted factory that builds the bundle from its
* own <em>bytecode-level</em> {@code LambdaMetafactory} sites, so linking is one call and
* this method neither looks anything up nor defines any class. Works unchanged under GraalVM
* native image, where those sites are pre-processed at image build time and the factory's
* method references reach the class's own private tables without reflection metadata
* (GROOVY-12227). Emitted bytecode reaches this through
* {@code org.codehaus.groovy.vmplugin.v8.IndyInterface#packedDispatchers}.
*
* @param caller the hosting class's lookup (supplied by the JVM, unused)
* @param name the invoked name (unused)
* @param type the accessor's type (see the three-argument overload)
* @param factory the hosting class's {@code $packedDispatchersFactory$}, {@code () -> Bundle}
* @return a constant call site producing the bundle
* @throws Throwable if the factory fails (a compiler bug)
*/
static CallSite bootstrap(final MethodHandles.Lookup caller, final String name, final MethodType type,
final MethodHandle factory) throws Throwable {
return new ConstantCallSite(MethodHandles.constant(type.returnType(), factory.invoke()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class CacheableCallSite extends MutableCallSite {
private volatile SoftReference<MethodHandleWrapper> latestHitMethodHandleWrapperSoftReference = null;
private final AtomicLong fallbackCount = new AtomicLong();
private final AtomicLong fallbackRound = new AtomicLong();
private final boolean aotLinked;
private MethodHandle defaultTarget;
private MethodHandle fallbackTarget;
private final Map<String, SoftReference<MethodHandleWrapper>> lruCache =
Expand Down Expand Up @@ -85,6 +86,34 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) {
super(type);
this.lookup = lookup;
// captured once, at link time (this constructor only runs while linking a site), so
// per-invocation code reads a plain field instead of probing system properties
this.aotLinked = org.apache.groovy.runtime.indy.AotDispatch.isAotLinkRequested();
}

/**
* Whether this site was linked in AOT mode (GraalVM native image, or the
* {@code groovy.indy.aot.link} diagnostic knob): the site is wrapped in a
* {@code ConstantCallSite} over the cache-consulting default path, is never retargeted,
* and cache freshness is carried by the {@code AotDispatch} stamp instead of SwitchPoints.
*/
public boolean isAotLinked() {
return aotLinked;
}

/**
* Fails fast on any retarget attempt in AOT mode. Under a real native image
* {@code setTarget} throws {@code UnsupportedFeatureError} anyway — and worse, execution
* would continue with the stale target if that error were swallowed — so a missed gate is
* a bug on every platform; this surfaces it on the JVM, where tests run with the
* diagnostic knob.
*/
@Override
public void setTarget(final MethodHandle newTarget) {
if (aotLinked) {
throw new IllegalStateException("call site retargeting is disabled in AOT link mode (GROOVY-12227 spike)");
}
super.setTarget(newTarget);
}

/**
Expand All @@ -94,6 +123,22 @@ public CacheableCallSite(MethodType type, MethodHandles.Lookup lookup) {
* @param valueProvider the provider used to compute a missing entry
* @return the cached or newly created wrapper
*/
/**
* Read-only PIC lookup: the cached wrapper for the receiver class, or {@code null} when
* absent or its soft reference has been cleared. Used by the AOT dispatch path, which
* resolves misses itself and must not pay for a value-provider allocation per call.
*
* @param className the receiver cache key
* @return the cached wrapper or {@code null}
*/
public MethodHandleWrapper getIfPresent(String className) {
final SoftReference<MethodHandleWrapper> ref;
synchronized (lruCache) {
ref = lruCache.get(className);
}
return ref == null ? null : ref.get();
}

public MethodHandleWrapper getAndPut(String className, MemoizeCache.ValueProvider<? super String, ? extends MethodHandleWrapper> valueProvider) {
MethodHandleWrapper result = null;
SoftReference<MethodHandleWrapper> resultSoftReference;
Expand Down
Loading
Loading