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
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.graal.compiler.truffle.test;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;

import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.instrumentation.test.InstrumentationTestLanguage;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.api.test.SubprocessTestUtils;
import com.oracle.truffle.compiler.TruffleCompilerListener;
import com.oracle.truffle.runtime.AbstractCompilationTask;
import com.oracle.truffle.runtime.FixedPointMath;
import com.oracle.truffle.runtime.OptimizedCallTarget;
import com.oracle.truffle.runtime.OptimizedTruffleRuntime;
import com.oracle.truffle.runtime.OptimizedTruffleRuntimeListener;

public class DynamicCompilationThresholdsTest {
private static final Pattern TARGET_NAME_PATTERN = Pattern.compile("DynamicCompilationThresholdsTest(\\d+)");
static AtomicInteger ID = new AtomicInteger();

static class SourceCompilation {
private final int id;
private final Source source;
private final CountDownLatch compilationDoneLatch = new CountDownLatch(1);
private final CountDownLatch compilationStartedLatch = new CountDownLatch(1);
private final CountDownLatch compilationGoLatch = new CountDownLatch(1);

SourceCompilation(int id, Source source) {
this.id = id;
this.source = source;
}
}

Map<Integer, SourceCompilation> compilationMap = new ConcurrentHashMap<>();

@Test
public void testDynamicCompilationThreshods() throws IOException, InterruptedException {
Assume.assumeTrue(Truffle.getRuntime() instanceof OptimizedTruffleRuntime);
Runnable test = () -> {
OptimizedTruffleRuntime optimizedTruffleRuntime = (OptimizedTruffleRuntime) Truffle.getRuntime();
OptimizedTruffleRuntimeListener listener = new OptimizedTruffleRuntimeListener() {
@Override
public void onCompilationSuccess(OptimizedCallTarget target, AbstractCompilationTask task, TruffleCompilerListener.GraphInfo graph,
TruffleCompilerListener.CompilationResultInfo result) {
if (getSourceCompilation(target) instanceof SourceCompilation compilation) {
compilation.compilationDoneLatch.countDown();
}
}

private SourceCompilation getSourceCompilation(OptimizedCallTarget target) {
if (target.getRootNode().getSourceSection() instanceof SourceSection section && section.getSource() != null) {
Matcher matcher = TARGET_NAME_PATTERN.matcher(section.getSource().getName());
if (matcher.find()) {
return compilationMap.get(Integer.parseInt(matcher.group(1)));
}
}
return null;
}

@Override
public void onCompilationStarted(OptimizedCallTarget target, AbstractCompilationTask task) {
if (getSourceCompilation(target) instanceof SourceCompilation compilation) {
compilation.compilationStartedLatch.countDown();
try {
compilation.compilationGoLatch.await();
} catch (InterruptedException ie) {
throw new AssertionError(ie);
}
}
}
};
optimizedTruffleRuntime.addListener(listener);
try (Context context = Context.newBuilder().allowExperimentalOptions(true) //
.option("engine.BackgroundCompilation", "true") //
.option("engine.CompileImmediately", "false") //
.option("engine.SingleTierCompilationThreshold", "1") //
.option("engine.MultiTier", "false") //
.option("engine.DynamicCompilationThresholds", "true") //
.option("engine.DynamicCompilationThresholdsMaxNormalLoad", "20") //
.option("engine.DynamicCompilationThresholdsMinNormalLoad", "10") //
.option("engine.DynamicCompilationThresholdsMinScale", "0.1") //
.option("engine.CompilerThreads", "1").build()) {
int firstCompilation = submitCompilation(context);
waitForCompilationStart(firstCompilation);
LinkedList<Integer> scales = new LinkedList<>();
int firstScale = FixedPointMath.toFixedPoint(0.1);
Assert.assertEquals(firstScale, optimizedTruffleRuntime.compilationThresholdScale());
scales.push(firstScale);
for (int i = 1; i <= 10; i++) {
submitCompilation(context);
int scale = FixedPointMath.toFixedPoint(0.1 + 0.9 * i / 10);
Assert.assertEquals(scale, optimizedTruffleRuntime.compilationThresholdScale());
scales.push(scale);
}
for (int i = 1; i <= 10; i++) {
submitCompilation(context);
int scale = FixedPointMath.toFixedPoint(1.0);
Assert.assertEquals(scale, optimizedTruffleRuntime.compilationThresholdScale());
scales.push(scale);
}
for (int i = 1; i <= 10; i++) {
submitCompilation(context);
int scale = FixedPointMath.toFixedPoint(1.0 + 0.9 * i / 10);
Assert.assertEquals(scale, optimizedTruffleRuntime.compilationThresholdScale());
scales.push(scale);
}
allowCompilationToProceed(firstCompilation);
waitForCompilationDone(firstCompilation);
scales.pop();
for (int i = firstCompilation + 1; i <= 30; i++) {
waitForCompilationStart(i);
Assert.assertEquals((int) scales.pop(), optimizedTruffleRuntime.compilationThresholdScale());
allowCompilationToProceed(i);
waitForCompilationDone(i);
}
Assert.assertTrue(scales.isEmpty());
} catch (IOException | InterruptedException e) {
throw new AssertionError(e);
} finally {
optimizedTruffleRuntime.removeListener(listener);
}
};
SubprocessTestUtils.newBuilder(DynamicCompilationThresholdsTest.class, test).run();
}

private void allowCompilationToProceed(int id) throws InterruptedException {
compilationMap.get(id).compilationGoLatch.countDown();
}

private void waitForCompilationStart(int id) throws InterruptedException {
if (!compilationMap.get(id).compilationStartedLatch.await(5, TimeUnit.MINUTES)) {
throw new AssertionError("Compilation of source " + id + " did not start in time");
}
}

private void waitForCompilationDone(int id) throws InterruptedException {
if (!compilationMap.get(id).compilationDoneLatch.await(5, TimeUnit.MINUTES)) {
throw new AssertionError("Compilation of source " + id + " did not finish in time");
}
}

int submitCompilation(Context context) throws IOException {
int id = ID.getAndIncrement();
Source source = Source.newBuilder(InstrumentationTestLanguage.ID, "CONSTANT(" + id + ")", "DynamicCompilationThresholdsTest" + id).build();
SourceCompilation compilation = new SourceCompilation(id, source);
compilationMap.put(id, compilation);
context.eval(source);
return id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
Expand Down Expand Up @@ -149,10 +150,25 @@ private ExecutorService getExecutorService(OptimizedCallTarget callTarget) {
long compilerIdleDelay = runtime.getCompilerIdleDelay(callTarget);
long keepAliveTime = compilerIdleDelay >= 0 ? compilerIdleDelay : 0;

BlockingQueue<Runnable> queue = createQueue(callTarget, threads);
BlockingQueue<Runnable> queue;
if (callTarget.getOptionValue(OptimizedRuntimeOptions.TraversingCompilationQueue)) {
queue = new TraversingBlockingQueue(new IdlingLinkedBlockingDeque<>());
} else {
queue = new IdlingPriorityBlockingQueue<>();
}

DynamicCompilationThresholds dynamicCompilationThresholds = null;
if (callTarget.getOptionValue(OptimizedRuntimeOptions.TraversingCompilationQueue)) {
if (callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholds) && callTarget.getOptionValue(OptimizedRuntimeOptions.BackgroundCompilation)) {
double minScale = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMinScale);
int minNormalLoad = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMinNormalLoad);
int maxNormalLoad = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMaxNormalLoad);
dynamicCompilationThresholds = new DynamicCompilationThresholds(threads, minScale, minNormalLoad, maxNormalLoad);
}
}
TruffleThreadPoolExecutor threadPoolExecutor = new TruffleThreadPoolExecutor(threads, threads,
keepAliveTime, TimeUnit.MILLISECONDS,
queue, factory);
queue, factory, dynamicCompilationThresholds);

