Skip to content
Merged
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
111 changes: 111 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,111 @@
/*
* 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 (incubating).
* <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>
* <li>a pre-selection sample guarding the PIC write itself: when the stamp moves while a
* selection runs, the sentinel is cached instead of the wrapper, since the wrapper's
* construction-time stamp would postdate an invalidation its selection may have missed
* (SwitchPoint guards are immune to this window — their token is acquired during
* selection and mutated by the invalidation itself).</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.
* <p>
* The retargeting restriction is particular to GraalVM native image — other ahead-of-time
* or checkpointed runtimes (ART, CRaC, HotSpot AOT caches) retarget call sites normally and
* never need this mode — so auto-detection probes only GraalVM's image-code property. The
* mode itself relies on nothing GraalVM-specific; {@link #FORCE_PROPERTY} is the opt-in for
* any runtime that turns out to share the restriction.
*
* @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.
* <p>
* Set it at JVM startup (or before any Groovy code runs) and leave it alone. Because
* {@link #isAotLinkRequested()} is re-evaluated per link and per invalidation, flipping
* the property on mid-run suppresses the real {@link SwitchPoint#invalidateAll} that
* sites already linked in mutable mode depend on — their guards never fire and they
* dispatch stale. (Flipping it off is merely wasteful: sites linked while it was on keep
* consulting the stamp, which keeps advancing, so they stay correct but never regain the
* retargeting fast path.)
*/
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
4 changes: 3 additions & 1 deletion src/main/java/org/codehaus/groovy/reflection/ClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import groovy.lang.MetaClassRegistry;
import groovy.lang.MetaMethod;
import groovy.transform.Internal;
import org.apache.groovy.runtime.indy.AotDispatch;
import org.apache.groovy.runtime.indy.IndyInvalidation;
import org.apache.groovy.runtime.indy.SwitchPointInvalidator;
import org.apache.groovy.util.concurrent.ManagedIdentityConcurrentMap;
Expand Down Expand Up @@ -239,7 +240,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
AotDispatch.invalidateAll(batch.toArray(new SwitchPoint[0]));
}
}

Expand Down
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,50 @@ 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-12234)");
}
super.setTarget(newTarget);
}

/**
* 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();
}

/**
Expand Down
Loading
Loading