diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DynamicCompilationThresholdsTest.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DynamicCompilationThresholdsTest.java new file mode 100644 index 000000000000..7612562655a3 --- /dev/null +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/DynamicCompilationThresholdsTest.java @@ -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 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 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; + } +} diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/BackgroundCompileQueue.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/BackgroundCompileQueue.java index a8da704feaa4..17e1182ab3f1 100644 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/BackgroundCompileQueue.java +++ b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/BackgroundCompileQueue.java @@ -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; @@ -149,10 +150,25 @@ private ExecutorService getExecutorService(OptimizedCallTarget callTarget) { long compilerIdleDelay = runtime.getCompilerIdleDelay(callTarget); long keepAliveTime = compilerIdleDelay >= 0 ? compilerIdleDelay : 0; - BlockingQueue queue = createQueue(callTarget, threads); + BlockingQueue 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 @@ -165,21 +181,6 @@ private ExecutorService getExecutorService(OptimizedCallTarget callTarget) { } } - private BlockingQueue 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); @@ -326,9 +327,12 @@ private static final class TruffleThreadPoolExecutor extends ThreadPoolExecutor * queued and active compilations without duplicates and misses. */ private final Set allTargets = ConcurrentHashMap.newKeySet(); + private final DynamicCompilationThresholds dynamicCompilationThresholds; - private TruffleThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) { + private TruffleThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, + DynamicCompilationThresholds dynamicCompilationThresholds) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + this.dynamicCompilationThresholds = dynamicCompilationThresholds; } void flush(EngineData engine) { @@ -370,6 +374,24 @@ public boolean remove(Runnable task) { return super.remove(task); } + @Override + public Future submit(Callable task) { + Future 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); @@ -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())); + } + } } diff --git a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/DynamicThresholdsQueue.java b/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/DynamicThresholdsQueue.java deleted file mode 100644 index 991351ed5ae8..000000000000 --- a/truffle/src/com.oracle.truffle.runtime/src/com/oracle/truffle/runtime/DynamicThresholdsQueue.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * The Universal Permissive License (UPL), Version 1.0 - * - * Subject to the condition set forth below, permission is hereby granted to any - * person obtaining a copy of this software, associated documentation and/or - * data (collectively the "Software"), free of charge and under any and all - * copyright rights in the Software, and any and all patent rights owned or - * freely licensable by each licensor hereunder covering either (i) the - * unmodified Software as contributed to or provided by such licensor, or (ii) - * the Larger Works (as defined below), to deal in both - * - * (a) the Software, and - * - * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if - * one is included with the Software each a "Larger Work" to which the Software - * is contributed by such licensors), - * - * without restriction, including without limitation the rights to copy, create - * derivative works of, display, perform, and distribute the Software and make, - * use, sell, offer for sale, import, export, have made, and have sold the - * Software and the Larger Work(s), and to sublicense the foregoing rights on - * either these or other terms. - * - * This license is subject to the following condition: - * - * The above copyright notice and either this complete permission notice or at a - * minimum a reference to the UPL must be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package com.oracle.truffle.runtime; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * See https://github.com/oracle/graal/blob/master/truffle/docs/TraversingCompilationQueue.md . - */ -final class DynamicThresholdsQueue extends TraversingBlockingQueue { - - private final int threads; - private final double minScale; - private final int minNormalLoad; - private final int maxNormalLoad; - private final double slope; - - DynamicThresholdsQueue(int threads, double minScale, int minNormalLoad, int maxNormalLoad, BlockingQueue entries) { - super(entries); - this.threads = threads; - this.minScale = minScale; - this.minNormalLoad = minNormalLoad; - this.maxNormalLoad = maxNormalLoad; - this.slope = (1 - minScale) / minNormalLoad; - } - - private double load() { - return (double) entries.size() / threads; - } - - @Override - public boolean add(Runnable e) { - scaleThresholds(); - return super.add(e); - } - - @Override - public boolean offer(Runnable e) { - scaleThresholds(); - return super.offer(e); - } - - @Override - public boolean offer(Runnable e, long timeout, TimeUnit unit) throws InterruptedException { - scaleThresholds(); - return super.offer(e, timeout, unit); - } - - @Override - public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { - scaleThresholds(); - return super.poll(timeout, unit); - } - - @Override - public Runnable poll() { - scaleThresholds(); - return super.poll(); - } - - private void scaleThresholds() { - OptimizedTruffleRuntime.getRuntime().setCompilationThresholdScale(FixedPointMath.toFixedPoint(scale())); - } - - /** - * @return f(x) where x is the load of the queue and f is a function that - * - * - Grows linearly between coordinates (0, minScale) and (minNormalLoad, 1) - * - * - Equals 1 for all x between minNormalLoad and maxNormalLoad (inclusively) - * - * - For all x > maxNormalLoad - grows at the same rate as for x < minNormalLoad, but - * starting at coordinate (maxNormalLoad, 1) - */ - 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); - } -}