if (compilerIdleDelay > 0) {
// There are two mechanisms to signal idleness: if core threads can timeout, then
Expand All @@ -165,21 +181,6 @@ private ExecutorService getExecutorService(OptimizedCallTarget callTarget) {
}
}

private BlockingQueue<Runnable> createQueue(OptimizedCallTarget callTarget, int threads) {
if (callTarget.getOptionValue(OptimizedRuntimeOptions.TraversingCompilationQueue)) {
if (callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholds) && callTarget.getOptionValue(OptimizedRuntimeOptions.BackgroundCompilation)) {
double minScale = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMinScale);
int minNormalLoad = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMinNormalLoad);
int maxNormalLoad = callTarget.getOptionValue(OptimizedRuntimeOptions.DynamicCompilationThresholdsMaxNormalLoad);
return new DynamicThresholdsQueue(threads, minScale, minNormalLoad, maxNormalLoad, new IdlingLinkedBlockingDeque<>());
} else {
return new TraversingBlockingQueue(new IdlingLinkedBlockingDeque<>());
}
} else {
return new IdlingPriorityBlockingQueue<>();
}
}

@SuppressWarnings("unused")
protected ThreadFactory newThreadFactory(String threadNamePrefix, OptimizedCallTarget callTarget) {
return new TruffleCompilerThreadFactory(threadNamePrefix, runtime);
Expand Down Expand Up @@ -326,9 +327,12 @@ private static final class TruffleThreadPoolExecutor extends ThreadPoolExecutor
* queued and active compilations without duplicates and misses.
*/
private final Set<CompilationTask.ExecutorServiceWrapper> allTargets = ConcurrentHashMap.newKeySet();
private final DynamicCompilationThresholds dynamicCompilationThresholds;

private TruffleThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
private TruffleThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
DynamicCompilationThresholds dynamicCompilationThresholds) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
this.dynamicCompilationThresholds = dynamicCompilationThresholds;
}

