From ec0c74f0e4c8958b66112a614c895fba6049bbda Mon Sep 17 00:00:00 2001 From: Cesar Soares Date: Thu, 3 Jul 2025 14:05:14 -0700 Subject: [PATCH 1/3] Detect cold method invalidation & reprofile. Limit on number of compilations --- .../truffle/test/DeoptInvalidateListener.java | 2 +- .../truffle/test/MaximumCompilationsTest.java | 139 +++++++++++++++++- truffle/docs/Options.md | 3 +- .../TruffleCompilerAssumptionDependency.java | 2 + .../oracle/truffle/runtime/EngineData.java | 3 + .../truffle/runtime/OptimizedCallTarget.java | 71 +++++++-- .../runtime/OptimizedRuntimeOptions.java | 12 +- .../OptimizedTruffleRuntimeListener.java | 19 ++- ...mizedTruffleRuntimeListenerDispatcher.java | 14 +- .../truffle/runtime/debug/JFRListener.java | 2 +- .../debug/TraceCompilationListener.java | 30 +++- .../hotspot/HotSpotOptimizedCallTarget.java | 111 +++++++++++--- .../hotspot/HotSpotTruffleRuntime.java | 21 +++ 13 files changed, 380 insertions(+), 49 deletions(-) diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DeoptInvalidateListener.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DeoptInvalidateListener.java index 742a153e1f1c..e65dd5025dbd 100644 --- a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DeoptInvalidateListener.java +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DeoptInvalidateListener.java @@ -45,7 +45,7 @@ protected DeoptInvalidateListener(OptimizedTruffleRuntime runtime, OptimizedCall } @Override - public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { + public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { if (target == focus) { deoptimized = true; } diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java index 23e2a97150f3..8d2f05af4857 100644 --- a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java @@ -24,22 +24,26 @@ */ package jdk.graal.compiler.truffle.test; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - -import org.graalvm.polyglot.Context; -import org.junit.Assume; -import org.junit.Test; +import static org.junit.Assert.assertEquals; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.RootNode; +import com.oracle.truffle.compiler.TruffleCompilerListener; +import com.oracle.truffle.runtime.AbstractCompilationTask; import com.oracle.truffle.runtime.OptimizedCallTarget; import com.oracle.truffle.runtime.OptimizedTruffleRuntime; import com.oracle.truffle.runtime.OptimizedTruffleRuntimeListener; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.junit.Assume; +import org.junit.Test; public class MaximumCompilationsTest { public static class AllwaysDeoptRoot extends RootNode { @@ -90,4 +94,125 @@ public void onCompilationFailed(OptimizedCallTarget target, String reason, boole } } } + + @Test + public void testUnlimitedRecompilations() { + Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); + OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); + AtomicBoolean compilationResult = new AtomicBoolean(); + + try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// + option("engine.BackgroundCompilation", "false").// + option("engine.CompileImmediately", "true").build()) { + try (Context ctx = Context.newBuilder().engine(eng).build()) { + ctx.enter(); + CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); + optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); + + for (int i = 0; i < 16; i++) { + callTarget.call(); + assertEquals(true, compilationResult.get()); + } + } + } + } + + @Test + public void testMaxTwoCompilations() throws InterruptedException { + Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); + OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); + AtomicBoolean compilationResult = new AtomicBoolean(); + + try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// + option("engine.BackgroundCompilation", "false").// + option("engine.CompileImmediately", "true").// + option("engine.MaximumCompilations", "2").build()) { + try (Context ctx = Context.newBuilder().engine(eng).build()) { + ctx.enter(); + + CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); + optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); + + callTarget.call(); + assertEquals(true, compilationResult.get()); + + callTarget.call(); + assertEquals(true, compilationResult.get()); + + callTarget.call(); + assertEquals(false, compilationResult.get()); + + TimeUnit.SECONDS.sleep(90); + + // The method will not be recompiled because it has reached all compilations + // possible in its lifetime + callTarget.call(); + assertEquals(false, compilationResult.get()); + } + } + } + + @Test + public void testMaxTwoCompilationsPerMinute() throws InterruptedException { + Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); + OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); + AtomicBoolean compilationResult = new AtomicBoolean(); + + try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// + option("engine.BackgroundCompilation", "false").// + option("engine.CompileImmediately", "true").// + option("engine.MaximumCompilations", "2").// + option("engine.MaximumCompilationsWindow", "1").build()) { + try (Context ctx = Context.newBuilder().engine(eng).build()) { + ctx.enter(); + + CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); + optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); + + callTarget.call(); + assertEquals(true, compilationResult.get()); + + callTarget.call(); + assertEquals(true, compilationResult.get()); + + // This shouldn't trigger the compilation because there was already two compilations + // of this call target in the last minute. + callTarget.call(); + assertEquals(false, compilationResult.get()); + + // Wait to make sure we overflow the compilation period + TimeUnit.SECONDS.sleep(90); + + // this should trigger a new compilation as there was no compilation of this call + // target in the last minute + callTarget.call(); + assertEquals(true, compilationResult.get()); + } + } + } + + private class CustomListener implements OptimizedTruffleRuntimeListener { + public CallTarget callTargetFilter = null; + public AtomicBoolean compilationResult = new AtomicBoolean(); + + CustomListener(CallTarget callTarget, AtomicBoolean compilationResult) { + this.callTargetFilter = callTarget; + this.compilationResult = compilationResult; + } + + @Override + public void onCompilationSuccess(OptimizedCallTarget target, AbstractCompilationTask task, TruffleCompilerListener.GraphInfo graph, TruffleCompilerListener.CompilationResultInfo result) { + OptimizedTruffleRuntimeListener.super.onCompilationSuccess(target, task, graph, result); + if (target == callTargetFilter) { + compilationResult.set(true); + } + } + + @Override + public void onCompilationFailed(OptimizedCallTarget target, String reason, boolean bailout, boolean permanentBailout, int tier, Supplier lazyStackTrace) { + if (target == callTargetFilter) { + compilationResult.set(false); + } + } + } } diff --git a/truffle/docs/Options.md b/truffle/docs/Options.md index f16a7c574075..bb1d2f1ea4f5 100644 --- a/truffle/docs/Options.md +++ b/truffle/docs/Options.md @@ -74,7 +74,8 @@ The accepted values are: - `--engine.FirstTierCompilationThreshold=[1, inf)` : Number of invocations or loop iterations needed to compile a guest language root in first tier under normal compilation load.Might be reduced/increased when compilation load is low/high if DynamicCompilationThresholds is enabled. (default: 400). - `--engine.FirstTierMinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled in the first tier (default: 1) - `--engine.LastTierCompilationThreshold=[1, inf)` : Number of invocations or loop iterations needed to compile a guest language root in first tier under normal compilation load.Might be reduced/increased when compilation load is low/high if DynamicCompilationThresholds is enabled. (default: 10000). -- `--engine.MaximumCompilations=(-inf, inf)` : Maximum number of successful compilations for a single call target before a permanent bailout. Exceeding the limit will result in a compilation failure with the appropriate reason and there will be no further attempts to compile the call target. (negative integer means no limit, default: 100) +- `--engine.MaximumCompilations=(-inf, inf)` : Maximum number of successful compilations for a single call target before a temporary/permanent bailout. Exceeding this limit will result in a compilation failure with the appropriate reason and there will be no further attempts to compile the call target within the time window specified in MaximumCompilationsWindow. (negative integer means no limit, default: 100) +- `--engine.MaximumCompilationsWindow=(-inf, inf)` : Time window in minutes used to limit the number of compilations of a call target. If the value is a negative integer it means an infinite window. This parameter is ignored if MaximumCompilations is a negative integer. (default: -1) - `--engine.MinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled (default: 3). - `--engine.Mode=latency|throughput` : Configures the execution mode of the engine. Available modes are 'latency' and 'throughput'. The default value balances between the two. - `--engine.MultiTier=true|false` : Whether to use multiple Truffle compilation tiers by default. (default: true) diff --git a/truffle/src/com.oracle.truffle.compiler/src/com/oracle/truffle/compiler/TruffleCompilerAssumptionDependency.java b/truffle/src/com.oracle.truffle.compiler/src/com/oracle/truffle/compiler/TruffleCompilerAssumptionDependency.java index 10883dc35999..def8e39d45a9 100644 --- a/truffle/src/com.oracle.truffle.compiler/src/com/oracle/truffle/compiler/TruffleCompilerAssumptionDependency.java +++ b/truffle/src/com.oracle.truffle.compiler/src/com/oracle/truffle/compiler/TruffleCompilerAssumptionDependency.java @@ -68,6 +68,8 @@ public void onAssumptionInvalidated(Object source, CharSequence reason) { boolean wasActive = false; InstalledCode code = getInstalledCode(); if (code != null && code.isAlive()) { + // No need to set deoptimize or invalidation reason here because the defaults, 'true' + // and 'JVMCI_INVALIDATE' are the appropriate. code.invalidate(); wasActive = true; } else { diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java index ad1d735268b5..e0ed8b5baade 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java @@ -53,6 +53,7 @@ import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.FirstTierMinInvokeThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.LastTierCompilationThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MaximumCompilations; +import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MaximumCompilationsWindow; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MinInvokeThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.Mode; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MultiTier; @@ -165,6 +166,7 @@ public final class EngineData { @CompilationFinal public boolean propagateCallAndLoopCount; @CompilationFinal public int propagateCallAndLoopCountMaxDepth; @CompilationFinal public int maximumCompilations; + @CompilationFinal public int maximumCompilationsWindowInMinutes; // computed fields. @CompilationFinal public int callThresholdInInterpreter; @@ -323,6 +325,7 @@ private void loadOptions(OptionValues options, SandboxPolicy sandboxPolicy) { // See usage of traversingFirstTierBonus for explanation of this formula. traversingFirstTierBonus = options.get(TraversingQueueFirstTierBonus) * options.get(LastTierCompilationThreshold) / options.get(FirstTierCompilationThreshold); maximumCompilations = options.get(MaximumCompilations); + maximumCompilationsWindowInMinutes = options.get(MaximumCompilationsWindow); traversingInvalidatedBonus = options.get(TraversingQueueInvalidatedBonus); traversingOSRBonus = options.get(TraversingQueueOSRBonus); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java index 5ce4cc521c43..0b772bdacda8 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java @@ -42,6 +42,8 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -300,6 +302,7 @@ public Class getType() { */ private volatile CompilationTask compilationTask; private int successfulCompilationsCount; + private Instant timeOfFirstCompilationInWindow; private volatile boolean needsSplit; @@ -537,6 +540,7 @@ public final RootNode getRootNode() { public final void resetCompilationProfile() { this.callCount = 0; this.callAndLoopCount = 0; + this.timeOfFirstCompilationInWindow = Instant.now(); } @Override @@ -842,8 +846,8 @@ private RuntimeException handleException(VirtualFrame frame, Throwable t) { throw rethrow(profiledT); } - private void notifyDeoptimized(VirtualFrame frame) { - runtime().getListener().onCompilationDeoptimized(this, frame); + protected void notifyDeoptimized(VirtualFrame frame) { + runtime().getListener().onCompilationDeoptimized(this, frame, null); } protected static OptimizedTruffleRuntime runtime() { @@ -1001,13 +1005,7 @@ public final boolean compile(boolean lastTierCompilation) { try { assert compilationTask == null; - if (engine.maximumCompilations >= 0 && successfulCompilationsCount >= engine.maximumCompilations) { - compilationFailed = true; - runtime().getListener().onCompilationStarted(this, new PresubmitFailureCompilationTask(engine.firstTierOnly, lastTier)); - String failureReason = String.format("Maximum compilation count %d reached.", engine.maximumCompilations); - runtime().getListener().onCompilationFailed(this, failureReason, true, true, - lastTier ? 2 : 1, null); - handleCompilationFailure(() -> failureReason, false, true, true); + if (blockNewCompilations(lastTier)) { return false; } this.compilationTask = task = runtime().submitForCompilation(this, lastTier); @@ -1024,6 +1022,58 @@ public final boolean compile(boolean lastTierCompilation) { return false; } + private boolean blockNewCompilations(boolean lastTier) { + // No limit on number of re-compilations + if (engine.maximumCompilations < 0) { + return false; + } + + // If there is a window specified for the maximum number of compilations we check if we + // should reset the number of compilations because we overflowed the window period. + if (engine.maximumCompilationsWindowInMinutes > 0) { + long ageInMinutes = ChronoUnit.MINUTES.between(timeOfFirstCompilationInWindow, Instant.now()); + if (ageInMinutes >= engine.maximumCompilationsWindowInMinutes) { + // This compilation would have been blocked if the window hadn't overflowed, + // therefore we print a log saying that compilations are now "enabled". I don't want + // to print this message if the number of compilations of the method hadn't reached + // the limit. + if (successfulCompilationsCount == engine.maximumCompilations) { + runtime().getListener().onCompilationReenabled(this); + } + + // Reset window information + successfulCompilationsCount = 0; + timeOfFirstCompilationInWindow = Instant.now(); + } + } + + if (successfulCompilationsCount >= engine.maximumCompilations) { + if (successfulCompilationsCount == engine.maximumCompilations) { + // This bailout will be permanent if there is no window set for the maximum number + // of compilations + compilationFailed = (engine.maximumCompilationsWindowInMinutes < 0); + String failureReason = String.format("Maximum compilation count %d reached.", engine.maximumCompilations); + + // If the bailout is not permanent, i.e., this method is blocked for new + // compilations only for a period, then I bump the {@code + // successfulCompilationsCount} + // counter by one to indicate that the method is temporarily blocked for new + // compilations. Since there is a time window set, this value will eventually be + // reset to zero, so its temporary value should be imaterial. + if (!compilationFailed) { + successfulCompilationsCount++; + } + + runtime().getListener().onCompilationStarted(this, new PresubmitFailureCompilationTask(engine.firstTierOnly, lastTier)); + runtime().getListener().onCompilationFailed(this, failureReason, true, compilationFailed, lastTier ? 2 : 1, null); + handleCompilationFailure(() -> failureReason, false, true, compilationFailed); + } + return true; + } + + return false; + } + public final boolean maybeWaitForTask(CompilationTask task) { boolean mayBeAsynchronous = engine.backgroundCompilation; runtime().finishCompilation(this, task, mayBeAsynchronous); @@ -1169,6 +1219,9 @@ public final boolean computeBlockCompilations() { @Override public void onCompilationSuccess(int compilationTier, boolean lastTier) { + if (this.timeOfFirstCompilationInWindow == null) { + timeOfFirstCompilationInWindow = Instant.now(); + } successfulCompilationsCount++; } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java index 82224e6cda72..daa941cf44ef 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java @@ -205,11 +205,19 @@ public ExceptionAction apply(String s) { usageSyntax = "[1, inf)", category = OptionCategory.EXPERT) // public static final OptionKey LastTierCompilationThreshold = new OptionKey<>(10000); - @Option(help = "Maximum number of successful compilations for a single call target before a permanent bailout. Exceeding the limit will result in a compilation failure with the appropriate reason and " + // - "there will be no further attempts to compile the call target. (negative integer means no limit, default: 100)", // + @Option(help = "Maximum number of successful compilations for a single call target before a temporary/permanent bailout." + + "Exceeding this limit will result in a compilation failure with the appropriate reason and there will be " + // + "no further attempts to compile the call target within the time window specified in MaximumCompilationsWindow." + + "(negative integer means no limit, default: 100)", // usageSyntax = "(-inf, inf)", category = OptionCategory.EXPERT) // public static final OptionKey MaximumCompilations = new OptionKey<>(100); + @Option(help = "Time window in minutes used to limit the number of compilations of a call target." + + "If the value is a negative integer it means an infinite window." + // + "This parameter is ignored if MaximumCompilations is a negative integer. (default: -1)", // + usageSyntax = "(-inf, inf)", category = OptionCategory.EXPERT) // + public static final OptionKey MaximumCompilationsWindow = new OptionKey<>(-1); + @Option(help = "Minimum number of calls before a call target is compiled (default: 3).", usageSyntax = "[1, inf)", category = OptionCategory.EXPERT) // public static final OptionKey MinInvokeThreshold = new OptionKey<>(3); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java index ad34c5b95c64..5766d7a0f485 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java @@ -294,8 +294,25 @@ default void onCompilationInvalidated(OptimizedCallTarget target, Object source, * * @param target the call target whose compiled code was just deoptimized * @param frame + * @param reason optional reason why the deoptimization happened. */ - default void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { + default void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { + } + + /** + * Notifies this object when {@code target} has its execution profile reset. + * + * @param target the call target whose profile was just reset. + */ + default void onProfileReset(OptimizedCallTarget target) { + } + + /** + * Notifies this object when compilations of {@code target} are enabled. + * + * @param target the call target whose compilations are enabled. + */ + default void onCompilationReenabled(OptimizedCallTarget target) { } /** diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java index 3714964b3d38..6de212cae83b 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java @@ -115,8 +115,18 @@ public void onCompilationInvalidated(OptimizedCallTarget target, Object source, } @Override - public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { - invokeListeners((l) -> l.onCompilationDeoptimized(target, frame)); + public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { + invokeListeners((l) -> l.onCompilationDeoptimized(target, frame, reason)); + } + + @Override + public void onCompilationReenabled(OptimizedCallTarget target) { + invokeListeners((l) -> l.onCompilationReenabled(target)); + } + + @Override + public void onProfileReset(OptimizedCallTarget target) { + invokeListeners((l) -> l.onProfileReset(target)); } @Override diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java index e5e8223a1808..ed8cefd95279 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/JFRListener.java @@ -118,7 +118,7 @@ public void onCompilationStarted(OptimizedCallTarget target, AbstractCompilation } @Override - public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { + public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { DeoptimizationEvent event = factory.createDeoptimizationEvent(); if (event.isEnabled()) { event.setRootFunction(target); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java index ea87cd2a3d79..3a6a70c5ebc4 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java @@ -102,7 +102,9 @@ public static void install(OptimizedTruffleRuntime runtime) { private static final String FAILED_FORMAT = "opt failed " + TARGET_FORMAT + "|" + TIER_FORMAT + "|Time %18s|Reason: %s|UTC %s|Src %s"; private static final String PADDING = " "; private static final String INV_FORMAT = "opt inval. " + TARGET_FORMAT + " " + PADDING + "|UTC %s|Src %s|Reason %s"; - private static final String DEOPT_FORMAT = "opt deopt " + TARGET_FORMAT + "|" + PADDING + "|UTC %s|Src %s"; + private static final String DEOPT_FORMAT = "opt deopt " + TARGET_FORMAT + "|" + PADDING + "|UTC %s|Src %s|Reason %s"; + private static final String REPROF_FORMAT = "opt reprofile " + TARGET_FORMAT + "|" + PADDING + "|UTC %s|Src %s"; + private static final String ENABLED_FORMAT = "opt enabled " + TARGET_FORMAT + "|UTC %s|Src %s"; // @formatter:on @Override @@ -222,9 +224,22 @@ private void log(OptimizedCallTarget target, String message) { } @Override - public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { + public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { if (target.engine.traceCompilation || target.engine.traceCompilationDetails) { log(target, String.format(DEOPT_FORMAT, + target.engineId(), + target.id, + safeTargetName(target), + TIME_FORMATTER.format(ZonedDateTime.now()), + formatSourceSection(safeSourceSection(target)), + reason != null ? reason : "unknown")); + } + } + + @Override + public void onProfileReset(OptimizedCallTarget target) { + if (target.engine.traceCompilation || target.engine.traceCompilationDetails) { + log(target, String.format(REPROF_FORMAT, target.engineId(), target.id, safeTargetName(target), @@ -269,6 +284,17 @@ public void onCompilationSuccess(OptimizedCallTarget target, AbstractCompilation currentCompilation.remove(); } + public void onCompilationReenabled(OptimizedCallTarget target) { + if (target.engine.traceCompilationDetails) { + log(target, String.format(ENABLED_FORMAT, + target.engineId(), + target.id, + safeTargetName(target), + TIME_FORMATTER.format(ZonedDateTime.now()), + formatSourceSection(safeSourceSection(target)))); + } + } + static String getCompilationId(CompilationResultInfo result) { /* * When the CompilationResultInfo class is from GraalVM, the getCompilationId method might diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java index b0b1eca6a8ee..2833332ab2ec 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java @@ -42,6 +42,8 @@ import java.lang.reflect.Method; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.compiler.TruffleCompiler; import com.oracle.truffle.runtime.EngineData; @@ -97,24 +99,35 @@ public HotSpotOptimizedCallTarget(EngineData engine) { private static final Method setSpeculationLog; /** - * Reflective reference to {@code InstalledCode.invalidate(boolean deoptimize)} so that this + * Reflective reference to + * {@code InstalledCode.invalidate(boolean deoptimize, int invalidationReason)} so that this * code can be compiled against older JVMCI API. */ - @SuppressWarnings("unused") private static final Method invalidateInstalledCode; + @SuppressWarnings("unused") private static final Method invalidateInstalledCodeWithReasonMethodRef; + @SuppressWarnings("unused") private static final Method invalidateInstalledCodeWithoutReasonMethodRef; + + /** + * Reflective reference to {@code HotSpotNmethod.getInvalidationReason()} and + * {@code HotSpotNmethod.getInvalidationReasonDescription()} so that this code can be compiled + * against older JVMCI API. + */ + @SuppressWarnings("unused") private static final Method getInvalidationReasonMethodRef; + @SuppressWarnings("unused") private static final Method getInvalidationReasonDescriptionMethodRef; static { - Method method = null; - try { - method = HotSpotNmethod.class.getDeclaredMethod("setSpeculationLog", HotSpotSpeculationLog.class); - } catch (NoSuchMethodException e) { - } - setSpeculationLog = method; - method = null; + setSpeculationLog = initialize(HotSpotNmethod.class, "setSpeculationLog", HotSpotSpeculationLog.class); + invalidateInstalledCodeWithReasonMethodRef = initialize(HotSpotNmethod.class, "invalidate", boolean.class, int.class); + invalidateInstalledCodeWithoutReasonMethodRef = initialize(InstalledCode.class, "invalidate", boolean.class); + getInvalidationReasonMethodRef = initialize(HotSpotNmethod.class, "getInvalidationReason"); + getInvalidationReasonDescriptionMethodRef = initialize(HotSpotNmethod.class, "getInvalidationReasonDescription"); + } + + private static Method initialize(Class clx, String methodName, Class... args) { try { - method = InstalledCode.class.getDeclaredMethod("invalidate", boolean.class); + return clx.getDeclaredMethod(methodName, args); } catch (NoSuchMethodException e) { + return null; } - invalidateInstalledCode = method; } /** @@ -122,20 +135,11 @@ public HotSpotOptimizedCallTarget(EngineData engine) { */ public void setInstalledCode(InstalledCode code) { assert code != null : "code must never become null"; - InstalledCode oldCode = this.installedCode; - if (oldCode == code) { + if (this.installedCode == code) { return; } - if (oldCode != INVALID_CODE && invalidateInstalledCode != null) { - try { - invalidateInstalledCode.invoke(oldCode, false); - } catch (Error e) { - throw e; - } catch (Throwable throwable) { - throw new InternalError(throwable); - } - } + invalidateExistingCode(); // A default nmethod can be called from entry points in the VM (e.g., Method::_code) // and so allowing it to be installed here would invalidate the truth of @@ -174,9 +178,23 @@ private void tetherSpeculationLog(HotSpotNmethod nmethod) throws Error, Internal } } + /** + * This method will reset the execution profile counters of this call target if the installed + * code was invalidated because it became cold. + * + * @return whether the currently installed code is valid/executable. + */ @Override public boolean isValid() { - return installedCode.isValid(); + boolean isValid = installedCode.isValid(); + if (!isValid && installedCode != INVALID_CODE) { + if (getInvalidationReason() == ((HotSpotTruffleRuntime) runtime()).getColdMethodInvalidationReason()) { + invalidateExistingCode(); + resetCompilationProfile(); + runtime().getListener().onProfileReset(this); + } + } + return isValid; } @Override @@ -195,4 +213,51 @@ public SpeculationLog getCompilationSpeculationLog() { return HotSpotTruffleRuntimeServices.getCompilationSpeculationLog(this); } + @Override + protected void notifyDeoptimized(VirtualFrame frame) { + runtime().getListener().onCompilationDeoptimized(this, frame, getInvalidationReasonDescription()); + } + + @TruffleBoundary + private void invalidateExistingCode() throws Error, InternalError { + if (this.installedCode != INVALID_CODE) { + try { + InstalledCode oldCode = this.installedCode; + if (invalidateInstalledCodeWithReasonMethodRef != null) { + invalidateInstalledCodeWithReasonMethodRef.invoke(this.installedCode, false, ((HotSpotTruffleRuntime) runtime()).getJVMCIReplacedMethodInvalidationReason()); + } else if (invalidateInstalledCodeWithoutReasonMethodRef != null) { + invalidateInstalledCodeWithoutReasonMethodRef.invoke(oldCode, false); + } + this.installedCode = INVALID_CODE; + } catch (Error e) { + throw e; + } catch (Throwable throwable) { + throw new InternalError(throwable); + } + } + } + + @TruffleBoundary + private int getInvalidationReason() { + try { + if (getInvalidationReasonMethodRef != null) { + return (int) getInvalidationReasonMethodRef.invoke(this.installedCode); + } + } catch (Exception e) { + // ignore + } + return 0; + } + + @TruffleBoundary + private String getInvalidationReasonDescription() { + try { + if (getInvalidationReasonDescriptionMethodRef != null) { + return (String) getInvalidationReasonDescriptionMethodRef.invoke(this.installedCode); + } + } catch (Exception e) { + // ignore + } + return null; + } } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java index 0e50c0ca9a31..08347dce1370 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java @@ -176,11 +176,24 @@ private Lazy lazy() { private final HotSpotVMConfigAccess vmConfigAccess; + /** + * This constant is used to detect when a method was invalidated by HotSpot because the code + * cache heuristic considered it cold. + */ + private final int coldMethodInvalidationReason; + + /** + * This constant is used when Truffle invalidates an installed code. + */ + private final int jvmciReplacedMethodInvalidationReason; + public HotSpotTruffleRuntime(TruffleCompilationSupport compilationSupport) { super(compilationSupport, Arrays.asList(HotSpotOptimizedCallTarget.class, InstalledCode.class, HotSpotThreadLocalHandshake.class, HotSpotTruffleRuntime.class)); installCallBoundaryMethods(null); this.vmConfigAccess = new HotSpotVMConfigAccess(HotSpotJVMCIRuntime.runtime().getConfigStore()); + this.jvmciReplacedMethodInvalidationReason = vmConfigAccess.getConstant("nmethod::InvalidationReason::JVMCI_REPLACED_WITH_NEW_CODE", Integer.class, -1); + this.coldMethodInvalidationReason = vmConfigAccess.getConstant("nmethod::InvalidationReason::UNLOADING_COLD", Integer.class, -1); int jvmciReservedReference0Offset = vmConfigAccess.getFieldOffset("JavaThread::_jvmci_reserved_oop0", Integer.class, "oop", -1); if (jvmciReservedReference0Offset == -1) { @@ -644,6 +657,14 @@ protected int getObjectAlignment() { return getVMOptionValue("ObjectAlignmentInBytes", Integer.class); } + public int getColdMethodInvalidationReason() { + return this.coldMethodInvalidationReason; + } + + public int getJVMCIReplacedMethodInvalidationReason() { + return this.jvmciReplacedMethodInvalidationReason; + } + @Override protected int getArrayIndexScale(Class componentType) { MetaAccessProvider meta = getMetaAccess(); From b4d6789fe8581c01d5f1763d0535d9417a6699f8 Mon Sep 17 00:00:00 2001 From: Cesar Soares Date: Wed, 16 Jul 2025 10:41:08 -0700 Subject: [PATCH 2/3] Force new build. --- truffle/docs/Options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/truffle/docs/Options.md b/truffle/docs/Options.md index bb1d2f1ea4f5..1e249276763b 100644 --- a/truffle/docs/Options.md +++ b/truffle/docs/Options.md @@ -75,7 +75,7 @@ The accepted values are: - `--engine.FirstTierMinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled in the first tier (default: 1) - `--engine.LastTierCompilationThreshold=[1, inf)` : Number of invocations or loop iterations needed to compile a guest language root in first tier under normal compilation load.Might be reduced/increased when compilation load is low/high if DynamicCompilationThresholds is enabled. (default: 10000). - `--engine.MaximumCompilations=(-inf, inf)` : Maximum number of successful compilations for a single call target before a temporary/permanent bailout. Exceeding this limit will result in a compilation failure with the appropriate reason and there will be no further attempts to compile the call target within the time window specified in MaximumCompilationsWindow. (negative integer means no limit, default: 100) -- `--engine.MaximumCompilationsWindow=(-inf, inf)` : Time window in minutes used to limit the number of compilations of a call target. If the value is a negative integer it means an infinite window. This parameter is ignored if MaximumCompilations is a negative integer. (default: -1) +- `--engine.MaximumCompilationsWindow=(-inf, inf)` : Time window in minutes used to limit the number of compilations of a call target. If the value is a negative integer it means an infinite window. The parameter is ignored if MaximumCompilations is a negative integer. (default: -1) - `--engine.MinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled (default: 3). - `--engine.Mode=latency|throughput` : Configures the execution mode of the engine. Available modes are 'latency' and 'throughput'. The default value balances between the two. - `--engine.MultiTier=true|false` : Whether to use multiple Truffle compilation tiers by default. (default: true) From 5825caf71ad6282ad4e02c27b5527140ff2b8096 Mon Sep 17 00:00:00 2001 From: Cesar Soares Date: Thu, 14 Aug 2025 14:06:47 -0700 Subject: [PATCH 3/3] Addressing feedback: drop maximum compilation window changes, refactor logic for resetting call target profile. --- .../truffle/test/MaximumCompilationsTest.java | 139 +----------------- truffle/docs/Options.md | 3 +- .../oracle/truffle/runtime/EngineData.java | 3 - .../truffle/runtime/OptimizedCallTarget.java | 68 +-------- .../runtime/OptimizedRuntimeOptions.java | 12 +- .../runtime/OptimizedTruffleRuntime.java | 18 ++- .../OptimizedTruffleRuntimeListener.java | 20 ++- ...mizedTruffleRuntimeListenerDispatcher.java | 7 +- .../debug/TraceCompilationListener.java | 12 -- .../hotspot/HotSpotOptimizedCallTarget.java | 84 +++++------ .../hotspot/HotSpotTruffleRuntime.java | 25 +++- 11 files changed, 103 insertions(+), 288 deletions(-) diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java index 8d2f05af4857..23e2a97150f3 100644 --- a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/MaximumCompilationsTest.java @@ -24,26 +24,22 @@ */ package jdk.graal.compiler.truffle.test; -import static org.junit.Assert.assertEquals; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.graalvm.polyglot.Context; +import org.junit.Assume; +import org.junit.Test; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.RootNode; -import com.oracle.truffle.compiler.TruffleCompilerListener; -import com.oracle.truffle.runtime.AbstractCompilationTask; import com.oracle.truffle.runtime.OptimizedCallTarget; import com.oracle.truffle.runtime.OptimizedTruffleRuntime; import com.oracle.truffle.runtime.OptimizedTruffleRuntimeListener; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.junit.Assume; -import org.junit.Test; public class MaximumCompilationsTest { public static class AllwaysDeoptRoot extends RootNode { @@ -94,125 +90,4 @@ public void onCompilationFailed(OptimizedCallTarget target, String reason, boole } } } - - @Test - public void testUnlimitedRecompilations() { - Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); - OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); - AtomicBoolean compilationResult = new AtomicBoolean(); - - try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// - option("engine.BackgroundCompilation", "false").// - option("engine.CompileImmediately", "true").build()) { - try (Context ctx = Context.newBuilder().engine(eng).build()) { - ctx.enter(); - CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); - optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); - - for (int i = 0; i < 16; i++) { - callTarget.call(); - assertEquals(true, compilationResult.get()); - } - } - } - } - - @Test - public void testMaxTwoCompilations() throws InterruptedException { - Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); - OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); - AtomicBoolean compilationResult = new AtomicBoolean(); - - try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// - option("engine.BackgroundCompilation", "false").// - option("engine.CompileImmediately", "true").// - option("engine.MaximumCompilations", "2").build()) { - try (Context ctx = Context.newBuilder().engine(eng).build()) { - ctx.enter(); - - CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); - optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); - - callTarget.call(); - assertEquals(true, compilationResult.get()); - - callTarget.call(); - assertEquals(true, compilationResult.get()); - - callTarget.call(); - assertEquals(false, compilationResult.get()); - - TimeUnit.SECONDS.sleep(90); - - // The method will not be recompiled because it has reached all compilations - // possible in its lifetime - callTarget.call(); - assertEquals(false, compilationResult.get()); - } - } - } - - @Test - public void testMaxTwoCompilationsPerMinute() throws InterruptedException { - Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime); - OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime(); - AtomicBoolean compilationResult = new AtomicBoolean(); - - try (Engine eng = Engine.newBuilder().allowExperimentalOptions(true).// - option("engine.BackgroundCompilation", "false").// - option("engine.CompileImmediately", "true").// - option("engine.MaximumCompilations", "2").// - option("engine.MaximumCompilationsWindow", "1").build()) { - try (Context ctx = Context.newBuilder().engine(eng).build()) { - ctx.enter(); - - CallTarget callTarget = new AllwaysDeoptRoot().getCallTarget(); - optimizedTruffleRuntime.addListener(new CustomListener(callTarget, compilationResult)); - - callTarget.call(); - assertEquals(true, compilationResult.get()); - - callTarget.call(); - assertEquals(true, compilationResult.get()); - - // This shouldn't trigger the compilation because there was already two compilations - // of this call target in the last minute. - callTarget.call(); - assertEquals(false, compilationResult.get()); - - // Wait to make sure we overflow the compilation period - TimeUnit.SECONDS.sleep(90); - - // this should trigger a new compilation as there was no compilation of this call - // target in the last minute - callTarget.call(); - assertEquals(true, compilationResult.get()); - } - } - } - - private class CustomListener implements OptimizedTruffleRuntimeListener { - public CallTarget callTargetFilter = null; - public AtomicBoolean compilationResult = new AtomicBoolean(); - - CustomListener(CallTarget callTarget, AtomicBoolean compilationResult) { - this.callTargetFilter = callTarget; - this.compilationResult = compilationResult; - } - - @Override - public void onCompilationSuccess(OptimizedCallTarget target, AbstractCompilationTask task, TruffleCompilerListener.GraphInfo graph, TruffleCompilerListener.CompilationResultInfo result) { - OptimizedTruffleRuntimeListener.super.onCompilationSuccess(target, task, graph, result); - if (target == callTargetFilter) { - compilationResult.set(true); - } - } - - @Override - public void onCompilationFailed(OptimizedCallTarget target, String reason, boolean bailout, boolean permanentBailout, int tier, Supplier lazyStackTrace) { - if (target == callTargetFilter) { - compilationResult.set(false); - } - } - } } diff --git a/truffle/docs/Options.md b/truffle/docs/Options.md index 1e249276763b..f16a7c574075 100644 --- a/truffle/docs/Options.md +++ b/truffle/docs/Options.md @@ -74,8 +74,7 @@ The accepted values are: - `--engine.FirstTierCompilationThreshold=[1, inf)` : Number of invocations or loop iterations needed to compile a guest language root in first tier under normal compilation load.Might be reduced/increased when compilation load is low/high if DynamicCompilationThresholds is enabled. (default: 400). - `--engine.FirstTierMinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled in the first tier (default: 1) - `--engine.LastTierCompilationThreshold=[1, inf)` : Number of invocations or loop iterations needed to compile a guest language root in first tier under normal compilation load.Might be reduced/increased when compilation load is low/high if DynamicCompilationThresholds is enabled. (default: 10000). -- `--engine.MaximumCompilations=(-inf, inf)` : Maximum number of successful compilations for a single call target before a temporary/permanent bailout. Exceeding this limit will result in a compilation failure with the appropriate reason and there will be no further attempts to compile the call target within the time window specified in MaximumCompilationsWindow. (negative integer means no limit, default: 100) -- `--engine.MaximumCompilationsWindow=(-inf, inf)` : Time window in minutes used to limit the number of compilations of a call target. If the value is a negative integer it means an infinite window. The parameter is ignored if MaximumCompilations is a negative integer. (default: -1) +- `--engine.MaximumCompilations=(-inf, inf)` : Maximum number of successful compilations for a single call target before a permanent bailout. Exceeding the limit will result in a compilation failure with the appropriate reason and there will be no further attempts to compile the call target. (negative integer means no limit, default: 100) - `--engine.MinInvokeThreshold=[1, inf)` : Minimum number of calls before a call target is compiled (default: 3). - `--engine.Mode=latency|throughput` : Configures the execution mode of the engine. Available modes are 'latency' and 'throughput'. The default value balances between the two. - `--engine.MultiTier=true|false` : Whether to use multiple Truffle compilation tiers by default. (default: true) diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java index 73eac1d6dcf1..a4e193802c92 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/EngineData.java @@ -53,7 +53,6 @@ import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.FirstTierMinInvokeThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.LastTierCompilationThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MaximumCompilations; -import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MaximumCompilationsWindow; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MinInvokeThreshold; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.Mode; import static com.oracle.truffle.runtime.OptimizedRuntimeOptions.MultiTier; @@ -168,7 +167,6 @@ public final class EngineData { @CompilationFinal public boolean propagateCallAndLoopCount; @CompilationFinal public int propagateCallAndLoopCountMaxDepth; @CompilationFinal public int maximumCompilations; - @CompilationFinal public int maximumCompilationsWindowInMinutes; // computed fields. @CompilationFinal public int callThresholdInInterpreter; @@ -337,7 +335,6 @@ private void loadOptions(OptionValues options, SandboxPolicy sandboxPolicy) { // See usage of traversingFirstTierBonus for explanation of this formula. traversingFirstTierBonus = options.get(TraversingQueueFirstTierBonus) * options.get(LastTierCompilationThreshold) / options.get(FirstTierCompilationThreshold); maximumCompilations = options.get(MaximumCompilations); - maximumCompilationsWindowInMinutes = options.get(MaximumCompilationsWindow); traversingInvalidatedBonus = options.get(TraversingQueueInvalidatedBonus); traversingOSRBonus = options.get(TraversingQueueOSRBonus); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java index 0b772bdacda8..07f4fc7bc425 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedCallTarget.java @@ -42,8 +42,6 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -302,7 +300,6 @@ public Class getType() { */ private volatile CompilationTask compilationTask; private int successfulCompilationsCount; - private Instant timeOfFirstCompilationInWindow; private volatile boolean needsSplit; @@ -540,7 +537,7 @@ public final RootNode getRootNode() { public final void resetCompilationProfile() { this.callCount = 0; this.callAndLoopCount = 0; - this.timeOfFirstCompilationInWindow = Instant.now(); + this.successfulCompilationsCount = 0; } @Override @@ -1005,7 +1002,13 @@ public final boolean compile(boolean lastTierCompilation) { try { assert compilationTask == null; - if (blockNewCompilations(lastTier)) { + if (engine.maximumCompilations >= 0 && successfulCompilationsCount >= engine.maximumCompilations) { + compilationFailed = true; + runtime().getListener().onCompilationStarted(this, new PresubmitFailureCompilationTask(engine.firstTierOnly, lastTier)); + String failureReason = String.format("Maximum compilation count %d reached.", engine.maximumCompilations); + runtime().getListener().onCompilationFailed(this, failureReason, true, true, + lastTier ? 2 : 1, null); + handleCompilationFailure(() -> failureReason, false, true, true); return false; } this.compilationTask = task = runtime().submitForCompilation(this, lastTier); @@ -1022,58 +1025,6 @@ public final boolean compile(boolean lastTierCompilation) { return false; } - private boolean blockNewCompilations(boolean lastTier) { - // No limit on number of re-compilations - if (engine.maximumCompilations < 0) { - return false; - } - - // If there is a window specified for the maximum number of compilations we check if we - // should reset the number of compilations because we overflowed the window period. - if (engine.maximumCompilationsWindowInMinutes > 0) { - long ageInMinutes = ChronoUnit.MINUTES.between(timeOfFirstCompilationInWindow, Instant.now()); - if (ageInMinutes >= engine.maximumCompilationsWindowInMinutes) { - // This compilation would have been blocked if the window hadn't overflowed, - // therefore we print a log saying that compilations are now "enabled". I don't want - // to print this message if the number of compilations of the method hadn't reached - // the limit. - if (successfulCompilationsCount == engine.maximumCompilations) { - runtime().getListener().onCompilationReenabled(this); - } - - // Reset window information - successfulCompilationsCount = 0; - timeOfFirstCompilationInWindow = Instant.now(); - } - } - - if (successfulCompilationsCount >= engine.maximumCompilations) { - if (successfulCompilationsCount == engine.maximumCompilations) { - // This bailout will be permanent if there is no window set for the maximum number - // of compilations - compilationFailed = (engine.maximumCompilationsWindowInMinutes < 0); - String failureReason = String.format("Maximum compilation count %d reached.", engine.maximumCompilations); - - // If the bailout is not permanent, i.e., this method is blocked for new - // compilations only for a period, then I bump the {@code - // successfulCompilationsCount} - // counter by one to indicate that the method is temporarily blocked for new - // compilations. Since there is a time window set, this value will eventually be - // reset to zero, so its temporary value should be imaterial. - if (!compilationFailed) { - successfulCompilationsCount++; - } - - runtime().getListener().onCompilationStarted(this, new PresubmitFailureCompilationTask(engine.firstTierOnly, lastTier)); - runtime().getListener().onCompilationFailed(this, failureReason, true, compilationFailed, lastTier ? 2 : 1, null); - handleCompilationFailure(() -> failureReason, false, true, compilationFailed); - } - return true; - } - - return false; - } - public final boolean maybeWaitForTask(CompilationTask task) { boolean mayBeAsynchronous = engine.backgroundCompilation; runtime().finishCompilation(this, task, mayBeAsynchronous); @@ -1219,9 +1170,6 @@ public final boolean computeBlockCompilations() { @Override public void onCompilationSuccess(int compilationTier, boolean lastTier) { - if (this.timeOfFirstCompilationInWindow == null) { - timeOfFirstCompilationInWindow = Instant.now(); - } successfulCompilationsCount++; } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java index daa941cf44ef..82224e6cda72 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedRuntimeOptions.java @@ -205,19 +205,11 @@ public ExceptionAction apply(String s) { usageSyntax = "[1, inf)", category = OptionCategory.EXPERT) // public static final OptionKey LastTierCompilationThreshold = new OptionKey<>(10000); - @Option(help = "Maximum number of successful compilations for a single call target before a temporary/permanent bailout." + - "Exceeding this limit will result in a compilation failure with the appropriate reason and there will be " + // - "no further attempts to compile the call target within the time window specified in MaximumCompilationsWindow." + - "(negative integer means no limit, default: 100)", // + @Option(help = "Maximum number of successful compilations for a single call target before a permanent bailout. Exceeding the limit will result in a compilation failure with the appropriate reason and " + // + "there will be no further attempts to compile the call target. (negative integer means no limit, default: 100)", // usageSyntax = "(-inf, inf)", category = OptionCategory.EXPERT) // public static final OptionKey MaximumCompilations = new OptionKey<>(100); - @Option(help = "Time window in minutes used to limit the number of compilations of a call target." + - "If the value is a negative integer it means an infinite window." + // - "This parameter is ignored if MaximumCompilations is a negative integer. (default: -1)", // - usageSyntax = "(-inf, inf)", category = OptionCategory.EXPERT) // - public static final OptionKey MaximumCompilationsWindow = new OptionKey<>(-1); - @Option(help = "Minimum number of calls before a call target is compiled (default: 3).", usageSyntax = "[1, inf)", category = OptionCategory.EXPERT) // public static final OptionKey MinInvokeThreshold = new OptionKey<>(3); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntime.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntime.java index 7573f0d39dc2..dfde4b5aa4e8 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntime.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntime.java @@ -181,7 +181,7 @@ protected void clearState() { this.knownMethods = null; } - private final OptimizedTruffleRuntimeListenerDispatcher listeners = new OptimizedTruffleRuntimeListenerDispatcher(); + protected final OptimizedTruffleRuntimeListenerDispatcher listeners = new OptimizedTruffleRuntimeListenerDispatcher(); protected volatile TruffleCompiler truffleCompiler; protected volatile OptimizedCallTarget initializeCallTarget; @@ -847,6 +847,11 @@ private void shutdown() { protected final void doCompile(OptimizedCallTarget callTarget, AbstractCompilationTask task) { Objects.requireNonNull(callTarget, "Cannot compile null call target."); Objects.requireNonNull(task, "Compilation task required."); + + if (shouldAbortCompilation(callTarget)) { + return; + } + List oldBlockCompilations = callTarget.blockCompilations; if (oldBlockCompilations != null) { for (OptimizedCallTarget blockTarget : oldBlockCompilations) { @@ -1478,4 +1483,15 @@ public enum CompilationActivityMode { protected CompilationActivityMode getCompilationActivityMode() { return CompilationActivityMode.RUN_COMPILATION; } + + /** + * This method is intended to give subclasses a last say on whether the compilation should be + * aborted or not. + * + * @param callTarget - The OptimizedCallTarget for the compilation. + * @return true if the compilation should be aborted. + */ + protected boolean shouldAbortCompilation(OptimizedCallTarget callTarget) { + return false; + } } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java index 5766d7a0f485..ba50ad8d5cd5 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListener.java @@ -294,25 +294,29 @@ default void onCompilationInvalidated(OptimizedCallTarget target, Object source, * * @param target the call target whose compiled code was just deoptimized * @param frame - * @param reason optional reason why the deoptimization happened. */ - default void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { + @Deprecated(since = "26.0") + default void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame) { + onCompilationDeoptimized(target, frame, null); } /** - * Notifies this object when {@code target} has its execution profile reset. + * Notifies this object when {@code target} has just deoptimized and is now executing in the + * Truffle interpreter instead of executing compiled code. * - * @param target the call target whose profile was just reset. + * @param target the call target whose compiled code was just deoptimized + * @param frame + * @param reason optional reason why the deoptimization happened. */ - default void onProfileReset(OptimizedCallTarget target) { + default void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, String reason) { } /** - * Notifies this object when compilations of {@code target} are enabled. + * Notifies this object when {@code target} has its execution profile reset. * - * @param target the call target whose compilations are enabled. + * @param target the call target whose profile was just reset. */ - default void onCompilationReenabled(OptimizedCallTarget target) { + default void onProfileReset(OptimizedCallTarget target) { } /** diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java index 6de212cae83b..dbbfe723f7e6 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/OptimizedTruffleRuntimeListenerDispatcher.java @@ -54,7 +54,7 @@ * {@link TruffleCompilerListener} events to {@link OptimizedTruffleRuntimeListener} events. */ @SuppressWarnings("serial") -final class OptimizedTruffleRuntimeListenerDispatcher extends CopyOnWriteArrayList implements OptimizedTruffleRuntimeListener, TruffleCompilerListener { +public final class OptimizedTruffleRuntimeListenerDispatcher extends CopyOnWriteArrayList implements OptimizedTruffleRuntimeListener, TruffleCompilerListener { @Override public boolean add(OptimizedTruffleRuntimeListener e) { @@ -119,11 +119,6 @@ public void onCompilationDeoptimized(OptimizedCallTarget target, Frame frame, St invokeListeners((l) -> l.onCompilationDeoptimized(target, frame, reason)); } - @Override - public void onCompilationReenabled(OptimizedCallTarget target) { - invokeListeners((l) -> l.onCompilationReenabled(target)); - } - @Override public void onProfileReset(OptimizedCallTarget target) { invokeListeners((l) -> l.onProfileReset(target)); diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java index 3a6a70c5ebc4..cbc5182bfbca 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/debug/TraceCompilationListener.java @@ -104,7 +104,6 @@ public static void install(OptimizedTruffleRuntime runtime) { private static final String INV_FORMAT = "opt inval. " + TARGET_FORMAT + " " + PADDING + "|UTC %s|Src %s|Reason %s"; private static final String DEOPT_FORMAT = "opt deopt " + TARGET_FORMAT + "|" + PADDING + "|UTC %s|Src %s|Reason %s"; private static final String REPROF_FORMAT = "opt reprofile " + TARGET_FORMAT + "|" + PADDING + "|UTC %s|Src %s"; - private static final String ENABLED_FORMAT = "opt enabled " + TARGET_FORMAT + "|UTC %s|Src %s"; // @formatter:on @Override @@ -284,17 +283,6 @@ public void onCompilationSuccess(OptimizedCallTarget target, AbstractCompilation currentCompilation.remove(); } - public void onCompilationReenabled(OptimizedCallTarget target) { - if (target.engine.traceCompilationDetails) { - log(target, String.format(ENABLED_FORMAT, - target.engineId(), - target.id, - safeTargetName(target), - TIME_FORMATTER.format(ZonedDateTime.now()), - formatSourceSection(safeSourceSection(target)))); - } - } - static String getCompilationId(CompilationResultInfo result) { /* * When the CompilationResultInfo class is from GraalVM, the getCompilationId method might diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java index 2833332ab2ec..fe97930b4dbe 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotOptimizedCallTarget.java @@ -42,7 +42,6 @@ import java.lang.reflect.Method; -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.compiler.TruffleCompiler; @@ -135,11 +134,24 @@ private static Method initialize(Class clx, String methodName, Class... ar */ public void setInstalledCode(InstalledCode code) { assert code != null : "code must never become null"; - if (this.installedCode == code) { + InstalledCode oldCode = this.installedCode; + if (oldCode == code) { return; } - invalidateExistingCode(); + if (oldCode != INVALID_CODE) { + try { + if (invalidateInstalledCodeWithReasonMethodRef != null) { + invalidateInstalledCodeWithReasonMethodRef.invoke(oldCode, false, ((HotSpotTruffleRuntime) runtime()).getJVMCIReplacedMethodInvalidationReason()); + } else if (invalidateInstalledCodeWithoutReasonMethodRef != null) { + invalidateInstalledCodeWithoutReasonMethodRef.invoke(oldCode, false); + } + } catch (Error e) { + throw e; + } catch (Throwable throwable) { + throw new InternalError(throwable); + } + } // A default nmethod can be called from entry points in the VM (e.g., Method::_code) // and so allowing it to be installed here would invalidate the truth of @@ -178,23 +190,9 @@ private void tetherSpeculationLog(HotSpotNmethod nmethod) throws Error, Internal } } - /** - * This method will reset the execution profile counters of this call target if the installed - * code was invalidated because it became cold. - * - * @return whether the currently installed code is valid/executable. - */ @Override public boolean isValid() { - boolean isValid = installedCode.isValid(); - if (!isValid && installedCode != INVALID_CODE) { - if (getInvalidationReason() == ((HotSpotTruffleRuntime) runtime()).getColdMethodInvalidationReason()) { - invalidateExistingCode(); - resetCompilationProfile(); - runtime().getListener().onProfileReset(this); - } - } - return isValid; + return installedCode.isValid(); } @Override @@ -215,49 +213,35 @@ public SpeculationLog getCompilationSpeculationLog() { @Override protected void notifyDeoptimized(VirtualFrame frame) { - runtime().getListener().onCompilationDeoptimized(this, frame, getInvalidationReasonDescription()); - } - - @TruffleBoundary - private void invalidateExistingCode() throws Error, InternalError { - if (this.installedCode != INVALID_CODE) { - try { - InstalledCode oldCode = this.installedCode; - if (invalidateInstalledCodeWithReasonMethodRef != null) { - invalidateInstalledCodeWithReasonMethodRef.invoke(this.installedCode, false, ((HotSpotTruffleRuntime) runtime()).getJVMCIReplacedMethodInvalidationReason()); - } else if (invalidateInstalledCodeWithoutReasonMethodRef != null) { - invalidateInstalledCodeWithoutReasonMethodRef.invoke(oldCode, false); - } - this.installedCode = INVALID_CODE; - } catch (Error e) { - throw e; - } catch (Throwable throwable) { - throw new InternalError(throwable); - } - } - } - - @TruffleBoundary - private int getInvalidationReason() { + String reason = null; try { - if (getInvalidationReasonMethodRef != null) { - return (int) getInvalidationReasonMethodRef.invoke(this.installedCode); + if (getInvalidationReasonDescriptionMethodRef != null) { + reason = (String) getInvalidationReasonDescriptionMethodRef.invoke(this.installedCode); } } catch (Exception e) { // ignore + } finally { + runtime().getListener().onCompilationDeoptimized(this, frame, reason); } - return 0; } - @TruffleBoundary - private String getInvalidationReasonDescription() { + /** + * This method is intended only for resetting {@installedCode}, it should only be called on + * CallTargets that are already invalid. + */ + public void resetInstalledCode() { + assert !isValid(); + this.installedCode = INVALID_CODE; + } + + public int getInvalidationReason() { try { - if (getInvalidationReasonDescriptionMethodRef != null) { - return (String) getInvalidationReasonDescriptionMethodRef.invoke(this.installedCode); + if (getInvalidationReasonMethodRef != null) { + return (int) getInvalidationReasonMethodRef.invoke(this.installedCode); } } catch (Exception e) { // ignore } - return null; + return 0; } } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java index 08347dce1370..0367154d1da5 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/hotspot/HotSpotTruffleRuntime.java @@ -657,10 +657,6 @@ protected int getObjectAlignment() { return getVMOptionValue("ObjectAlignmentInBytes", Integer.class); } - public int getColdMethodInvalidationReason() { - return this.coldMethodInvalidationReason; - } - public int getJVMCIReplacedMethodInvalidationReason() { return this.jvmciReplacedMethodInvalidationReason; } @@ -757,4 +753,25 @@ private static CompilationActivityMode resolveHotSpotActivityMode(int i) { default -> throw CompilerDirectives.shouldNotReachHere("Invalid CompilationActivityMode " + i); }; } + + /** + * When running as part of HotSpot we should pay special attention to CallTargets (CTs) that + * have been flushed from the code cache because they were cold (According to Code Cache's + * heuristics). Truffle's CallTargets' Profile counter don't decay. For that reason, we need + * special handling for cold (according to code cache heuristics) CTs that were flushed from the + * code cache. Otherwise, we can enter a recompilation cycle because Truffle will always see the + * method as hot (because the profile counters never reset). To handle this case we reset the CT + * profile whenever its prior compilation was invalidated because it was cold. + */ + @Override + protected boolean shouldAbortCompilation(OptimizedCallTarget callTarget) { + HotSpotOptimizedCallTarget hsCallTarget = (HotSpotOptimizedCallTarget) callTarget; + if (hsCallTarget.getInvalidationReason() == this.coldMethodInvalidationReason) { + hsCallTarget.resetCompilationProfile(); + hsCallTarget.resetInstalledCode(); + listeners.onProfileReset(hsCallTarget); + return true; + } + return false; + } }