From 110d50a649363c3c48022d77f5d87873eab74fc1 Mon Sep 17 00:00:00 2001 From: David Kozak Date: Wed, 18 Jun 2025 16:07:58 +0200 Subject: [PATCH 1/4] introduce Transitive Effect Summary Analysis (TESA) --- .../jdk/graal/compiler/nodes/InvokeNode.java | 20 +- .../nodes/InvokeWithExceptionNode.java | 22 +- .../graal/compiler/nodes/StructuredGraph.java | 7 + .../compiler/nodes/spi/MemoryEdgeProxy.java | 5 +- .../pointsto/AbstractAnalysisEngine.java | 4 +- .../graal/pointsto/PointsToAnalysis.java | 8 +- .../com/oracle/graal/pointsto/util/Timer.java | 4 +- .../graal/pointsto/util/TimerCollection.java | 14 +- .../svm/hosted/NativeImageGenerator.java | 37 +++ .../oracle/svm/hosted/ProgressReporter.java | 52 +++- .../svm/hosted/SubstrateStrengthenGraphs.java | 5 + .../hosted/analysis/tesa/AbstractTesa.java | 287 ++++++++++++++++++ .../analysis/tesa/KilledLocationTesa.java | 113 +++++++ .../analysis/tesa/TesaReverseCallGraph.java | 179 +++++++++++ ...TransitiveEffectSummaryAnalysisEngine.java | 256 ++++++++++++++++ .../analysis/tesa/effect/LocationEffect.java | 129 ++++++++ .../analysis/tesa/effect/TesaEffect.java | 72 +++++ .../oracle/svm/hosted/code/CompileQueue.java | 5 + .../oracle/svm/util/ImageBuildStatistics.java | 11 + .../NativeImageWasmGeneratorRunner.java | 2 +- .../CLIVisualizationSupport.java | 14 +- 21 files changed, 1207 insertions(+), 39 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/AbstractTesa.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java index 2e740ea1d01e..cfe7a5f06750 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java @@ -74,7 +74,11 @@ public final class InvokeNode extends AbstractMemoryCheckpoint implements Invoke protected int bci; protected boolean polymorphic; protected InlineControl inlineControl; - protected final LocationIdentity identity; + /** + * The location killed by the invoke. Typically, it will be {@link LocationIdentity#any()}, but + * an interprocedural analysis can provide more precise location. + */ + protected LocationIdentity killedLocationIdentity; private boolean isInOOMETry; private boolean sideEffect; @@ -82,21 +86,21 @@ public InvokeNode(CallTargetNode callTarget, int bci) { this(callTarget, bci, callTarget.returnStamp().getTrustedStamp()); } - public InvokeNode(CallTargetNode callTarget, int bci, LocationIdentity identity) { - this(callTarget, bci, callTarget.returnStamp().getTrustedStamp(), identity); + public InvokeNode(CallTargetNode callTarget, int bci, LocationIdentity killedLocationIdentity) { + this(callTarget, bci, callTarget.returnStamp().getTrustedStamp(), killedLocationIdentity); } public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp) { this(callTarget, bci, stamp, LocationIdentity.any()); } - public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp, LocationIdentity identity) { + public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp, LocationIdentity killedLocationIdentity) { super(TYPE, stamp); this.callTarget = callTarget; this.bci = bci; this.polymorphic = false; this.inlineControl = InlineControl.Normal; - this.identity = identity; + this.killedLocationIdentity = killedLocationIdentity; this.sideEffect = super.hasSideEffect(); } @@ -125,6 +129,10 @@ public void setPolymorphic(boolean value) { this.polymorphic = value; } + public void setKilledLocationIdentity(LocationIdentity identity) { + this.killedLocationIdentity = identity; + } + @Override public void setInlineControl(InlineControl control) { this.inlineControl = control; @@ -146,7 +154,7 @@ public Map getDebugProperties(Map map) { @Override public LocationIdentity getKilledLocationIdentity() { - return identity; + return killedLocationIdentity; } @Override diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java index 11c8ccc673b9..c0bfcf7e6668 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java @@ -34,6 +34,8 @@ import java.util.Map; +import org.graalvm.word.LocationIdentity; + import jdk.graal.compiler.core.common.type.Stamp; import jdk.graal.compiler.graph.IterableNodeType; import jdk.graal.compiler.graph.Node; @@ -49,8 +51,6 @@ import jdk.graal.compiler.nodes.spi.SimplifierTool; import jdk.graal.compiler.nodes.spi.UncheckedInterfaceProvider; import jdk.graal.compiler.nodes.util.GraphUtil; -import org.graalvm.word.LocationIdentity; - import jdk.vm.ci.code.BytecodeFrame; // @formatter:off @@ -70,6 +70,12 @@ public final class InvokeWithExceptionNode extends WithExceptionNode implements protected boolean polymorphic; protected InlineControl inlineControl; private boolean isInOOMETry; + /** + * The location killed by the invoke. Typically, it will be {@link LocationIdentity#any()}, but + * an interprocedural analysis can provide more precise location. + */ + private LocationIdentity killedLocationIdentity = LocationIdentity.any(); + private boolean sideEffect = true; public InvokeWithExceptionNode(CallTargetNode callTarget, AbstractBeginNode exceptionEdge, int bci) { super(TYPE, callTarget.returnStamp().getTrustedStamp()); @@ -158,12 +164,16 @@ public void setStateAfter(FrameState stateAfter) { @Override public boolean hasSideEffect() { - return true; + return sideEffect; } @Override public LocationIdentity getKilledLocationIdentity() { - return LocationIdentity.any(); + return killedLocationIdentity; + } + + public void setKilledLocationIdentity(LocationIdentity killedLocationIdentity) { + this.killedLocationIdentity = killedLocationIdentity; } @Override @@ -264,4 +274,8 @@ public boolean isInOOMETry() { public void setInOOMETry(boolean isInOOMETry) { this.isInOOMETry = isInOOMETry; } + + public void setSideEffect(boolean withSideEffects) { + this.sideEffect = withSideEffects; + } } diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java index ffeaf0c0e350..8332d8acce6c 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java @@ -861,6 +861,13 @@ public ParameterNode getParameter(int index) { return null; } + /** + * Returns an iterable for looping over all {@link Invoke}s that are associated with + * {@link MethodCallTargetNode}s. Note that since the internal iteration is based on + * {@link MethodCallTargetNode}s, it does not cover other kinds of invokes in the graph. + * If accessing all invokes is necessary for correctness, use a loop over all nodes with + * {@link #getNodes()} and an {@code instanceof} check instead. + */ public Iterable getInvokes() { final Iterator callTargets = getNodes(MethodCallTargetNode.TYPE).iterator(); return new Iterable<>() { diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/spi/MemoryEdgeProxy.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/spi/MemoryEdgeProxy.java index 5507d9f27b7c..5fd8b57abe12 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/spi/MemoryEdgeProxy.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/spi/MemoryEdgeProxy.java @@ -24,10 +24,11 @@ */ package jdk.graal.compiler.nodes.spi; -import jdk.graal.compiler.nodes.memory.MemoryKill; import org.graalvm.word.LocationIdentity; -public interface MemoryEdgeProxy extends Proxy, MemoryKill { +import jdk.graal.compiler.nodes.memory.SingleMemoryKill; + +public interface MemoryEdgeProxy extends Proxy, SingleMemoryKill { LocationIdentity getLocationIdentity(); } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java index ae17eb81f750..364cb78153fa 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/AbstractAnalysisEngine.java @@ -257,8 +257,8 @@ public void cleanupAfterAnalysis() { @Override public void printTimerStatistics(PrintWriter out) { // todo print reachability here - StatisticsPrinter.print(out, "features_time_ms", processFeaturesTimer.getTotalTime()); - StatisticsPrinter.print(out, "total_analysis_time_ms", analysisTimer.getTotalTime()); + StatisticsPrinter.print(out, "features_time_ms", processFeaturesTimer.getTotalTimeMs()); + StatisticsPrinter.print(out, "total_analysis_time_ms", analysisTimer.getTotalTimeMs()); StatisticsPrinter.printLast(out, "total_memory_bytes", analysisTimer.getTotalMemory()); } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java index 0df4afcc5c24..858ae79af882 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/PointsToAnalysis.java @@ -187,10 +187,10 @@ protected CompletionExecutor.Timing getTiming() { @Override public void printTimerStatistics(PrintWriter out) { - StatisticsPrinter.print(out, "typeflow_time_ms", typeFlowTimer.getTotalTime()); - StatisticsPrinter.print(out, "verify_time_ms", verifyHeapTimer.getTotalTime()); - StatisticsPrinter.print(out, "features_time_ms", processFeaturesTimer.getTotalTime()); - StatisticsPrinter.print(out, "total_analysis_time_ms", analysisTimer.getTotalTime()); + StatisticsPrinter.print(out, "typeflow_time_ms", typeFlowTimer.getTotalTimeMs()); + StatisticsPrinter.print(out, "verify_time_ms", verifyHeapTimer.getTotalTimeMs()); + StatisticsPrinter.print(out, "features_time_ms", processFeaturesTimer.getTotalTimeMs()); + StatisticsPrinter.print(out, "total_analysis_time_ms", analysisTimer.getTotalTimeMs()); StatisticsPrinter.printLast(out, "total_memory_bytes", analysisTimer.getTotalMemory()); } diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/Timer.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/Timer.java index 91a382854a2a..83d944227795 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/Timer.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/Timer.java @@ -54,8 +54,8 @@ public void stop() { } /** Get timer total time in milliseconds. */ - public double getTotalTime() { - return totalTime / 1000000d; + public double getTotalTimeMs() { + return totalTime / 1_000_000d; } /** Get total VM memory in bytes. */ diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java index acb0be088563..bdedc98c7e56 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/util/TimerCollection.java @@ -24,16 +24,18 @@ */ package com.oracle.graal.pointsto.util; -import com.oracle.graal.pointsto.reports.StatisticsPrinter; -import com.oracle.svm.util.ImageBuildStatistics; -import jdk.graal.compiler.debug.GraalError; -import org.graalvm.nativeimage.ImageSingletons; - import java.io.PrintWriter; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.graalvm.nativeimage.ImageSingletons; + +import com.oracle.graal.pointsto.reports.StatisticsPrinter; +import com.oracle.svm.util.ImageBuildStatistics; + +import jdk.graal.compiler.debug.GraalError; + public class TimerCollection implements ImageBuildStatistics.TimerCollectionPrinter { public static TimerCollection singleton() { @@ -102,7 +104,7 @@ public void printTimerStats(PrintWriter out) { Iterator it = this.timers.values().iterator(); while (it.hasNext()) { Timer timer = it.next(); - StatisticsPrinter.print(out, timer.getName() + "_time", ((int) timer.getTotalTime())); + StatisticsPrinter.print(out, timer.getName() + "_time", ((int) timer.getTotalTimeMs())); if (it.hasNext()) { StatisticsPrinter.print(out, timer.getName() + "_memory", timer.getTotalMemory()); } else { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index b7ced2130d15..0d3061cefbf6 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -201,6 +201,7 @@ import com.oracle.svm.hosted.analysis.ReachabilityTracePrinter; import com.oracle.svm.hosted.analysis.SVMAnalysisMetaAccess; import com.oracle.svm.hosted.analysis.SubstrateUnsupportedFeatures; +import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; import com.oracle.svm.hosted.annotation.SubstrateAnnotationExtractor; import com.oracle.svm.hosted.c.CAnnotationProcessorCache; import com.oracle.svm.hosted.c.CConstantValueSupportImpl; @@ -253,6 +254,7 @@ import com.oracle.svm.hosted.phases.SubstrateClassInitializationPlugin; import com.oracle.svm.hosted.phases.VerifyDeoptLIRFrameStatesPhase; import com.oracle.svm.hosted.phases.VerifyNoGuardsPhase; +import com.oracle.svm.hosted.pltgot.PLTGOTOptions; import com.oracle.svm.hosted.reflect.proxy.ProxyRenamingSubstitutionProcessor; import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; @@ -594,6 +596,15 @@ protected void doRun(Map entryPoints, JavaMainSupport j new UniverseBuilder(aUniverse, bb.getMetaAccess(), hUniverse, hMetaAccess, HostedConfiguration.instance().createStrengthenGraphs(bb, hUniverse), bb.getUnsupportedFeatures()).build(debug); + if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + /* + * Fixed-point loops are started after universe building, because the initial + * state for each method is computed after strengthen graphs, which is executed + * in UniverseBuilder. + */ + TransitiveEffectSummaryAnalysisEngine.get().runFixedPointLoops(bb); + } + BuildPhaseProvider.markHostedUniverseBuilt(); ClassInitializationSupport classInitializationSupport = bb.getHostVM().getClassInitializationSupport(); SubstratePlatformConfigurationProvider platformConfig = getPlatformConfig(hMetaAccess); @@ -825,6 +836,16 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De HostedImageLayerBuildingSupport.singleton().getWriter().initializeExternalValues(); } } + if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + /* + * Seal the TESA engine, prevent more analyses from being registered. + * Technically, we could currently allow registrations even during the + * Feature#beforeUniverseBuilding callbacks. But we are intentionally being + * stricter about registrations in case we would like to perform some TESA steps + * already during or immediately after PTA in the future. + */ + TransitiveEffectSummaryAnalysisEngine.get().seal(); + } } try (ReporterClosable _ = ProgressReporter.singleton().printAnalysis(bb.getUniverse(), nativeLibraries.getLibraries())) { @@ -881,6 +902,10 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De bb.getUnsupportedFeatures().report(bb); bb.checkUserLimitations(); + if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + TransitiveEffectSummaryAnalysisEngine.get().saveCallGraph(bb); + } + bb.afterAnalysis(); } catch (UnsupportedFeatureException ufe) { throw FallbackFeature.reportAsFallback(ufe); @@ -1056,6 +1081,18 @@ protected void setupNativeImage(OptionValues options, Map features, ImageClassLoader classLoader) { - stagePrinter.end(getTimer(TimerCollection.Registry.CLASSLIST).getTotalTime() + getTimer(TimerCollection.Registry.SETUP).getTotalTime()); + stagePrinter.end(getTimer(TimerCollection.Registry.CLASSLIST).getTotalTimeMs() + getTimer(TimerCollection.Registry.SETUP).getTotalTimeMs()); VM vm = ImageSingletons.lookup(VM.class); recordJsonMetric(GeneralInfo.JAVA_VERSION, vm.version); recordJsonMetric(GeneralInfo.VENDOR_VERSION, vm.vendorVersion); @@ -601,7 +604,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH Timer imageTimer = getTimer(TimerCollection.Registry.IMAGE); Timer writeTimer = getTimer(TimerCollection.Registry.WRITE); Timer archiveTimer = getTimer(TimerCollection.Registry.ARCHIVE_LAYER); - stagePrinter.end(imageTimer.getTotalTime() + writeTimer.getTotalTime() + archiveTimer.getTotalTime()); + stagePrinter.end(imageTimer.getTotalTimeMs() + writeTimer.getTotalTimeMs() + archiveTimer.getTotalTimeMs()); creationStageEndCompleted = true; String format = BYTES_TO_HUMAN_FORMAT + " (%5.2f%%) for "; l().a(format, ByteFormattingUtil.bytesToHuman(codeAreaSize), ProgressReporterUtils.toPercentage(codeAreaSize, imageFileSize)) @@ -622,7 +625,7 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH .doclink("debug info", "#glossary-debug-info"); if (debugInfoTimer != null) { - l.a(" generated in %.1fs", ProgressReporterUtils.millisToSeconds(debugInfoTimer.getTotalTime())); + l.a(" generated in %.1fs", ProgressReporterUtils.millisToSeconds(debugInfoTimer.getTotalTimeMs())); } l.println(); if (!(ImageSingletons.contains(NativeImageDebugInfoStripFeature.class) && ImageSingletons.lookup(NativeImageDebugInfoStripFeature.class).hasStrippedSuccessfully())) { @@ -642,6 +645,9 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH l().a(", %s in total file size", ByteFormattingUtil.bytesToHuman(imageDiskFileSize)); } l().println(); + if (TransitiveEffectSummaryAnalysisEngine.enabled() && TransitiveEffectSummaryAnalysisEngine.Options.TesaPrintToConsole.getValue()) { + printTesaStatistics(); + } printBreakdowns(); ImageSingletons.lookup(ProgressReporterFeature.class).afterBreakdowns(); printRecommendations(); @@ -756,6 +762,42 @@ private void printRecommendations() { } } + private void printTesaStatistics() { + TransitiveEffectSummaryAnalysisEngine engine = TransitiveEffectSummaryAnalysisEngine.get(); + + /* Separate the text from the previous section. */ + l().printLineSeparator(); + + /* Print high-level overview. */ + l().yellowBold().a("Transitive Effect Summary Analysis Results:").reset().println(); + l().a(" - Scope: %d methods, %d invokes", engine.getTotalMethods(), engine.getTotalInvokes()).println(); + l().a(" - Call graph initialization: %.2f ms", engine.getCallGraph().getCallGraphInitializationTimer().getTotalTimeMs()).println(); + double worstCaseTesaTime = engine.getAllAnalyses().stream().mapToDouble(AbstractTesa::getFixedPointLoopTimeMs).max().getAsDouble(); + l().a(" - Worst case TESA time (analyses run in parallel): %.2f ms", worstCaseTesaTime).println(); + l().a(" - Results per analysis (methods and invokes with more precise results than Any):").println(); + + /* Print details table header. */ + l().a(" %-25s | %10s | %10s | %6s", "-".repeat(25), "-".repeat(19), "-".repeat(19), "-".repeat(9)).println(); + l().a(" %-25s | %10s (%%) | %10s (%%) | %6s", "Analysis", "Methods", "Invokes", "Time (ms)").println(); + l().a(" %-25s | %10s | %10s | %6s", "-".repeat(25), "-".repeat(19), "-".repeat(19), "-".repeat(9)).println(); + + /* Print statistics for each TESA instance. */ + for (AbstractTesa analysis : engine.getAllAnalyses()) { + String classname = ClassUtil.getUnqualifiedName(analysis.getClass()); + int optimizableMethods = analysis.getOptimizableMethods(); + int optimizableInvokes = analysis.getOptimizableInvokes(); + double timeMs = analysis.getFixedPointLoopTimeMs(); + double methodsPercentage = (double) optimizableMethods / engine.getTotalMethods() * 100; + double invokesPercentage = (double) optimizableInvokes / engine.getTotalInvokes() * 100; + + l().a(" %-25s | %10d (%5.2f%%) | %10d (%5.2f%%) | %8.2f", + classname, + optimizableMethods, methodsPercentage, + optimizableInvokes, invokesPercentage, + timeMs).println(); + } + } + public void printEpilog(Optional optionalImageName, Optional optionalGenerator, ImageClassLoader classLoader, NativeImageGeneratorRunner.BuildOutcome buildOutcome, Optional optionalUnhandledThrowable, OptionValues parsedHostedOptions) { executor.shutdown(); @@ -784,7 +826,7 @@ public void printEpilog(Optional optionalImageName, Optional the effect tracked by the given analysis. + * @see TransitiveEffectSummaryAnalysisEngine + */ +public abstract class AbstractTesa> { + /** + * Holds the current state for each method. + */ + protected final Map methodToState = new ConcurrentHashMap<>(); + + /** + * The number of methods that can be optimized by this analysis, used for statistics. + */ + private final AtomicInteger optimizableMethodsCounter = new AtomicInteger(); + + /** + * The number of invokes that can be optimized by this analysis, used for statistics. + */ + private final AtomicInteger optimizableInvokesCounter = new AtomicInteger(); + + /** + * Timer for the core fixed-point loop. + */ + private final Timer fixedPointLoopTimer = TimerCollection.singleton().createTimer(ClassUtil.getUnqualifiedName(getClass()) + "FixedPointLoop"); + + /** + * Computes the initial state for the given {@code method}. + */ + protected abstract T computeInitialState(AnalysisMethod method, StructuredGraph graph); + + /** + * Computes and saves the initial state of this method to be used later by the analysis. + */ + void initializeStateForMethod(AnalysisMethod method, StructuredGraph graph) { + AnalysisError.guarantee(methodToState.put(method, computeInitialState(method, graph)) == null, "A state for method %s has already been initialized.", method); + } + + /** + * By default, we are conservative about invokes with unknown target methods (as we cannot + * compute their transitive effects). Subclasses may extend the check to reject more nodes, but + * they should always call the check in this class as well unless they can somehow prove that + * the unknown invoke is not a problem in their domain. + */ + protected boolean isSupportedNode(Node node) { + return !(node instanceof Invoke invoke && invoke.getTargetMethod() == null); + } + + /** + * By default, we skip the {@link StartNode} which has a {@code killedLocation} Any, + * {@link Invoke}s, because their transitive effect is computed by the analyses, and + * {@link ExceptionObjectNode}s, because they can only be reached from places covered by the + * analyses. Subclasses may change this check based on their domain. + */ + protected boolean shouldSkipNode(Node node) { + return node instanceof StartNode || node instanceof Invoke || node instanceof ExceptionObjectNode; + } + + /** + * Core loop of any TESA. Runs a fixed-point analysis propagating information about methods in + * reverse direction (callees to callers). + *

+ * We currently initialize the worklist with all methods. Intuitively, we could start + * only with leaves (e.g. methods that do not call any other methods). Unfortunately, we cannot + * guarantee that all methods would be reached from the leaves, because there may be a cycle in + * the graph and such a cycle would have no leaf in it (as each method calls another) and it + * does not have to be reachable from any leaf either. + *

+ * We could avoid having to run a fixed-point algorithm if we would perform + * Strongly-Connected-Component (SCC) condensation first: We could identify all SCCs and merge + * each of them into a single node. This would be sound and would lose no precision, because all + * the nodes in a SCC will end up having the same state anyway as each node in the SCC will + * propagate its state to the rest of the SCC, and we never remove anything from the state (go + * down the lattice), i.e., we have no "kill sets" in the data-flow terminology. After SCC + * condensation, we would have a directed acyclic graph and could use topological ordering to + * perform a linear sweep over the graph. However, since the overhead of the fixed-point + * algorithm is reasonably low, we stick with it for now, as it is simpler. + */ + void runFixedPointLoop(TransitiveEffectSummaryAnalysisEngine engine, BigBang bb) { + try (var _ = fixedPointLoopTimer.start()) { + var scheduledMethods = TesaReverseCallGraph.getAllMethods(bb).collect(Collectors.toCollection(HashSet::new)); + var worklist = new ArrayDeque<>(scheduledMethods); + /* + * Reaching more than a quadratic number of iterations suggests that there is an issue + * with the analysis (the merge is probably not monotonic). + */ + long iterations = 0; + long limit = ((long) scheduledMethods.size()) * scheduledMethods.size(); + while (!worklist.isEmpty()) { + if (iterations >= limit) { + if (TransitiveEffectSummaryAnalysisEngine.Options.TesaThrowOnNonTermination.getValue()) { + throw AnalysisError.shouldNotReachHere(ClassUtil.getUnqualifiedName(getClass()) + ": fixed-point loop did not terminate after " + iterations + " iterations."); + } else { + /* + * Do not fail in production builds. Clearing methodToState results in + * treating each method as unoptimizable. Clearing the map is important + * because stopping the algorithm early will most likely make its results + * unsound. + */ + methodToState.clear(); + break; + } + } + iterations++; + var currentMethod = worklist.removeFirst(); + scheduledMethods.remove(currentMethod); + var currentState = getState(currentMethod); + Set callers = engine.getCallGraph().getCallers(currentMethod); + if (callers != null) { + for (var caller : callers) { + var callerState = getState(caller); + var merged = callerState.combineEffects(currentState); + if (!merged.equals(callerState)) { + methodToState.put(caller, merged); + if (scheduledMethods.add(caller)) { + worklist.add(caller); + } + } + } + } + } + } + } + + /** + * Get the element corresponding to any effect (worst optimization potential). + */ + protected abstract T anyEffect(); + + /** + * Get the element corresponding to no effect (best optimization potential). + */ + protected abstract T noEffect(); + + /** + * Get the state for the given method. + */ + public T getState(AnalysisMethod currentMethod) { + T state = methodToState.get(currentMethod.getMultiMethod(MultiMethod.ORIGINAL_METHOD)); + if (state == null) { + /* Can occur, e.g., for multimethods or in layers. */ + return anyEffect(); + } + return state; + } + + /** + * Gets the state for the given invoke. For direct invokes, use the state of the target method. + * For indirect invokes, use the {@link TesaEffect#combineEffects} over the states of all target + * methods. + */ + public T getState(TransitiveEffectSummaryAnalysisEngine engine, Invoke invoke) { + var targetMethod = ((HostedMethod) invoke.callTarget().targetMethod()); + if (invoke.getInvokeKind().isDirect()) { + return getState(targetMethod.wrapped); + } else { + T cummulativeState = noEffect(); + for (AnalysisMethod targetMethods : engine.getCallGraph().getTargetMethods(invoke, targetMethod)) { + var targetState = getState(targetMethods); + cummulativeState = cummulativeState.combineEffects(targetState); + if (cummulativeState.isAnyEffect()) { + break; + } + } + return cummulativeState; + } + } + + /** + * Apply the results of this analysis on the given compilation {@code graph}. + */ + public void applyResults(TransitiveEffectSummaryAnalysisEngine engine, HostedMethod method, StructuredGraph graph) { + var state = getState(method.wrapped); + if (hasOptimizationPotential(state)) { + optimizableMethodsCounter.incrementAndGet(); + } + for (Node node : graph.getNodes()) { + if (node instanceof Invoke invoke && canOptimizedInvoke(invoke) && isSupportedNode(node)) { + var targetState = getState(engine, invoke); + if (hasOptimizationPotential(targetState)) { + optimizableInvokesCounter.incrementAndGet(); + optimizeInvoke(graph, invoke, targetState); + } + } + } + } + + /** + * Hook for subclasses to check if the given {@code invoke} can be optimized by the given + * analysis. By default, try to optimize all invokes. + */ + @SuppressWarnings("unused") + protected boolean canOptimizedInvoke(Invoke invoke) { + return true; + } + + /** + * Hook for subclasses to check if the given {@code state} can lead to optimizations. By + * default, anything but the top element of the lattice has optimization potential. + */ + protected boolean hasOptimizationPotential(T state) { + return !state.isAnyEffect(); + } + + /** + * Optimize the given {@code invoke} based on the computed {@code targetState}. + *

+ * By default, an empty method, so that analyses that cannot directly optimize the graph do not + * have to implement it. + */ + @SuppressWarnings("unused") + protected void optimizeInvoke(StructuredGraph graph, Invoke invoke, T targetState) { + + } + + /** + * @see #optimizableInvokesCounter + */ + public int getOptimizableInvokes() { + return optimizableInvokesCounter.get(); + } + + /** + * @see #optimizableMethodsCounter + */ + public int getOptimizableMethods() { + return optimizableMethodsCounter.get(); + } + + /** + * @see #fixedPointLoopTimer + */ + public double getFixedPointLoopTimeMs() { + return fixedPointLoopTimer.getTotalTimeMs(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java new file mode 100644 index 000000000000..b3d8c594b085 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, 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 com.oracle.svm.hosted.analysis.tesa; + +import org.graalvm.word.LocationIdentity; + +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.hosted.analysis.tesa.effect.LocationEffect; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.InvokeNode; +import jdk.graal.compiler.nodes.InvokeWithExceptionNode; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.memory.MemoryKill; +import jdk.graal.compiler.nodes.memory.MultiMemoryKill; +import jdk.graal.compiler.nodes.memory.SingleMemoryKill; + +/** + * TESA instance that computes information about killed locations. + */ +public class KilledLocationTesa extends AbstractTesa { + + @Override + protected LocationEffect noEffect() { + return LocationEffect.noEffect(); + } + + @Override + protected LocationEffect anyEffect() { + return LocationEffect.anyEffect(); + } + + @Override + protected LocationEffect computeInitialState(AnalysisMethod method, StructuredGraph graph) { + LocationEffect location = noEffect(); + for (Node node : graph.getNodes()) { + if (!isSupportedNode(node)) { + return anyEffect(); + } + if (shouldSkipNode(node)) { + continue; + } + if (MemoryKill.isMemoryKill(node)) { + LocationIdentity[] identities = extractLocationIdentities(node); + for (LocationIdentity identity : identities) { + if (identity.equals(MemoryKill.NO_LOCATION) || identity.isInit()) { + continue; + } + if (identity.isAny()) { + return LocationEffect.anyEffect(); + } + location = location.combineEffects(LocationEffect.singleLocation(identity)); + if (location.isAnyEffect()) { + return LocationEffect.anyEffect(); + } + } + } + } + return location; + } + + /** + * Returns all memory locations that may be killed by the given node. + */ + public static LocationIdentity[] extractLocationIdentities(Node node) { + return switch (node) { + case SingleMemoryKill single -> new LocationIdentity[]{single.getKilledLocationIdentity()}; + case MultiMemoryKill multi -> multi.getKilledLocationIdentities(); + default -> new LocationIdentity[]{LocationIdentity.any()}; + }; + } + + @Override + protected void optimizeInvoke(StructuredGraph graph, Invoke invoke, LocationEffect targetState) { + switch (targetState) { + case LocationEffect.Empty _ -> insertNewLocationIdentity(invoke, MemoryKill.NO_LOCATION); + case LocationEffect.Single single -> insertNewLocationIdentity(invoke, single.location); + default -> AnalysisError.shouldNotReachHere(targetState + " is not actionable."); + } + } + + private static void insertNewLocationIdentity(Invoke invoke, LocationIdentity locationIdentity) { + switch (invoke) { + case InvokeNode invokeNode -> invokeNode.setKilledLocationIdentity(locationIdentity); + case InvokeWithExceptionNode invokeWithExceptionNode -> invokeWithExceptionNode.setKilledLocationIdentity(locationIdentity); + default -> AnalysisError.shouldNotReachHere("Unsupported invoke type: " + invoke.getClass() + " " + invoke); + } + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java new file mode 100644 index 000000000000..7ce63568cba7 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025, 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 com.oracle.svm.hosted.analysis.tesa; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import com.oracle.graal.pointsto.AbstractAnalysisEngine; +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.PointsToAnalysis; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.InvokeInfo; +import com.oracle.graal.pointsto.meta.PointsToAnalysisMethod; +import com.oracle.graal.pointsto.util.Timer; +import com.oracle.graal.pointsto.util.TimerCollection; +import com.oracle.svm.hosted.meta.HostedMethod; + +import jdk.graal.compiler.nodes.Invoke; +import jdk.vm.ci.code.BytecodePosition; + +/** + * A helper containing the call-graph-related information used by + * {@link TransitiveEffectSummaryAnalysisEngine} and its analyses. + */ +public final class TesaReverseCallGraph { + + /** + * Timer used to measure the initial processing of the call graph. + */ + private final Timer callGraphInitializationTimer = TimerCollection.singleton().createTimer("TesaCallGraphInitialization"); + + /** + * Reverse call edges (from callees to callers). + */ + private final Map> callers = new ConcurrentHashMap<>(); + + /** + * A key to be used in {@link #invokeToCallees}. + */ + public record InvokePosition(AnalysisMethod sourceMethod, int bci, AnalysisMethod targetMethod) { + } + + /** + * A mapping from {@link InvokePosition} to the corresponding set of callees as computed by the + * {@link AbstractAnalysisEngine}. As we cannot always guarantee a unique mapping, it is a + * best-effort delivery with collision handling. The values can be either a {@link Collection} + * of {@link AnalysisMethod} or the {@link #COLLISION} marker object when the mapping is not + * unique. In that case, we cannot retrieve the more precise location-specific set of callees, + * but we can always fall back to set of all implementations of the given target method, loosing + * precision without compromising soundness. + *

+ * Using a custom {@link InvokePosition} as opposed to {@link BytecodePosition} leads to + * slightly fewer collisions, because the target method sometimes helps to disambiguate more + * invokes, e.g. those that have artificial source positions created by + * {@link AbstractAnalysisEngine#sourcePosition}. + */ + private final Map invokeToCallees = new ConcurrentHashMap<>(); + + /** + * Returns a stream of methods that should be analyzed by TESA, which are those that are + * "implementation invoked" by the terminology of {@link AbstractAnalysisEngine}. i.e. they are + * either root methods or they can be invoked from another reachable method. We do not use + * {@link AnalysisMethod#isReachable} to skip methods that are always inlined, because such + * methods exist only in compilation graphs of other methods. + */ + public static Stream getAllMethods(BigBang bb) { + return bb.getUniverse().getMethods().stream().filter(AnalysisMethod::isImplementationInvoked); + } + + /** + * Singleton collision object used to mark that the given {@link InvokePosition} is not unique. + */ + private static final Object COLLISION = new Object(); + + /** + * Private to enforce using the {@link #create} method. + */ + private TesaReverseCallGraph() { + } + + /** + * Create a new {@link TesaReverseCallGraph} instance with data populated by the results from + * {@code bb}. We use a factory method for the creation of the call graph as the process is + * quite complex to be placed in a constructor. + *

+ * We could lean on the {@link AbstractAnalysisEngine} more and use + * {@link AnalysisMethod#getCallers()} instead of computing and storing the call graph + * ourselves. However, the callers for each method are not tracked by default. This has to be + * enabled per-method via {@link PointsToAnalysisMethod#startTrackInvocations}, which would + * increase the memory footprint of {@link PointsToAnalysis}. + */ + public static TesaReverseCallGraph create(BigBang bb) { + var callGraph = new TesaReverseCallGraph(); + try (var _ = callGraph.callGraphInitializationTimer.start()) { + getAllMethods(bb).parallel().forEach(callGraph::addMethod); + return callGraph; + } + } + + /** + * Adds the {@code method} to the callgraph, including all the back edges from its callees. + */ + private void addMethod(AnalysisMethod method) { + for (InvokeInfo invoke : method.getInvokes()) { + InvokePosition key = new InvokePosition(method, invoke.getPosition().getBCI(), invoke.getTargetMethod()); + Collection callees = invoke.getAllCallees(); + var previousValue = invokeToCallees.get(key); + if (previousValue == null) { + invokeToCallees.put(key, callees); + } else if (previousValue != COLLISION) { + invokeToCallees.put(key, COLLISION); + } + for (AnalysisMethod callee : callees) { + callers.computeIfAbsent(callee, _ -> ConcurrentHashMap.newKeySet()).add(method); + } + } + } + + /** + * Returns the set of callers of the given method {@code callee}. + */ + public Set getCallers(AnalysisMethod callee) { + return callers.get(callee); + } + + /** + * Returns the set of call target methods for the given invoke. We use the information obtained + * from static analysis: We use the value extracted from the corresponding {@link InvokeInfo} if + * we can locate it and there are no collisions. Otherwise, we use all the reachable + * implementations of the given {@code targetMethod}. + */ + @SuppressWarnings("unchecked") + public Collection getTargetMethods(Invoke invoke, HostedMethod targetMethod) { + var node = invoke.asFixedNode(); + var key = new InvokePosition(((HostedMethod) node.graph().method()).wrapped, invoke.bci(), targetMethod.wrapped); + Object callees = invokeToCallees.get(key); + if (callees != null && callees != COLLISION) { + return ((Collection) callees); + } + return Arrays.stream(targetMethod.getImplementations()).map(HostedMethod::getWrapped).toList(); + } + + public Timer getCallGraphInitializationTimer() { + return callGraphInitializationTimer; + } + + /** + * Exposed only for testing. Should not be used in production code. + */ + public Map getInvokeToCallees() { + return invokeToCallees; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java new file mode 100644 index 000000000000..166f4cab6c68 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2025, 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 com.oracle.svm.hosted.analysis.tesa; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; + +import com.oracle.graal.pointsto.AbstractAnalysisEngine; +import com.oracle.graal.pointsto.BigBang; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.results.StrengthenGraphs; +import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.util.VMError; +import com.oracle.svm.hosted.analysis.tesa.effect.TesaEffect; +import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.util.ClassUtil; +import com.oracle.svm.util.ImageBuildStatistics; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.util.json.JsonBuilder; +import jdk.graal.compiler.util.json.JsonWriter; + +/** + * The core class of the Transitive Effect Summary Analysis (TESA) framework. Manages a set of + * {@link AbstractTesa} instances, each implementing a separate TESA analysis based on a given + * {@link TesaEffect}. + *

+ * The main entry points are the following methods, which should be called in this order: + *

    + *
  1. {@link #initializeStateForMethod} - for each method separately. Should be called after + * {@link StrengthenGraphs} so that we start with graphs already optimized by the results of + * {@link AbstractAnalysisEngine}.
  2. + *
  3. {@link #runFixedPointLoops} - once per image build.
  4. + *
  5. {@link #applyResults} - for each method separately.
  6. + *
+ * + * @see AbstractTesa + * @see TesaEffect + * + * @implNote For a concrete example on how to create and register a custom TESA instance, search for + * {@code ExampleUnsafeAnalysisTesaTest}. + */ +public class TransitiveEffectSummaryAnalysisEngine implements ImageBuildStatistics.TransitiveEffectSummaryAnalysisPrinter { + + public static class Options { + @Option(help = "Enable Transitive Effect Summary Analysis (TESA).")// + public static final HostedOptionKey TransitiveEffectSummaryAnalysis = new HostedOptionKey<>(true); + + @Option(help = "Print TESA results to the console.")// + public static final HostedOptionKey TesaPrintToConsole = new HostedOptionKey<>(true); + + @Option(help = "Throw an exception if any TESA instance fails to reach a fixed point within the expected number of iterations.")// + public static final HostedOptionKey TesaThrowOnNonTermination = new HostedOptionKey<>(true); + } + + /** + * Contains TESA instances to be performed. + */ + private final Map, AbstractTesa> analyses = new LinkedHashMap<>(); + + /** + * Used to make sure that TESA instances are not registered too late, after the initial state + * computation has already been done. + */ + private volatile boolean isSealed; + + /** + * Call-graph-related data. + */ + private TesaReverseCallGraph callGraph; + + /** + * The total number of methods visited by TESA during the optimization stage. Used for + * statistics. + */ + private final AtomicInteger totalMethodsCounter = new AtomicInteger(); + + /** + * The total number of invokes visited by TESA during the optimization stage. Used for + * statistics. + */ + private final AtomicInteger totalInvokesCounter = new AtomicInteger(); + + public TransitiveEffectSummaryAnalysisEngine() { + registerDefaultAnalyses(); + } + + /** + * Register a set of TESA instances that are enabled out of the box. More may be added by + * explicitly calling {@link #registerTesa}. + */ + private void registerDefaultAnalyses() { + registerTesa(new KilledLocationTesa()); + } + + /** + * Registers a given TESA instance. Should be called during the configuration stage of the + * build, at the latest during {@link Feature#beforeAnalysis} callbacks. + *

+ * The analyses may then be retrieved by calling {@link #getAnalysis}. + */ + public final void registerTesa(AbstractTesa tesa) { + VMError.guarantee(!isSealed, "The TESA engine has already been sealed, no more TESA instances may be added."); + analyses.put(tesa.getClass(), tesa); + } + + public static boolean enabled() { + return ImageSingletons.contains(TransitiveEffectSummaryAnalysisEngine.class); + } + + /** + * Gets the singleton instance of the engine. + */ + public static TransitiveEffectSummaryAnalysisEngine get() { + return ImageSingletons.lookup(TransitiveEffectSummaryAnalysisEngine.class); + } + + /** + * This method marks the end of the configuration phase, after which no more analyses may be + * registered via {@link #registerTesa}. + */ + public void seal() { + isSealed = true; + } + + /** + * Runs the {@link AbstractTesa#initializeStateForMethod} for each registered TESA. + * + * @see AbstractTesa#initializeStateForMethod + */ + public void initializeStateForMethod(AnalysisMethod method, StructuredGraph graph) { + analyses.values().parallelStream() + .forEach(analysis -> analysis.initializeStateForMethod(method, graph)); + } + + /** + * @see AbstractTesa#runFixedPointLoop + */ + public void runFixedPointLoops(BigBang bb) { + analyses.values().parallelStream() + .forEach(analysis -> analysis.runFixedPointLoop(this, bb)); + } + + /** + * @see AbstractTesa#applyResults + */ + public void applyResults(HostedMethod method, StructuredGraph graph) { + totalMethodsCounter.incrementAndGet(); + for (Node node : graph.getNodes()) { + if (node instanceof Invoke) { + totalInvokesCounter.incrementAndGet(); + } + } + for (AbstractTesa analysis : analyses.values()) { + analysis.applyResults(this, method, graph); + } + } + + /** + * Print the results as a JSON object reported as a part of {@link ImageBuildStatistics}. + */ + @Override + public void printTesaResults(PrintWriter out) { + try { + var writer = new JsonWriter(out); + // we are extending an already existing JSON object + writer.appendSeparator(); + // create a nested tesa object for clarity + writer.quote("tesa").appendFieldSeparator(); + try (var objectBuilder = writer.objectBuilder()) { + printTesaResults0(objectBuilder); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + // Do not close the writer. The outer JSON object has not been fully printed yet. + } + + private void printTesaResults0(JsonBuilder.ObjectBuilder objectBuilder) throws IOException { + objectBuilder.append(callGraph.getCallGraphInitializationTimer().getName() + "Ms", callGraph.getCallGraphInitializationTimer().getTotalTimeMs()); + objectBuilder.append("totalMethods", totalMethodsCounter.get()); + objectBuilder.append("totalInvokes", totalInvokesCounter.get()); + for (AbstractTesa analysis : analyses.values()) { + printSingleAnalysisResults(objectBuilder, analysis); + } + } + + private static void printSingleAnalysisResults(JsonBuilder.ObjectBuilder parentBuilder, AbstractTesa analysis) throws IOException { + var classname = ClassUtil.getUnqualifiedName(analysis.getClass()); + try (var objectBuilder = parentBuilder.append(classname).object()) { + objectBuilder.append("methods", analysis.getOptimizableMethods()); + objectBuilder.append("invokes", analysis.getOptimizableInvokes()); + objectBuilder.append("timeMs", analysis.getFixedPointLoopTimeMs()); + } + } + + /** + * Save the relevant call-graph-related data computed by {@link AbstractAnalysisEngine} and + * store them in {@link TesaReverseCallGraph} for later use. + */ + public void saveCallGraph(BigBang bb) { + callGraph = TesaReverseCallGraph.create(bb); + } + + public TesaReverseCallGraph getCallGraph() { + return callGraph; + } + + public T getAnalysis(Class clazz) { + return clazz.cast(analyses.get(clazz)); + } + + public Collection> getAllAnalyses() { + return analyses.values(); + } + + public int getTotalMethods() { + return totalMethodsCounter.get(); + } + + public int getTotalInvokes() { + return totalInvokesCounter.get(); + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java new file mode 100644 index 000000000000..4cacd31ddc44 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025, 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 com.oracle.svm.hosted.analysis.tesa.effect; + +import java.util.Objects; + +import org.graalvm.word.LocationIdentity; + +/** + * Effect over {@link LocationIdentity} instances. Differentiates between empty location (noEffect), + * single location, and any location (anyEffect). + */ +public sealed class LocationEffect + implements TesaEffect permits LocationEffect.Any, LocationEffect.Empty, LocationEffect.Single { + + @Override + public boolean isAnyEffect() { + return this == Any.INSTANCE; + } + + @Override + public boolean hasNoEffects() { + return this == Empty.INSTANCE; + } + + public static Any anyEffect() { + return Any.INSTANCE; + } + + public static Single singleLocation(LocationIdentity location) { + return new Single(location); + } + + public static Empty noEffect() { + return Empty.INSTANCE; + } + + @Override + public LocationEffect combineEffects(LocationEffect other) { + if (this.isAnyEffect() || other.isAnyEffect()) { + return anyEffect(); + } + if (other.hasNoEffects()) { + return this; + } + if (this.hasNoEffects()) { + return other; + } + var thisLoc = ((Single) this).location; + var otherLoc = ((Single) other).location; + if (thisLoc.equals(otherLoc)) { + return this; + } + return anyEffect(); + } + + public static final class Any extends LocationEffect { + private static final Any INSTANCE = new Any(); + + private Any() { + } + + @Override + public String toString() { + return "AnyLocation"; + } + } + + public static final class Empty extends LocationEffect { + private static final Empty INSTANCE = new Empty(); + + private Empty() { + } + + @Override + public String toString() { + return "EmptyLocation"; + } + } + + public static final class Single extends LocationEffect { + public final LocationIdentity location; + + private Single(LocationIdentity location) { + this.location = location; + } + + @Override + public String toString() { + return "SingleLocation(" + location + ")"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Single that)) { + return false; + } + return Objects.equals(location, that.location); + } + + @Override + public int hashCode() { + return Objects.hashCode(location); + } + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java new file mode 100644 index 000000000000..a0c836950f18 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025, 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 com.oracle.svm.hosted.analysis.tesa.effect; + +import com.oracle.graal.pointsto.PointsToAnalysis; +import com.oracle.svm.hosted.analysis.tesa.AbstractTesa; +import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; + +/** + * The base effect used by the analyses implemented in + * {@link TransitiveEffectSummaryAnalysisEngine}. + *

+ * The effects form a lattice ordered so that lower values mean higher optimization potential, the + * best one being {@code noEffect} at the bottom, while the {@code anyEffect} value represents that + * there is probably no optimization potential. + *

+ * To guarantee termination (fixed point is found), any implementation of this interface should + * represent a lattice with finite height, i.e. there should always be a finite amount of + * calls to the {@link #combineEffects} method producing larger values (higher in the lattice) + * before the {@code top} is reached. + *

+ * To guarantee that the fixed point is reached fast, the height of the lattice should be limited, + * further restricting the amount of {@link #combineEffects} calls before reaching {@code top}. + * + * @see AbstractTesa + * @see TransitiveEffectSummaryAnalysisEngine + * + */ +public interface TesaEffect> { + /** + * Returns {@code true} if this element represent that anything is possible, which typically + * suggests no optimization potential, i.e. the analysis run into an invoke with unknown + * {@code targetMethod}, native call, or the analysis simply stopped tracking precise + * information, e.g. because it started to be intractable. This is conceptually similar to a + * saturation in {@link PointsToAnalysis}. + */ + boolean isAnyEffect(); + + /** + * Returns {@code true} if this element represents that no effect was observed, suggesting the + * strongest optimization potential. + */ + boolean hasNoEffects(); + + /** + * Returns an element representing the meet ("merge", "union"), i.e. the "bigger" fact, obtained + * by combining {@code this} lattice element and {@code other}. + */ + T combineEffects(T other); +} 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 45eb46681417..d45bc1b22a3f 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 @@ -72,6 +72,7 @@ import com.oracle.svm.hosted.NativeImageGenerator; import com.oracle.svm.hosted.NativeImageOptions; import com.oracle.svm.hosted.ProgressReporter; +import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; import com.oracle.svm.hosted.diagnostic.HostedHeapDumpFeature; import com.oracle.svm.hosted.imagelayer.HostedImageLayerBuildingSupport; import com.oracle.svm.hosted.imagelayer.LayeredDispatchTableFeature; @@ -1397,6 +1398,10 @@ private CompilationResult defaultCompileFunction(DebugContext debug, HostedMetho /* Check that graph is in good shape before compilation. */ assert GraphOrder.assertSchedulableGraph(graph); + if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + TransitiveEffectSummaryAnalysisEngine.get().applyResults(method, graph); + } + try (DebugContext.Scope _ = debug.scope("Compiling", graph, method, this); DebugCloseable _ = GraalServices.GCTimerScope.create(debug)) { diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java index d757dccd8c14..3cf79de22d48 100644 --- a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java @@ -129,6 +129,7 @@ void print(PrintWriter out) { } out.print(json); printTimerStats(out); + printTesaStats(out); out.format("}%n"); } @@ -137,10 +138,20 @@ private void printTimerStats(PrintWriter out) { dumper.printTimerStats(out); } + private void printTesaStats(PrintWriter out) { + if (ImageSingletons.contains(TransitiveEffectSummaryAnalysisPrinter.class)) { + ImageSingletons.lookup(TransitiveEffectSummaryAnalysisPrinter.class).printTesaResults(out); + } + } + static final String INDENT = " "; } public interface TimerCollectionPrinter { void printTimerStats(PrintWriter out); } + + public interface TransitiveEffectSummaryAnalysisPrinter { + void printTesaResults(PrintWriter out); + } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java index 759d00d663c6..ce9e9846ff0f 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/NativeImageWasmGeneratorRunner.java @@ -187,7 +187,7 @@ public int build(ImageClassLoader classLoader) { protected void reportEpilog(String imageName, ProgressReporter reporter, ImageClassLoader classLoader, BuildOutcome buildOutcome, Throwable vmError, OptionValues parsedHostedOptions) { super.reportEpilog(imageName, reporter, classLoader, buildOutcome, vmError, parsedHostedOptions); if (buildOutcome.successful()) { - BenchmarkLogger.printBuildTime((int) TimerCollection.singleton().get(TimerCollection.Registry.TOTAL).getTotalTime(), parsedHostedOptions); + BenchmarkLogger.printBuildTime((int) TimerCollection.singleton().get(TimerCollection.Registry.TOTAL).getTotalTimeMs(), parsedHostedOptions); } } diff --git a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/logging/visualization/CLIVisualizationSupport.java b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/logging/visualization/CLIVisualizationSupport.java index 3b220b5c2fa8..c984483ffda1 100644 --- a/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/logging/visualization/CLIVisualizationSupport.java +++ b/web-image/src/com.oracle.svm.hosted.webimage/src/com/oracle/svm/hosted/webimage/logging/visualization/CLIVisualizationSupport.java @@ -100,13 +100,13 @@ public class CLIVisualizationSupport extends VisualizationSupport { public static Map toMap() { TimerCollection timerCollection = TimerCollection.singleton(); LinkedHashMap map = new LinkedHashMap<>(); - map.put(TimerCollection.Registry.SETUP.name, timerCollection.get(TimerCollection.Registry.SETUP).getTotalTime()); - map.put(TimerCollection.Registry.ANALYSIS.name, timerCollection.get(TimerCollection.Registry.ANALYSIS).getTotalTime()); - map.put(TimerCollection.Registry.UNIVERSE.name, timerCollection.get(TimerCollection.Registry.UNIVERSE).getTotalTime()); - map.put(TimerCollection.Registry.COMPILE_TOTAL.name, timerCollection.get(TimerCollection.Registry.COMPILE_TOTAL).getTotalTime()); - map.put(WebImageEmitTimer, timerCollection.get(WebImageEmitTimer).getTotalTime()); + map.put(TimerCollection.Registry.SETUP.name, timerCollection.get(TimerCollection.Registry.SETUP).getTotalTimeMs()); + map.put(TimerCollection.Registry.ANALYSIS.name, timerCollection.get(TimerCollection.Registry.ANALYSIS).getTotalTimeMs()); + map.put(TimerCollection.Registry.UNIVERSE.name, timerCollection.get(TimerCollection.Registry.UNIVERSE).getTotalTimeMs()); + map.put(TimerCollection.Registry.COMPILE_TOTAL.name, timerCollection.get(TimerCollection.Registry.COMPILE_TOTAL).getTotalTimeMs()); + map.put(WebImageEmitTimer, timerCollection.get(WebImageEmitTimer).getTotalTimeMs()); if (WebImageOptions.ClosureCompiler.getValue()) { - map.put(WebImageJSCodeGen.ClosureTimer, timerCollection.get(WebImageJSCodeGen.ClosureTimer).getTotalTime()); + map.put(WebImageJSCodeGen.ClosureTimer, timerCollection.get(WebImageJSCodeGen.ClosureTimer).getTotalTimeMs()); } return map; } @@ -198,7 +198,7 @@ private static LinkedHashMap createInfo() { } private static void visualizeStepTiming(LinkedHashMap contents) { - BarPlotWidget buildTimes = new BarPlotWidget(toMap(), Optional.of(TimerCollection.singleton().get(WebImageTotalTime).getTotalTime()), "ms", label -> Color.BLUE_BRIGHT); + BarPlotWidget buildTimes = new BarPlotWidget(toMap(), Optional.of(TimerCollection.singleton().get(WebImageTotalTime).getTotalTimeMs()), "ms", label -> Color.BLUE_BRIGHT); contents.put("build timing", buildTimes); } From ecb6b4e4a47f1f8c601aec2cfbd0e5239dff7d4a Mon Sep 17 00:00:00 2001 From: David Kozak Date: Tue, 18 Nov 2025 15:23:08 +0100 Subject: [PATCH 2/4] process feedback from reviews --- .../jdk/graal/compiler/nodes/InvokeNode.java | 8 ++--- .../nodes/InvokeWithExceptionNode.java | 8 ++--- .../svm/hosted/NativeImageGenerator.java | 25 ++++++------- .../oracle/svm/hosted/ProgressReporter.java | 7 ++-- .../svm/hosted/SubstrateStrengthenGraphs.java | 8 +++-- .../hosted/analysis/tesa/AbstractTesa.java | 35 ++++++++++++++----- .../analysis/tesa/KilledLocationTesa.java | 25 +++++++++++-- ...aryAnalysisEngine.java => TesaEngine.java} | 15 +++++--- .../analysis/tesa/TesaReverseCallGraph.java | 22 ++++++------ .../analysis/tesa/effect/LocationEffect.java | 8 +++++ .../analysis/tesa/effect/TesaEffect.java | 7 ++-- .../oracle/svm/hosted/code/CompileQueue.java | 6 ++-- .../oracle/svm/util/ImageBuildStatistics.java | 6 ++-- 13 files changed, 118 insertions(+), 62 deletions(-) rename substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/{TransitiveEffectSummaryAnalysisEngine.java => TesaEngine.java} (93%) diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java index cfe7a5f06750..10dfa58b1777 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeNode.java @@ -129,10 +129,6 @@ public void setPolymorphic(boolean value) { this.polymorphic = value; } - public void setKilledLocationIdentity(LocationIdentity identity) { - this.killedLocationIdentity = identity; - } - @Override public void setInlineControl(InlineControl control) { this.inlineControl = control; @@ -157,6 +153,10 @@ public LocationIdentity getKilledLocationIdentity() { return killedLocationIdentity; } + public void setKilledLocationIdentity(LocationIdentity identity) { + this.killedLocationIdentity = identity; + } + @Override public void generate(NodeLIRBuilderTool gen) { gen.emitInvoke(this); diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java index c0bfcf7e6668..e7c43b72d33d 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InvokeWithExceptionNode.java @@ -167,6 +167,10 @@ public boolean hasSideEffect() { return sideEffect; } + public void setSideEffect(boolean withSideEffects) { + this.sideEffect = withSideEffects; + } + @Override public LocationIdentity getKilledLocationIdentity() { return killedLocationIdentity; @@ -274,8 +278,4 @@ public boolean isInOOMETry() { public void setInOOMETry(boolean isInOOMETry) { this.isInOOMETry = isInOOMETry; } - - public void setSideEffect(boolean withSideEffects) { - this.sideEffect = withSideEffects; - } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index 0d3061cefbf6..783dff0b35c6 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -201,7 +201,7 @@ import com.oracle.svm.hosted.analysis.ReachabilityTracePrinter; import com.oracle.svm.hosted.analysis.SVMAnalysisMetaAccess; import com.oracle.svm.hosted.analysis.SubstrateUnsupportedFeatures; -import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; +import com.oracle.svm.hosted.analysis.tesa.TesaEngine; import com.oracle.svm.hosted.annotation.SubstrateAnnotationExtractor; import com.oracle.svm.hosted.c.CAnnotationProcessorCache; import com.oracle.svm.hosted.c.CConstantValueSupportImpl; @@ -255,6 +255,7 @@ import com.oracle.svm.hosted.phases.VerifyDeoptLIRFrameStatesPhase; import com.oracle.svm.hosted.phases.VerifyNoGuardsPhase; import com.oracle.svm.hosted.pltgot.PLTGOTOptions; +import com.oracle.svm.hosted.reflect.ReflectionFeature; import com.oracle.svm.hosted.reflect.proxy.ProxyRenamingSubstitutionProcessor; import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; @@ -596,13 +597,13 @@ protected void doRun(Map entryPoints, JavaMainSupport j new UniverseBuilder(aUniverse, bb.getMetaAccess(), hUniverse, hMetaAccess, HostedConfiguration.instance().createStrengthenGraphs(bb, hUniverse), bb.getUnsupportedFeatures()).build(debug); - if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + if (TesaEngine.enabled()) { /* * Fixed-point loops are started after universe building, because the initial * state for each method is computed after strengthen graphs, which is executed * in UniverseBuilder. */ - TransitiveEffectSummaryAnalysisEngine.get().runFixedPointLoops(bb); + TesaEngine.get().runFixedPointLoops(bb); } BuildPhaseProvider.markHostedUniverseBuilt(); @@ -836,7 +837,7 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De HostedImageLayerBuildingSupport.singleton().getWriter().initializeExternalValues(); } } - if (TransitiveEffectSummaryAnalysisEngine.enabled()) { + if (TesaEngine.enabled()) { /* * Seal the TESA engine, prevent more analyses from being registered. * Technically, we could currently allow registrations even during the @@ -844,7 +845,7 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De * stricter about registrations in case we would like to perform some TESA steps * already during or immediately after PTA in the future. */ - TransitiveEffectSummaryAnalysisEngine.get().seal(); + TesaEngine.get().seal(); } } @@ -902,8 +903,8 @@ protected boolean runPointsToAnalysis(String imageName, OptionValues options, De bb.getUnsupportedFeatures().report(bb); bb.checkUserLimitations(); - if (TransitiveEffectSummaryAnalysisEngine.enabled()) { - TransitiveEffectSummaryAnalysisEngine.get().saveCallGraph(bb); + if (TesaEngine.enabled()) { + TesaEngine.get().saveCallGraph(bb); } bb.afterAnalysis(); @@ -1081,16 +1082,16 @@ protected void setupNativeImage(OptionValues options, Map the effect tracked by the given analysis. - * @see TransitiveEffectSummaryAnalysisEngine + * @see TesaEngine */ public abstract class AbstractTesa> { /** @@ -77,6 +78,13 @@ public abstract class AbstractTesa> { /** * Computes the initial state for the given {@code method}. + *

+ * While the algorithm to compute the initial state can be arbitrary, it is advisable that it + * contains at most a single pass over the {@code graph} and does not rely on implementation + * details of the Graal IR. Ideally, only high-level APIs and marker interfaces should be used + * to make sure that the code in {@code computeInitialState} does not became incorrect when new + * Graal IR nodes are introduced. See how the {@link KilledLocationTesa#computeInitialState} + * uses {@link MemoryKill#isMemoryKill(Node)} as an example. */ protected abstract T computeInitialState(AnalysisMethod method, StructuredGraph graph); @@ -84,7 +92,8 @@ public abstract class AbstractTesa> { * Computes and saves the initial state of this method to be used later by the analysis. */ void initializeStateForMethod(AnalysisMethod method, StructuredGraph graph) { - AnalysisError.guarantee(methodToState.put(method, computeInitialState(method, graph)) == null, "A state for method %s has already been initialized.", method); + T previous = methodToState.put(method, computeInitialState(method, graph)); + AnalysisError.guarantee(previous == null, "A state for method %s has already been initialized.", method); } /** @@ -127,7 +136,7 @@ protected boolean shouldSkipNode(Node node) { * perform a linear sweep over the graph. However, since the overhead of the fixed-point * algorithm is reasonably low, we stick with it for now, as it is simpler. */ - void runFixedPointLoop(TransitiveEffectSummaryAnalysisEngine engine, BigBang bb) { + void runFixedPointLoop(TesaEngine engine, BigBang bb) { try (var _ = fixedPointLoopTimer.start()) { var scheduledMethods = TesaReverseCallGraph.getAllMethods(bb).collect(Collectors.toCollection(HashSet::new)); var worklist = new ArrayDeque<>(scheduledMethods); @@ -139,7 +148,7 @@ void runFixedPointLoop(TransitiveEffectSummaryAnalysisEngine engine, BigBang bb) long limit = ((long) scheduledMethods.size()) * scheduledMethods.size(); while (!worklist.isEmpty()) { if (iterations >= limit) { - if (TransitiveEffectSummaryAnalysisEngine.Options.TesaThrowOnNonTermination.getValue()) { + if (TesaEngine.Options.TesaThrowOnNonTermination.getValue()) { throw AnalysisError.shouldNotReachHere(ClassUtil.getUnqualifiedName(getClass()) + ": fixed-point loop did not terminate after " + iterations + " iterations."); } else { /* @@ -200,14 +209,14 @@ public T getState(AnalysisMethod currentMethod) { * For indirect invokes, use the {@link TesaEffect#combineEffects} over the states of all target * methods. */ - public T getState(TransitiveEffectSummaryAnalysisEngine engine, Invoke invoke) { + public T getState(TesaEngine engine, Invoke invoke) { var targetMethod = ((HostedMethod) invoke.callTarget().targetMethod()); if (invoke.getInvokeKind().isDirect()) { return getState(targetMethod.wrapped); } else { T cummulativeState = noEffect(); - for (AnalysisMethod targetMethods : engine.getCallGraph().getTargetMethods(invoke, targetMethod)) { - var targetState = getState(targetMethods); + for (AnalysisMethod callee : engine.getCallGraph().getCallees(invoke, targetMethod)) { + var targetState = getState(callee); cummulativeState = cummulativeState.combineEffects(targetState); if (cummulativeState.isAnyEffect()) { break; @@ -220,9 +229,10 @@ public T getState(TransitiveEffectSummaryAnalysisEngine engine, Invoke invoke) { /** * Apply the results of this analysis on the given compilation {@code graph}. */ - public void applyResults(TransitiveEffectSummaryAnalysisEngine engine, HostedMethod method, StructuredGraph graph) { + public void applyResults(TesaEngine engine, HostedMethod method, StructuredGraph graph) { var state = getState(method.wrapped); if (hasOptimizationPotential(state)) { + onOptimizableMethodDiscovered(method, state, graph); optimizableMethodsCounter.incrementAndGet(); } for (Node node : graph.getNodes()) { @@ -236,6 +246,15 @@ public void applyResults(TransitiveEffectSummaryAnalysisEngine engine, HostedMet } } + /** + * Hook for subclasses to perform any postprocessing or correctness checks for methods found as + * optimizable. + */ + @SuppressWarnings("unused") + protected void onOptimizableMethodDiscovered(HostedMethod method, T state, StructuredGraph graph) { + + } + /** * Hook for subclasses to check if the given {@code invoke} can be optimized by the given * analysis. By default, try to optimize all invokes. diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java index b3d8c594b085..de5c4955a9c0 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java @@ -29,11 +29,13 @@ import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.util.AnalysisError; import com.oracle.svm.hosted.analysis.tesa.effect.LocationEffect; +import com.oracle.svm.hosted.meta.HostedMethod; import jdk.graal.compiler.graph.Node; import jdk.graal.compiler.nodes.Invoke; import jdk.graal.compiler.nodes.InvokeNode; import jdk.graal.compiler.nodes.InvokeWithExceptionNode; +import jdk.graal.compiler.nodes.SafepointNode; import jdk.graal.compiler.nodes.StructuredGraph; import jdk.graal.compiler.nodes.memory.MemoryKill; import jdk.graal.compiler.nodes.memory.MultiMemoryKill; @@ -94,16 +96,33 @@ public static LocationIdentity[] extractLocationIdentities(Node node) { }; } + @Override + protected void onOptimizableMethodDiscovered(HostedMethod method, LocationEffect state, StructuredGraph graph) { + assert checkNoSafepointNodes(method, state, graph); + } + + /** + * Methods containing {@link SafepointNode} can kill multiple locations, which we do not track + * precisely at the moment. To guarantee correctness, we verify that only methods without + * safepoints have more precise killed location than {@code any}. + */ + private static boolean checkNoSafepointNodes(HostedMethod method, LocationEffect state, StructuredGraph graph) { + for (Node node : graph.getNodes()) { + AnalysisError.guarantee(!(node instanceof SafepointNode), "Method %s has a safepoint node in its graph, but its killed location is not any: %s", method.getQualifiedName(), state); + } + return true; + } + @Override protected void optimizeInvoke(StructuredGraph graph, Invoke invoke, LocationEffect targetState) { switch (targetState) { - case LocationEffect.Empty _ -> insertNewLocationIdentity(invoke, MemoryKill.NO_LOCATION); - case LocationEffect.Single single -> insertNewLocationIdentity(invoke, single.location); + case LocationEffect.Empty _ -> setKilledLocationIdentity(invoke, MemoryKill.NO_LOCATION); + case LocationEffect.Single single -> setKilledLocationIdentity(invoke, single.location); default -> AnalysisError.shouldNotReachHere(targetState + " is not actionable."); } } - private static void insertNewLocationIdentity(Invoke invoke, LocationIdentity locationIdentity) { + private static void setKilledLocationIdentity(Invoke invoke, LocationIdentity locationIdentity) { switch (invoke) { case InvokeNode invokeNode -> invokeNode.setKilledLocationIdentity(locationIdentity); case InvokeWithExceptionNode invokeWithExceptionNode -> invokeWithExceptionNode.setKilledLocationIdentity(locationIdentity); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java similarity index 93% rename from substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java rename to substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java index 166f4cab6c68..80f58f091c22 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TransitiveEffectSummaryAnalysisEngine.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java @@ -39,6 +39,10 @@ import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.results.StrengthenGraphs; import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.traits.BuiltinTraits.BuildtimeAccessOnly; +import com.oracle.svm.core.traits.BuiltinTraits.NoLayeredCallbacks; +import com.oracle.svm.core.traits.SingletonLayeredInstallationKind.Independent; +import com.oracle.svm.core.traits.SingletonTraits; import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.analysis.tesa.effect.TesaEffect; import com.oracle.svm.hosted.meta.HostedMethod; @@ -72,7 +76,8 @@ * @implNote For a concrete example on how to create and register a custom TESA instance, search for * {@code ExampleUnsafeAnalysisTesaTest}. */ -public class TransitiveEffectSummaryAnalysisEngine implements ImageBuildStatistics.TransitiveEffectSummaryAnalysisPrinter { +@SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class, layeredInstallationKind = Independent.class) +public class TesaEngine implements ImageBuildStatistics.TesaPrinter { public static class Options { @Option(help = "Enable Transitive Effect Summary Analysis (TESA).")// @@ -113,7 +118,7 @@ public static class Options { */ private final AtomicInteger totalInvokesCounter = new AtomicInteger(); - public TransitiveEffectSummaryAnalysisEngine() { + public TesaEngine() { registerDefaultAnalyses(); } @@ -137,14 +142,14 @@ public final void registerTesa(AbstractTesa tesa) { } public static boolean enabled() { - return ImageSingletons.contains(TransitiveEffectSummaryAnalysisEngine.class); + return ImageSingletons.contains(TesaEngine.class); } /** * Gets the singleton instance of the engine. */ - public static TransitiveEffectSummaryAnalysisEngine get() { - return ImageSingletons.lookup(TransitiveEffectSummaryAnalysisEngine.class); + public static TesaEngine get() { + return ImageSingletons.lookup(TesaEngine.class); } /** diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java index 7ce63568cba7..47b02d0ac137 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java @@ -45,8 +45,8 @@ import jdk.vm.ci.code.BytecodePosition; /** - * A helper containing the call-graph-related information used by - * {@link TransitiveEffectSummaryAnalysisEngine} and its analyses. + * A helper containing the call-graph-related information used by {@link TesaEngine} and its + * analyses. */ public final class TesaReverseCallGraph { @@ -68,12 +68,13 @@ public record InvokePosition(AnalysisMethod sourceMethod, int bci, AnalysisMetho /** * A mapping from {@link InvokePosition} to the corresponding set of callees as computed by the - * {@link AbstractAnalysisEngine}. As we cannot always guarantee a unique mapping, it is a - * best-effort delivery with collision handling. The values can be either a {@link Collection} - * of {@link AnalysisMethod} or the {@link #COLLISION} marker object when the mapping is not - * unique. In that case, we cannot retrieve the more precise location-specific set of callees, - * but we can always fall back to set of all implementations of the given target method, loosing - * precision without compromising soundness. + * {@link AbstractAnalysisEngine}. As we cannot always guarantee a unique mapping, e.g., due to + * the artificial source positions discussed below, it is a best-effort delivery with collision + * handling. The values can be either a {@link Collection} of {@link AnalysisMethod} or the + * {@link #COLLISION} marker object when the mapping is not unique. In that case, we cannot + * retrieve the more precise location-specific set of callees, but we can always fall back to + * set of all implementations of the given target method, loosing precision without compromising + * soundness. *

* Using a custom {@link InvokePosition} as opposed to {@link BytecodePosition} leads to * slightly fewer collisions, because the target method sometimes helps to disambiguate more @@ -132,7 +133,8 @@ private void addMethod(AnalysisMethod method) { Collection callees = invoke.getAllCallees(); var previousValue = invokeToCallees.get(key); if (previousValue == null) { - invokeToCallees.put(key, callees); + Object previous = invokeToCallees.putIfAbsent(key, callees); + assert previous == null : "A race condition occurred when inserting " + key; } else if (previousValue != COLLISION) { invokeToCallees.put(key, COLLISION); } @@ -156,7 +158,7 @@ public Set getCallers(AnalysisMethod callee) { * implementations of the given {@code targetMethod}. */ @SuppressWarnings("unchecked") - public Collection getTargetMethods(Invoke invoke, HostedMethod targetMethod) { + public Collection getCallees(Invoke invoke, HostedMethod targetMethod) { var node = invoke.asFixedNode(); var key = new InvokePosition(((HostedMethod) node.graph().method()).wrapped, invoke.bci(), targetMethod.wrapped); Object callees = invokeToCallees.get(key); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java index 4cacd31ddc44..fa0c9a36028a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java @@ -31,6 +31,14 @@ /** * Effect over {@link LocationIdentity} instances. Differentiates between empty location (noEffect), * single location, and any location (anyEffect). + * + * @formatter:off + * any-location + * / | \ + * location-0 ... location-n + * \ | / + * empty-location + * @formatter:on */ public sealed class LocationEffect implements TesaEffect permits LocationEffect.Any, LocationEffect.Empty, LocationEffect.Single { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java index a0c836950f18..73a5b76ab3dc 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java @@ -26,11 +26,10 @@ import com.oracle.graal.pointsto.PointsToAnalysis; import com.oracle.svm.hosted.analysis.tesa.AbstractTesa; -import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; +import com.oracle.svm.hosted.analysis.tesa.TesaEngine; /** - * The base effect used by the analyses implemented in - * {@link TransitiveEffectSummaryAnalysisEngine}. + * The base effect used by the analyses implemented in {@link TesaEngine}. *

* The effects form a lattice ordered so that lower values mean higher optimization potential, the * best one being {@code noEffect} at the bottom, while the {@code anyEffect} value represents that @@ -45,7 +44,7 @@ * further restricting the amount of {@link #combineEffects} calls before reaching {@code top}. * * @see AbstractTesa - * @see TransitiveEffectSummaryAnalysisEngine + * @see TesaEngine * */ public interface TesaEffect> { 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 d45bc1b22a3f..4274a545e538 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 @@ -72,7 +72,7 @@ import com.oracle.svm.hosted.NativeImageGenerator; import com.oracle.svm.hosted.NativeImageOptions; import com.oracle.svm.hosted.ProgressReporter; -import com.oracle.svm.hosted.analysis.tesa.TransitiveEffectSummaryAnalysisEngine; +import com.oracle.svm.hosted.analysis.tesa.TesaEngine; import com.oracle.svm.hosted.diagnostic.HostedHeapDumpFeature; import com.oracle.svm.hosted.imagelayer.HostedImageLayerBuildingSupport; import com.oracle.svm.hosted.imagelayer.LayeredDispatchTableFeature; @@ -1398,8 +1398,8 @@ private CompilationResult defaultCompileFunction(DebugContext debug, HostedMetho /* Check that graph is in good shape before compilation. */ assert GraphOrder.assertSchedulableGraph(graph); - if (TransitiveEffectSummaryAnalysisEngine.enabled()) { - TransitiveEffectSummaryAnalysisEngine.get().applyResults(method, graph); + if (TesaEngine.enabled()) { + TesaEngine.get().applyResults(method, graph); } try (DebugContext.Scope _ = debug.scope("Compiling", graph, method, this); diff --git a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java index 3cf79de22d48..e3e04d8da3fc 100644 --- a/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java +++ b/substratevm/src/com.oracle.svm.util/src/com/oracle/svm/util/ImageBuildStatistics.java @@ -139,8 +139,8 @@ private void printTimerStats(PrintWriter out) { } private void printTesaStats(PrintWriter out) { - if (ImageSingletons.contains(TransitiveEffectSummaryAnalysisPrinter.class)) { - ImageSingletons.lookup(TransitiveEffectSummaryAnalysisPrinter.class).printTesaResults(out); + if (ImageSingletons.contains(TesaPrinter.class)) { + ImageSingletons.lookup(TesaPrinter.class).printTesaResults(out); } } @@ -151,7 +151,7 @@ public interface TimerCollectionPrinter { void printTimerStats(PrintWriter out); } - public interface TransitiveEffectSummaryAnalysisPrinter { + public interface TesaPrinter { void printTesaResults(PrintWriter out); } } From e93b85b72ebaabd12208571d8090a7a894d0b541 Mon Sep 17 00:00:00 2001 From: David Kozak Date: Wed, 19 Nov 2025 18:41:44 +0100 Subject: [PATCH 3/4] make sure the AnalysisFields in FieldLocationIdentity are properly transplanted to HostedFields --- .../hosted/analysis/tesa/AbstractTesa.java | 7 +++-- .../analysis/tesa/KilledLocationTesa.java | 31 +++++++++++++++++-- .../svm/hosted/analysis/tesa/TesaEngine.java | 5 +-- .../oracle/svm/hosted/code/CompileQueue.java | 2 +- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/AbstractTesa.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/AbstractTesa.java index c0209795cbbd..3588b7c32d94 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/AbstractTesa.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/AbstractTesa.java @@ -40,6 +40,7 @@ import com.oracle.svm.common.meta.MultiMethod; import com.oracle.svm.hosted.analysis.tesa.effect.TesaEffect; import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedUniverse; import com.oracle.svm.util.ClassUtil; import jdk.graal.compiler.graph.Node; @@ -229,7 +230,7 @@ public T getState(TesaEngine engine, Invoke invoke) { /** * Apply the results of this analysis on the given compilation {@code graph}. */ - public void applyResults(TesaEngine engine, HostedMethod method, StructuredGraph graph) { + public void applyResults(TesaEngine engine, HostedUniverse universe, HostedMethod method, StructuredGraph graph) { var state = getState(method.wrapped); if (hasOptimizationPotential(state)) { onOptimizableMethodDiscovered(method, state, graph); @@ -240,7 +241,7 @@ public void applyResults(TesaEngine engine, HostedMethod method, StructuredGraph var targetState = getState(engine, invoke); if (hasOptimizationPotential(targetState)) { optimizableInvokesCounter.incrementAndGet(); - optimizeInvoke(graph, invoke, targetState); + optimizeInvoke(universe, graph, invoke, targetState); } } } @@ -279,7 +280,7 @@ protected boolean hasOptimizationPotential(T state) { * have to implement it. */ @SuppressWarnings("unused") - protected void optimizeInvoke(StructuredGraph graph, Invoke invoke, T targetState) { + protected void optimizeInvoke(HostedUniverse universe, StructuredGraph graph, Invoke invoke, T targetState) { } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java index de5c4955a9c0..f6fc23a8933f 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java @@ -26,12 +26,17 @@ import org.graalvm.word.LocationIdentity; +import com.oracle.graal.pointsto.meta.AnalysisField; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.util.AnalysisError; +import com.oracle.svm.core.graal.nodes.SubstrateFieldLocationIdentity; import com.oracle.svm.hosted.analysis.tesa.effect.LocationEffect; +import com.oracle.svm.hosted.code.AnalysisToHostedGraphTransplanter; import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedUniverse; import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.nodes.FieldLocationIdentity; import jdk.graal.compiler.nodes.Invoke; import jdk.graal.compiler.nodes.InvokeNode; import jdk.graal.compiler.nodes.InvokeWithExceptionNode; @@ -114,14 +119,36 @@ private static boolean checkNoSafepointNodes(HostedMethod method, LocationEffect } @Override - protected void optimizeInvoke(StructuredGraph graph, Invoke invoke, LocationEffect targetState) { + protected void optimizeInvoke(HostedUniverse universe, StructuredGraph graph, Invoke invoke, LocationEffect targetState) { switch (targetState) { case LocationEffect.Empty _ -> setKilledLocationIdentity(invoke, MemoryKill.NO_LOCATION); - case LocationEffect.Single single -> setKilledLocationIdentity(invoke, single.location); + case LocationEffect.Single single -> setKilledLocationIdentity(invoke, transplantIdentity(universe, single.location)); default -> AnalysisError.shouldNotReachHere(targetState + " is not actionable."); } } + /** + * The effects computed by the analysis may still contain analysis references that have + * to be transplanted to hosted. + * + * @see AnalysisToHostedGraphTransplanter + */ + private static LocationIdentity transplantIdentity(HostedUniverse universe, LocationIdentity location) { + return switch (location) { + case SubstrateFieldLocationIdentity substrateFieldLocationIdentity -> { + var field = substrateFieldLocationIdentity.getField(); + assert field instanceof AnalysisField : "The field computed by the TESA should be still an analysis field: " + field; + yield new SubstrateFieldLocationIdentity(universe.lookup(field), substrateFieldLocationIdentity.isImmutable()); + } + case FieldLocationIdentity fieldLocationIdentity -> { + var field = fieldLocationIdentity.getField(); + assert field instanceof AnalysisField : "The field computed by the TESA should be still an analysis field: " + field; + yield new FieldLocationIdentity(universe.lookup(field), fieldLocationIdentity.isImmutable()); + } + default -> location; + }; + } + private static void setKilledLocationIdentity(Invoke invoke, LocationIdentity locationIdentity) { switch (invoke) { case InvokeNode invokeNode -> invokeNode.setKilledLocationIdentity(locationIdentity); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java index 80f58f091c22..bc4e893edfb9 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java @@ -46,6 +46,7 @@ import com.oracle.svm.core.util.VMError; import com.oracle.svm.hosted.analysis.tesa.effect.TesaEffect; import com.oracle.svm.hosted.meta.HostedMethod; +import com.oracle.svm.hosted.meta.HostedUniverse; import com.oracle.svm.util.ClassUtil; import com.oracle.svm.util.ImageBuildStatistics; @@ -181,7 +182,7 @@ public void runFixedPointLoops(BigBang bb) { /** * @see AbstractTesa#applyResults */ - public void applyResults(HostedMethod method, StructuredGraph graph) { + public void applyResults(HostedUniverse universe, HostedMethod method, StructuredGraph graph) { totalMethodsCounter.incrementAndGet(); for (Node node : graph.getNodes()) { if (node instanceof Invoke) { @@ -189,7 +190,7 @@ public void applyResults(HostedMethod method, StructuredGraph graph) { } } for (AbstractTesa analysis : analyses.values()) { - analysis.applyResults(this, method, graph); + analysis.applyResults(this, universe, method, graph); } } 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 4274a545e538..6a9e37cfe27c 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 @@ -1399,7 +1399,7 @@ private CompilationResult defaultCompileFunction(DebugContext debug, HostedMetho assert GraphOrder.assertSchedulableGraph(graph); if (TesaEngine.enabled()) { - TesaEngine.get().applyResults(method, graph); + TesaEngine.get().applyResults(universe, method, graph); } try (DebugContext.Scope _ = debug.scope("Compiling", graph, method, this); From ed3ac7b9841f9f9d1a00445d5dd5d8fa51c48b8f Mon Sep 17 00:00:00 2001 From: David Kozak Date: Wed, 19 Nov 2025 19:12:55 +0100 Subject: [PATCH 4/4] auto-enable TesaPrintToConsole only with assertions enabled --- .../src/com/oracle/svm/hosted/NativeImageGenerator.java | 3 +-- .../src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index 783dff0b35c6..c648e6f6ea53 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -53,8 +53,6 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; -import com.oracle.svm.core.imagelayer.LayeredImageOptions; -import com.oracle.svm.hosted.reflect.ReflectionFeature; import org.graalvm.collections.EconomicSet; import org.graalvm.collections.Pair; import org.graalvm.nativeimage.ImageInfo; @@ -160,6 +158,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.image.ImageHeapLayouter; import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; +import com.oracle.svm.core.imagelayer.LayeredImageOptions; import com.oracle.svm.core.jdk.ServiceCatalogSupport; import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.meta.MethodOffset; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java index bc4e893edfb9..ec04a463e552 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java @@ -38,6 +38,7 @@ import com.oracle.graal.pointsto.BigBang; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.results.StrengthenGraphs; +import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.option.HostedOptionKey; import com.oracle.svm.core.traits.BuiltinTraits.BuildtimeAccessOnly; import com.oracle.svm.core.traits.BuiltinTraits.NoLayeredCallbacks; @@ -84,8 +85,8 @@ public static class Options { @Option(help = "Enable Transitive Effect Summary Analysis (TESA).")// public static final HostedOptionKey TransitiveEffectSummaryAnalysis = new HostedOptionKey<>(true); - @Option(help = "Print TESA results to the console.")// - public static final HostedOptionKey TesaPrintToConsole = new HostedOptionKey<>(true); + @Option(help = "Print TESA results to the console. Enabled automatically with assertions.")// + public static final HostedOptionKey TesaPrintToConsole = new HostedOptionKey<>(SubstrateUtil.assertionsEnabled()); @Option(help = "Throw an exception if any TESA instance fails to reach a fixed point within the expected number of iterations.")// public static final HostedOptionKey TesaThrowOnNonTermination = new HostedOptionKey<>(true);