void flush(EngineData engine) {
Expand Down Expand Up @@ -370,6 +374,24 @@ public boolean remove(Runnable task) {
return super.remove(task);
}

@Override
public <T> Future<T> submit(Callable<T> task) {
Future<T> future = super.submit(task);
scaleThresholds();
return future;
}

private void scaleThresholds() {
if (dynamicCompilationThresholds != null) {
dynamicCompilationThresholds.scaleThresholds();
}
}

@Override
protected void beforeExecute(Thread t, Runnable r) {
scaleThresholds();
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Expand Down Expand Up @@ -531,4 +553,39 @@ public E pollFirst(long timeout, TimeUnit unit) throws InterruptedException {
return super.pollFirst(timeoutMillis, TimeUnit.MILLISECONDS);
}
}

private final class DynamicCompilationThresholds {
private final int threads;
private final double minScale;
private final int minNormalLoad;
private final int maxNormalLoad;
private final double slope;

DynamicCompilationThresholds(int threads, double minScale, int minNormalLoad, int maxNormalLoad) {
this.threads = threads;
this.minScale = minScale;
this.minNormalLoad = minNormalLoad;
this.maxNormalLoad = maxNormalLoad;
this.slope = (1 - minScale) / minNormalLoad;
}

private double load() {
return (double) getQueueSize() / threads;
}

private double scale() {
double x = load();
if (minNormalLoad <= x && x <= maxNormalLoad) {
return 1;
}
if (x < minNormalLoad) {
return slope * x + minScale;
}
return slope * x + (1 - slope * maxNormalLoad);
}

private void scaleThresholds() {
OptimizedTruffleRuntime.getRuntime().setCompilationThresholdScale(FixedPointMath.toFixedPoint(scale()));
}
}
}
Loading