From ddbc103ab8eed4440c520e01ba51a67a9d44895b Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Fri, 23 Jan 2026 15:21:14 -0500 Subject: [PATCH 1/4] single callsite inlining --- .../com/oracle/svm/core/SubstrateOptions.java | 7 +- .../svm/hosted/code/CompilationInfo.java | 7 +- .../oracle/svm/hosted/code/CompileQueue.java | 249 +++++++++++++++--- .../svm/hosted/code/InliningGraphDecoder.java | 60 +++++ 4 files changed, 283 insertions(+), 40 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/InliningGraphDecoder.java diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index dfedba299396..fae58fa62930 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. 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 @@ -891,6 +892,10 @@ public static long getTearDownFailureNanos() { @Option(help = "Perform trivial method inlining in the AOT compiled native image")// public static final HostedOptionKey AOTTrivialInline = new HostedOptionKey<>(true); + @LayerVerifiedOption(kind = Kind.Changed, severity = Severity.Error)// + @Option(help = "Perform single callsite method inlining in the AOT compiled native image")// + public static final HostedOptionKey AOTSingleCallsiteInline = new HostedOptionKey<>(true); + @LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Warn, positional = false)// @Option(help = "file:doc-files/NeverInlineHelp.txt", type = OptionType.Debug)// public static final HostedOptionKey NeverInline = new HostedOptionKey<>(AccumulatingLocatableMultiOptionValue.Strings.build()); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompilationInfo.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompilationInfo.java index d81ad6942138..62e257eee86a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompilationInfo.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompilationInfo.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. 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 @@ -25,6 +26,7 @@ package com.oracle.svm.hosted.code; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; @@ -45,6 +47,9 @@ import jdk.graal.compiler.options.OptionValues; public class CompilationInfo { + int sizeLastRound; + + AtomicInteger callsites = new AtomicInteger(); protected final HostedMethod method; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java index f6b0b195b40c..49f706e5917d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2025, IBM Inc. 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 @@ -43,7 +44,6 @@ import org.graalvm.nativeimage.ImageSingletons; import com.oracle.graal.pointsto.api.PointstoOptions; -import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; import com.oracle.graal.pointsto.meta.HostedProviders; import com.oracle.graal.pointsto.reports.ReportUtils; import com.oracle.graal.pointsto.util.CompletionExecutor; @@ -91,7 +91,6 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.asm.Assembler; -import jdk.graal.compiler.bytecode.BytecodeProvider; import jdk.graal.compiler.code.CompilationResult; import jdk.graal.compiler.core.GraalCompiler; import jdk.graal.compiler.core.common.CompilationIdentifier; @@ -115,7 +114,6 @@ import jdk.graal.compiler.lir.phases.LIRSuites; import jdk.graal.compiler.nodes.CallTargetNode; import jdk.graal.compiler.nodes.ConstantNode; -import jdk.graal.compiler.nodes.EncodedGraph; import jdk.graal.compiler.nodes.FrameState; import jdk.graal.compiler.nodes.GraphEncoder; import jdk.graal.compiler.nodes.GraphState.StageFlag; @@ -130,6 +128,7 @@ import jdk.graal.compiler.nodes.java.MethodCallTargetNode; import jdk.graal.compiler.nodes.spi.CoreProviders; import jdk.graal.compiler.options.OptionValues; +import jdk.graal.compiler.phases.contract.NodeCostUtil; import jdk.graal.compiler.phases.OptimisticOptimizations; import jdk.graal.compiler.phases.Phase; import jdk.graal.compiler.phases.PhaseSuite; @@ -173,6 +172,12 @@ protected PhaseSuite getAfterParseSuite() { } } + /* + * Use the same fallback as + * "An Optimization-Driven Incremental Inline Substitution Algorithm for Just-in-Time Compilers" + */ + private static final int FALLBACK_SIZE = 50000; + protected final HostedUniverse universe; private final Boolean deoptimizeAll; protected final List policies; @@ -205,13 +210,21 @@ protected PhaseSuite getAfterParseSuite() { private final ResolvedJavaType generatedFoldInvocationPluginType; - public record UnpublishedTrivialMethods(CompilationGraph unpublishedGraph, boolean newlyTrivial) { + public record UnpublishedMethodInfo(CompilationGraph unpublishedGraph, boolean newlyTrivial) { } - private final ConcurrentMap unpublishedTrivialMethods = new ConcurrentHashMap<>(); + private final ConcurrentMap unpublishedMethods = new ConcurrentHashMap<>(); + + /* + * Edge case handling: For each method, track that last round that Single Callsite Inlining was + * unsuccessful + */ + private final ConcurrentMap sciLastRoundFailed = new ConcurrentHashMap<>(); private final LayeredDispatchTableFeature layeredDispatchTableSupport = ImageLayerBuildingSupport.buildingSharedLayer() ? LayeredDispatchTableFeature.singleton() : null; + private int inliningRound = 0; + public abstract static class CompileReason { /** * For debugging only: chaining of the compile reason, so that you can track the compilation @@ -369,6 +382,27 @@ public Description getDescription() { } } + protected class SingleCallsiteInlineTask implements Task { + + private final HostedMethod method; + private final Description description; + + SingleCallsiteInlineTask(HostedMethod method) { + this.method = method; + this.description = new Description(method, method.getName()); + } + + @Override + public void run(DebugContext debug) { + doInlineSingleCallsite(debug, method); + } + + @Override + public Description getDescription() { + return description; + } + } + public class ParseTask implements Task { protected final CompileReason reason; @@ -458,7 +492,11 @@ public void finish(DebugContext debug) { hostedHeapDumpHandler.dumpBeforeInlining(); } try (ProgressReporter.ReporterClosable _ = reporter.printInlining()) { + // The trivial stage must be run to handle any forced inlining due to annotations inlineTrivialMethods(debug); + if (SubstrateOptions.AOTSingleCallsiteInline.getValue()) { + inlineSingleCallsiteMethods(debug); + } } if (hostedHeapDumpHandler != null) { hostedHeapDumpHandler.dumpAfterInlining(); @@ -762,12 +800,10 @@ private static boolean checkNewlyTrivial(HostedMethod method, StructuredGraph gr } protected void inlineTrivialMethods(DebugContext debug) throws InterruptedException { - int round = 0; + inliningRound = 0; do { - ProgressReporter.singleton().reportStageProgress(); - inliningProgress = false; - round++; - try (Indent _ = debug.logAndIndent("==== Trivial Inlining round %d%n", round)) { + beginRound(); + try (Indent _ = debug.logAndIndent("==== Trivial Inlining round %d%n", inliningRound)) { runOnExecutor(() -> { universe.getMethods().forEach(method -> { assert method.isOriginalMethod(); @@ -780,17 +816,104 @@ protected void inlineTrivialMethods(DebugContext debug) throws InterruptedExcept }); }); } - for (Map.Entry entry : unpublishedTrivialMethods.entrySet()) { + for (Map.Entry entry : unpublishedMethods.entrySet()) { entry.getKey().compilationInfo.setCompilationGraph(entry.getValue().unpublishedGraph); if (entry.getValue().newlyTrivial) { inliningProgress = true; entry.getKey().compilationInfo.setTrivialMethod(); } } - unpublishedTrivialMethods.clear(); + unpublishedMethods.clear(); + } while (inliningProgress); + } + + /** + * Single callsite methods should be able to be inlined without duplicating code area. Single + * callsite inlining happens after trivial inlining to maximize the amount of trivial inlining + * possible. + * + * Say M1 is trivial and M2 has a single callsite, with M1 as the caller. If we inline M2 first, + * then M1 will likely no longer be trivial. If we inline M1 first, then M2 will no longer have + * a single callsite. It's preferable to miss out on a single instance of inlining rather than + * many - so trivial methods are inlined first. + */ + @SuppressWarnings("try") + protected void inlineSingleCallsiteMethods(DebugContext debug) throws InterruptedException { + inliningRound = 0; + // First count callsites + try (Indent _ = debug.logAndIndent("==== Single Callsite Inlining: counting callsites")) { + runOnExecutor(() -> { + universe.getMethods().forEach(method -> { + assert method.isOriginalMethod(); + for (MultiMethod multiMethod : method.getAllMultiMethods()) { + HostedMethod hMethod = (HostedMethod) multiMethod; + if (hMethod.compilationInfo.getCompilationGraph() != null) { + executor.execute(new SingleCallsiteInlineTask(hMethod)); + } + } + }); + }); + } + + // Inline single callsite methods + do { + beginRound(); + try (Indent _ = debug.logAndIndent("==== Single Callsite Inlining round %n")) { + runOnExecutor(() -> { + universe.getMethods().forEach(method -> { + assert method.isOriginalMethod(); + for (MultiMethod multiMethod : method.getAllMultiMethods()) { + HostedMethod hMethod = (HostedMethod) multiMethod; + if (shouldEvaluateRootForSingleCallsiteInlining(hMethod)) { + executor.execute(new SingleCallsiteInlineTask(hMethod)); + } + } + }); + }); + } + // Publish modified graphs + for (Map.Entry entry : unpublishedMethods.entrySet()) { + entry.getKey().compilationInfo.setCompilationGraph(entry.getValue().unpublishedGraph); + inliningProgress = true; + } + unpublishedMethods.clear(); } while (inliningProgress); } + private boolean shouldEvaluateRootForSingleCallsiteInlining(HostedMethod root) { + if (root.compilationInfo.getCompilationGraph() == null) { + return false; + } + + if (inliningRound == 1) { + /* + * Skip roots that are themselves single callsite methods. Otherwise, it's possible that + * the root and some of its callees are both single callsite methods. In such cases, the + * root becomes unreachable and we wasted effort inlining its callees. + */ + if (root.compilationInfo.callsites.get() != 1) { + return true; + } + } else { + /* + * After the first round, we must visit roots that are single callsite methods which + * failed the inlining threshold last round. Otherwise, their callees will never get + * evaluated. + */ + Integer lastRoundFailed = sciLastRoundFailed.get(root); + if (lastRoundFailed != null && lastRoundFailed.equals(inliningRound - 1)) { + return true; + } + } + return false; + } + + private void beginRound() { + ProgressReporter.singleton().reportStageProgress(); + inliningProgress = false; + inliningRound++; + } + class TrivialInliningPlugin implements InlineInvokePlugin { boolean inlinedDuringDecoding; @@ -810,35 +933,35 @@ public void notifyAfterInline(ResolvedJavaMethod methodToInline) { } } - class InliningGraphDecoder extends PEGraphDecoder { - - InliningGraphDecoder(StructuredGraph graph, Providers providers, TrivialInliningPlugin inliningPlugin) { - super(AnalysisParsedGraph.HOST_ARCHITECTURE, graph, providers, null, - null, - new InlineInvokePlugin[]{inliningPlugin}, - null, null, null, null, - new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), true, false); - } + /** This plugin will allow inlining methods that have a single callsite. */ + class SingleCallsiteInliningPlugin implements InlineInvokePlugin { + boolean inlinedDuringDecoding; @Override - protected EncodedGraph lookupEncodedGraph(ResolvedJavaMethod method, BytecodeProvider intrinsicBytecodeProvider) { - return ((HostedMethod) method).compilationInfo.getCompilationGraph().getEncodedGraph(); + public InlineInfo shouldInlineInvoke(GraphBuilderContext b, ResolvedJavaMethod method, ValueNode[] args) { + if (makeSingleCallsiteInlineDecision((HostedMethod) b.getMethod(), (HostedMethod) method) && b.recursiveInliningDepth(method) == 0) { + return InlineInfo.createStandardInlineInfo(method); + } else { + return InlineInfo.DO_NOT_INLINE_WITH_EXCEPTION; + } } @Override - protected LoopScope trySimplifyInvoke(PEMethodScope methodScope, LoopScope loopScope, InvokeData invokeData, MethodCallTargetNode callTarget) { - return super.trySimplifyInvoke(methodScope, loopScope, invokeData, callTarget); + public void notifyAfterInline(ResolvedJavaMethod methodToInline) { + inlinedDuringDecoding = true; } } // Wrapper to clearly identify phase - class TrivialInlinePhase extends Phase { - final InliningGraphDecoder decoder; + class InlinePhase extends Phase { + final PEGraphDecoder decoder; final HostedMethod method; + final String name; - TrivialInlinePhase(InliningGraphDecoder decoder, HostedMethod method) { + InlinePhase(PEGraphDecoder decoder, HostedMethod method, String name) { this.decoder = decoder; this.method = method; + this.name = name; } @Override @@ -848,10 +971,11 @@ protected void run(StructuredGraph graph) { @Override public CharSequence getName() { - return "TrivialInline"; + return name; } } + @SuppressWarnings("try") private void doInlineTrivial(DebugContext debug, HostedMethod method) { /* * Before doing any work, check if there is any potential for inlining. @@ -875,7 +999,7 @@ private void doInlineTrivial(DebugContext debug, HostedMethod method) { try (var _ = debug.scope("InlineTrivial", graph, method, this)) { var inliningPlugin = new TrivialInliningPlugin(); var decoder = new InliningGraphDecoder(graph, providers, inliningPlugin); - new TrivialInlinePhase(decoder, method).apply(graph); + new InlinePhase(decoder, method, "TrivialInline").apply(graph); if (inliningPlugin.inlinedDuringDecoding) { CanonicalizerPhase.create().apply(graph, providers); @@ -900,7 +1024,7 @@ private void doInlineTrivial(DebugContext debug, HostedMethod method) { * non-deterministic. This is why we are saving graphs to be published at the * end of each round. */ - unpublishedTrivialMethods.put(method, new UnpublishedTrivialMethods(CompilationGraph.encode(graph), checkNewlyTrivial(method, graph))); + unpublishedMethods.put(method, new UnpublishedMethodInfo(CompilationGraph.encode(graph), checkNewlyTrivial(method, graph))); } } } catch (Throwable ex) { @@ -908,13 +1032,28 @@ private void doInlineTrivial(DebugContext debug, HostedMethod method) { } } + private void doInlineSingleCallsite(DebugContext debug, HostedMethod method) { + var providers = runtimeConfig.lookupBackend(method).getProviders(); + var graph = method.compilationInfo.createGraph(debug, getCustomizedOptions(method, debug), CompilationIdentifier.INVALID_COMPILATION_ID, false); + try (var _ = debug.scope("InlineSingleCallsites", graph, method, this)) { + var inliningPlugin = new SingleCallsiteInliningPlugin(); + var decoder = new InliningGraphDecoder(graph, providers, inliningPlugin); + new InlinePhase(decoder, method, "SingleCallsiteInline").apply(graph); + + if (inliningPlugin.inlinedDuringDecoding) { + CanonicalizerPhase.create().apply(graph, providers); + unpublishedMethods.put(method, new UnpublishedMethodInfo(CompilationGraph.encode(graph), true)); + } + if (inliningPlugin.inlinedDuringDecoding || inliningRound == 0) { + method.compilationInfo.sizeLastRound = NodeCostUtil.computeGraphSize(graph); + } + } catch (Throwable ex) { + throw debug.handle(ex); + } + } + private boolean makeInlineDecision(HostedMethod method, HostedMethod callee) { - if (!LayeredImageOptions.UseSharedLayerStrengthenedGraphs.getValue() && callee.compilationInfo.getCompilationGraph() == null) { - /* - * We have compiled this method in a prior layer or this method's compilation is delayed - * to the application layer, but don't have the graph available here. - */ - assert callee.isCompiledInPriorLayer() || callee.wrapped.isDelayed() : method; + if (!isCalleeGraphAvailable(method, callee)) { return false; } if (universe.hostVM().neverInlineTrivial(method.getWrapped(), callee.getWrapped())) { @@ -929,6 +1068,40 @@ private boolean makeInlineDecision(HostedMethod method, HostedMethod callee) { return false; } + private boolean makeSingleCallsiteInlineDecision(HostedMethod caller, HostedMethod callee) { + if (!isCalleeGraphAvailable(caller, callee) || !callee.getWrapped().canBeInlined()) { + return false; + } + + // During the preliminary single callsite inlining round we must count callsites + if (inliningRound == 0) { + callee.compilationInfo.callsites.incrementAndGet(); + return false; + } + + if (callee.compilationInfo.callsites.get() != 1) { + return false; + } + + if (caller.compilationInfo.sizeLastRound + callee.compilationInfo.sizeLastRound >= FALLBACK_SIZE) { + sciLastRoundFailed.put(callee, inliningRound); + return false; + } + return true; + } + + private static boolean isCalleeGraphAvailable(HostedMethod caller, HostedMethod callee) { + if (!LayeredImageOptions.UseSharedLayerStrengthenedGraphs.getValue() && callee.compilationInfo.getCompilationGraph() == null) { + /* + * We have compiled this method in a prior layer or this method's compilation is delayed + * to the application layer, but don't have the graph available here. + */ + assert callee.isCompiledInPriorLayer() || callee.wrapped.isDelayed() : caller; + return false; + } + return true; + } + private static boolean mustNotAllocateCallee(HostedMethod method) { return ImageSingletons.lookup(RestrictHeapAccessCallees.class).mustNotAllocate(method); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/InliningGraphDecoder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/InliningGraphDecoder.java new file mode 100644 index 000000000000..973c24e03eb2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/InliningGraphDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 2026, IBM Inc. 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 com.oracle.svm.hosted.code; + +import java.util.concurrent.ConcurrentHashMap; + +import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; +import com.oracle.svm.hosted.meta.HostedMethod; + +import jdk.graal.compiler.bytecode.BytecodeProvider; +import jdk.graal.compiler.nodes.EncodedGraph; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.graphbuilderconf.InlineInvokePlugin; +import jdk.graal.compiler.phases.util.Providers; +import jdk.graal.compiler.replacements.PEGraphDecoder; + +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * This is a general inlining decoder that is used for both trivial inlining and single callsite + * inlining. Different plugins allow for different functionality. + */ +class InliningGraphDecoder extends PEGraphDecoder { + + InliningGraphDecoder(StructuredGraph graph, Providers providers, InlineInvokePlugin inliningPlugin) { + super(AnalysisParsedGraph.HOST_ARCHITECTURE, graph, providers, null, + null, + new InlineInvokePlugin[]{inliningPlugin}, + null, null, null, null, + new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), true, false); + } + + @Override + protected EncodedGraph lookupEncodedGraph(ResolvedJavaMethod method, BytecodeProvider intrinsicBytecodeProvider) { + return ((HostedMethod) method).compilationInfo.getCompilationGraph().getEncodedGraph(); + } +} From 999c51ab530ce5e7ebe1785d32b88ac8424a96fa Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Wed, 17 Jun 2026 12:33:25 -0400 Subject: [PATCH 2/4] add mx test for inliner --- .github/workflows/main.yml | 4 +- substratevm/mx.substratevm/mx_substratevm.py | 136 ++++++++++++++++++ substratevm/mx.substratevm/suite.py | 13 ++ .../oracle/svm/hosted/code/CompileQueue.java | 8 +- .../src/com/oracle/svm/test/sci/SciSmoke.java | 57 ++++++++ 5 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.test.sci/src/com/oracle/svm/test/sci/SciSmoke.java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 950b1ea85b28..31340ad8ce63 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -130,10 +130,10 @@ jobs: - os: ubuntu-24.04 env: JDK_VERSION: "latest" - GATE_TAGS: "build,debuginfotest" + GATE_TAGS: "build,debuginfotest,scismoketest" PRIMARY: "substratevm" - env: - JDK_VERSION: "latest" + JDK_VERSION: "25" GATE_TAGS: "hellomodule" PRIMARY: "substratevm" # /sulong diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 6f08a88bafad..82515a518351 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -262,6 +262,7 @@ def __getattr__(self, name): 'terminus', 'debuginfotest', 'standalone_pointsto_unittests', + 'scismoketest', 'native_unittests', 'all_native_unittests', 'java_desktop_integration', @@ -508,6 +509,11 @@ def svm_gate_body(args, tasks): with native_image_context(IMAGE_ASSERTION_FLAGS) as native_image: layereddebuginfotest(['--output-path', svmbuild_dir()] + args.extra_image_builder_arguments) + with Task('image scismoketest', tasks, tags=[GraalTags.scismoketest]) as t: + if t: + with native_image_context(IMAGE_ASSERTION_FLAGS) as native_image: + scismoketest(['--output-path', svmbuild_dir()] + args.extra_image_builder_arguments) + with Task('image debughelpertest', tasks, tags=[GraalTags.debuginfotest]) as t: if t: if mx.is_windows(): @@ -1482,6 +1488,117 @@ def testhello_ni_args(cincludepath, sourcepath): '-H:DebugInfoSourceSearchPath=' + sourcepath, ]) + +def _run_scismoke_binary(binary_path): + expected_output = 'PASS' + os.linesep + actual_output = [] + + def _collector(x): + actual_output.append(x) + mx.log(x) + + mx.run([binary_path], out=_collector) + + # Basic correctness check. + if ''.join(actual_output) != expected_output: + raise Exception('Unexpected output: ' + str(actual_output) + ' != ' + str([expected_output])) + + +class _ScismokeHistogramTracker: + _HISTOGRAM_HEADER = 'Code Size; Nodes Parsing' + _HISTOGRAM_FOOTER = 'Size all methods' + + def __init__(self): + self.in_histogram = False + self.has_single = False + self.has_multi = False + self.verified_histogram = False + + def process_line(self, line): + # Only verify within method histogram bounds. + if line.startswith(self._HISTOGRAM_HEADER): + self.in_histogram = True + self.verified_histogram = True + elif self.in_histogram: + if line.startswith(self._HISTOGRAM_FOOTER): + self.in_histogram = False + else: + if 'singleCallsiteHelper' in line: + self.has_single = True + if 'multiCallsiteHelper' in line: + self.has_multi = True + + +def _find_methodhistogram_file(reports_dir): + matches = glob(join(reports_dir, 'methodhistogram_*.txt')) + if not matches: + return None + return max(matches, key=os.path.getmtime) + + +def _parse_methodhistogram_file(histogram_file): + tracker = _ScismokeHistogramTracker() + with open(histogram_file, 'r', encoding='utf-8') as histogram: + for line in histogram: + tracker.process_line(line.rstrip('\n')) + return tracker + + +def _build_scismoke_image(native_image, build_args, reports_dir): + def _log_output(line): + mx.log(line) + + native_image(build_args, out=_log_output, err=_log_output) + histogram_file = _find_methodhistogram_file(reports_dir) + if histogram_file is None: + raise Exception('PrintMethodHistogram report not found in ' + reports_dir) + mx.log('Parsing method histogram from: ' + histogram_file) + tracker = _parse_methodhistogram_file(histogram_file) + if not tracker.verified_histogram: + raise Exception('PrintMethodHistogram output not found in ' + histogram_file) + return tracker + + +def _assert_scismoke_histogram(tracker, is_sci_enabled, variant_name): + if not tracker.has_multi: + raise Exception(variant_name + ': multiCallsiteHelper should always appear in histogram') + if is_sci_enabled: + if tracker.has_single: + raise Exception(variant_name + ': singleCallsiteHelper should not appear in histogram with SCI enabled') + elif not tracker.has_single: + raise Exception(variant_name + ': singleCallsiteHelper should appear in histogram with SCI disabled') + + +def _scismoketest(native_image, path, args): + mx_util.ensure_dir_exists(path) + + base_args = [ + '--native-image-info', + '-cp', classpath('com.oracle.svm.test.sci'), + '--initialize-at-build-time=com.oracle.svm.test.sci', + 'com.oracle.svm.test.sci.SciSmoke', + ] + args + + def build_and_run(variant_name, sci_flag, is_sci_enabled): + variant_path = join(path, variant_name) + mx_util.ensure_dir_exists(variant_path) + binary_path = join(variant_path, 'scismoke') + build_args = base_args + svm_experimental_options([ + sci_flag, + '-H:+PrintMethodHistogram', + ]) + [ + '-o', binary_path, + ] + reports_dir = join(variant_path, 'reports') + mx.log(f'native_image {build_args}') + tracker = _build_scismoke_image(native_image, build_args, reports_dir) + _assert_scismoke_histogram(tracker, is_sci_enabled, variant_name) + _run_scismoke_binary(binary_path) + + build_and_run('sci_on', '-H:+AOTSingleCallsiteInline', True) + build_and_run('sci_off', '-H:-AOTSingleCallsiteInline', False) + + def _gdbdebughelperstest(native_image, path, with_isolates_only, args): # ====== check gdb version ====== @@ -2386,6 +2503,25 @@ def run_helloworld_command(args, config, command_name, native_image_wrapper=None config=config, ) +@mx.command(suite_name=suite.name, command_name='scismoketest', usage_msg='[options]') +def scismoketest(args, config=None): + """ + Build and run a simple app with AOTSingleCallsiteInline enabled and disabled. + The output of the app is verified for correctness. + PrintMethodHistogram is parsed to verify the expected inlining happened. + """ + parser = ArgumentParser(prog='mx scismoketest') + all_args = ['--output-path', '--build-only'] + masked_args = [_mask(arg, all_args) for arg in args] + parser.add_argument(all_args[0], metavar='', nargs=1, help='Path of the generated image', default=[svmbuild_dir()]) + parser.add_argument('image_args', nargs='*', default=[]) + parsed = parser.parse_args(masked_args) + output_path = unmask(parsed.output_path)[0] + native_image_context_run( + lambda native_image, a: + _scismoketest(native_image, output_path, a), unmask(parsed.image_args), + config=config + ) @mx.command(suite_name=suite.name, command_name='debuginfotest', usage_msg='[options]') def debuginfotest(args, config=None): diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index 35da883a28d0..40e29e52f330 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -1274,6 +1274,19 @@ "testProject": True, }, + "com.oracle.svm.test.sci": { + "subDir": "src", + "sourceDirs": ["src"], + "dependencies": [ + "SVM", + ], + "checkstyle": "com.oracle.svm.test", + "workingSets": "SVM", + "javaCompliance": "22+", + "spotbugs": "false", + "jacoco": "exclude", + }, + "com.oracle.svm.with.space.test": { "subDir": "src", "sourceDirs": ["src"], diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java index 49f706e5917d..eb7954446d91 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/code/CompileQueue.java @@ -845,8 +845,8 @@ protected void inlineSingleCallsiteMethods(DebugContext debug) throws Interrupte runOnExecutor(() -> { universe.getMethods().forEach(method -> { assert method.isOriginalMethod(); - for (MultiMethod multiMethod : method.getAllMultiMethods()) { - HostedMethod hMethod = (HostedMethod) multiMethod; + for (MethodVariant methodVariant : method.getAllMethodVariants()) { + HostedMethod hMethod = (HostedMethod) methodVariant; if (hMethod.compilationInfo.getCompilationGraph() != null) { executor.execute(new SingleCallsiteInlineTask(hMethod)); } @@ -862,8 +862,8 @@ protected void inlineSingleCallsiteMethods(DebugContext debug) throws Interrupte runOnExecutor(() -> { universe.getMethods().forEach(method -> { assert method.isOriginalMethod(); - for (MultiMethod multiMethod : method.getAllMultiMethods()) { - HostedMethod hMethod = (HostedMethod) multiMethod; + for (MethodVariant methodVariant : method.getAllMethodVariants()) { + HostedMethod hMethod = (HostedMethod) methodVariant; if (shouldEvaluateRootForSingleCallsiteInlining(hMethod)) { executor.execute(new SingleCallsiteInlineTask(hMethod)); } diff --git a/substratevm/src/com.oracle.svm.test.sci/src/com/oracle/svm/test/sci/SciSmoke.java b/substratevm/src/com.oracle.svm.test.sci/src/com/oracle/svm/test/sci/SciSmoke.java new file mode 100644 index 000000000000..54f10b5c4dfa --- /dev/null +++ b/substratevm/src/com.oracle.svm.test.sci/src/com/oracle/svm/test/sci/SciSmoke.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, 2026, 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 com.oracle.svm.test.sci; + +import com.oracle.svm.core.NeverInlineTrivial; + +public class SciSmoke { + + @NeverInlineTrivial(reason = "Avoid trivial inliner interference") + private static int singleCallsiteHelper(int value) { + return value * 2; + } + + @NeverInlineTrivial(reason = "Avoid trivial inliner interference") + private static int multiCallsiteHelper(int value) { + return value + 2; + } + + private static int useSingleCallsite() { + return singleCallsiteHelper(1); + } + + private static int useMultiCallsite() { + return multiCallsiteHelper(2) + multiCallsiteHelper(3); + } + + public static void main(String[] args) { + if (useSingleCallsite() + useMultiCallsite() == 11) { + System.out.println("PASS"); + } else { + System.out.println("FAIL"); + } + } +} From 7d9d5a824de8b480406a5cd068d67d49f1303379 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Fri, 19 Jun 2026 11:25:01 -0400 Subject: [PATCH 3/4] make inliner option off by default and experimental --- .../src/com/oracle/svm/core/SubstrateOptions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index fae58fa62930..0b53490dbb61 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -893,8 +893,8 @@ public static long getTearDownFailureNanos() { public static final HostedOptionKey AOTTrivialInline = new HostedOptionKey<>(true); @LayerVerifiedOption(kind = Kind.Changed, severity = Severity.Error)// - @Option(help = "Perform single callsite method inlining in the AOT compiled native image")// - public static final HostedOptionKey AOTSingleCallsiteInline = new HostedOptionKey<>(true); + @Option(help = "Perform single callsite method inlining in the AOT compiled native image", stability = OptionStability.EXPERIMENTAL)// + public static final HostedOptionKey AOTSingleCallsiteInline = new HostedOptionKey<>(false); @LayerVerifiedOption(kind = Kind.Removed, severity = Severity.Warn, positional = false)// @Option(help = "file:doc-files/NeverInlineHelp.txt", type = OptionType.Debug)// From 5986cb21f33efa625d58d79136fa176819d5ed78 Mon Sep 17 00:00:00 2001 From: Robert Toyonaga Date: Fri, 19 Jun 2026 11:33:10 -0400 Subject: [PATCH 4/4] fix --- .github/workflows/main.yml | 2 +- substratevm/mx.substratevm/mx_substratevm.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31340ad8ce63..136660d05788 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -133,7 +133,7 @@ jobs: GATE_TAGS: "build,debuginfotest,scismoketest" PRIMARY: "substratevm" - env: - JDK_VERSION: "25" + JDK_VERSION: "latest" GATE_TAGS: "hellomodule" PRIMARY: "substratevm" # /sulong diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 82515a518351..d05470fa30c9 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -1501,7 +1501,7 @@ def _collector(x): # Basic correctness check. if ''.join(actual_output) != expected_output: - raise Exception('Unexpected output: ' + str(actual_output) + ' != ' + str([expected_output])) + mx.abort('Unexpected output: ' + str(actual_output) + ' != ' + str([expected_output])) class _ScismokeHistogramTracker: @@ -1551,22 +1551,22 @@ def _log_output(line): native_image(build_args, out=_log_output, err=_log_output) histogram_file = _find_methodhistogram_file(reports_dir) if histogram_file is None: - raise Exception('PrintMethodHistogram report not found in ' + reports_dir) + mx.abort('PrintMethodHistogram report not found in ' + reports_dir) mx.log('Parsing method histogram from: ' + histogram_file) tracker = _parse_methodhistogram_file(histogram_file) if not tracker.verified_histogram: - raise Exception('PrintMethodHistogram output not found in ' + histogram_file) + mx.abort('PrintMethodHistogram output not found in ' + histogram_file) return tracker def _assert_scismoke_histogram(tracker, is_sci_enabled, variant_name): if not tracker.has_multi: - raise Exception(variant_name + ': multiCallsiteHelper should always appear in histogram') + mx.abort(variant_name + ': multiCallsiteHelper should always appear in histogram') if is_sci_enabled: if tracker.has_single: - raise Exception(variant_name + ': singleCallsiteHelper should not appear in histogram with SCI enabled') + mx.abort(variant_name + ': singleCallsiteHelper should not appear in histogram with SCI enabled') elif not tracker.has_single: - raise Exception(variant_name + ': singleCallsiteHelper should appear in histogram with SCI disabled') + mx.abort(variant_name + ': singleCallsiteHelper should appear in histogram with SCI disabled') def _scismoketest(native_image, path, args):