Skip to content

Detect cold CallTarget invalidation and reset its profile; Limit number of recompilations within a time period #11610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String> lazyStackTrace) {
if (target == callTargetFilter) {
compilationResult.set(false);
}
}
}
}
3 changes: 2 additions & 1 deletion truffle/docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -300,6 +302,7 @@ public Class<?> getType() {
*/
private volatile CompilationTask compilationTask;
private int successfulCompilationsCount;
private Instant timeOfFirstCompilationInWindow;

private volatile boolean needsSplit;

Expand Down Expand Up @@ -537,6 +540,7 @@ public final RootNode getRootNode() {
public final void resetCompilationProfile() {
this.callCount = 0;
this.callAndLoopCount = 0;
this.timeOfFirstCompilationInWindow = Instant.now();
}

@Override
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -1169,6 +1219,9 @@ public final boolean computeBlockCompilations() {

@Override
public void onCompilationSuccess(int compilationTier, boolean lastTier) {
if (this.timeOfFirstCompilationInWindow == null) {
timeOfFirstCompilationInWindow = Instant.now();
}
successfulCompilationsCount++;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,19 @@ public ExceptionAction apply(String s) {
usageSyntax = "[1, inf)", category = OptionCategory.EXPERT) //
public static final OptionKey<Integer> 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<Integer> 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<Integer> 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<Integer> MinInvokeThreshold = new OptionKey<>(3);

Expand Down
Loading
Loading