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..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 @@ -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(); } @@ -146,7 +150,11 @@ public Map getDebugProperties(Map map) { @Override public LocationIdentity getKilledLocationIdentity() { - return identity; + return killedLocationIdentity; + } + + public void setKilledLocationIdentity(LocationIdentity identity) { + this.killedLocationIdentity = identity; } @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..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 @@ -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,20 @@ public void setStateAfter(FrameState stateAfter) { @Override public boolean hasSideEffect() { - return true; + return sideEffect; + } + + public void setSideEffect(boolean withSideEffects) { + this.sideEffect = withSideEffects; } @Override public LocationIdentity getKilledLocationIdentity() { - return LocationIdentity.any(); + return killedLocationIdentity; + } + + public void setKilledLocationIdentity(LocationIdentity killedLocationIdentity) { + this.killedLocationIdentity = killedLocationIdentity; } @Override 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..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; @@ -201,6 +200,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.TesaEngine; import com.oracle.svm.hosted.annotation.SubstrateAnnotationExtractor; import com.oracle.svm.hosted.c.CAnnotationProcessorCache; import com.oracle.svm.hosted.c.CConstantValueSupportImpl; @@ -253,6 +253,8 @@ 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.ReflectionFeature; 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 (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. + */ + TesaEngine.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 (TesaEngine.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. + */ + TesaEngine.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 (TesaEngine.enabled()) { + TesaEngine.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 +605,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 +626,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 +646,9 @@ public void printCreationEnd(int imageFileSize, int heapObjectCount, long imageH l().a(", %s in total file size", ByteFormattingUtil.bytesToHuman(imageDiskFileSize)); } l().println(); + if (TesaEngine.enabled() && TesaPrintToConsole.getValue()) { + printTesaStatistics(); + } printBreakdowns(); ImageSingletons.lookup(ProgressReporterFeature.class).afterBreakdowns(); printRecommendations(); @@ -756,6 +763,42 @@ private void printRecommendations() { } } + private void printTesaStatistics() { + TesaEngine engine = TesaEngine.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 +827,7 @@ public void printEpilog(Optional optionalImageName, Optional the effect tracked by the given analysis. + * @see TesaEngine + */ +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}. + *

+ * 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); + + /** + * Computes and saves the initial state of this method to be used later by the analysis. + */ + void initializeStateForMethod(AnalysisMethod method, StructuredGraph graph) { + T previous = methodToState.put(method, computeInitialState(method, graph)); + AnalysisError.guarantee(previous == 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(TesaEngine 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 (TesaEngine.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(TesaEngine engine, Invoke invoke) { + var targetMethod = ((HostedMethod) invoke.callTarget().targetMethod()); + if (invoke.getInvokeKind().isDirect()) { + return getState(targetMethod.wrapped); + } else { + T cummulativeState = noEffect(); + for (AnalysisMethod callee : engine.getCallGraph().getCallees(invoke, targetMethod)) { + var targetState = getState(callee); + 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(TesaEngine engine, HostedUniverse universe, HostedMethod method, StructuredGraph graph) { + var state = getState(method.wrapped); + if (hasOptimizationPotential(state)) { + onOptimizableMethodDiscovered(method, state, graph); + 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(universe, graph, invoke, targetState); + } + } + } + } + + /** + * 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. + */ + @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(HostedUniverse universe, 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..f6fc23a8933f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/KilledLocationTesa.java @@ -0,0 +1,159 @@ +/* + * 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.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; +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; +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 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(HostedUniverse universe, StructuredGraph graph, Invoke invoke, LocationEffect targetState) { + switch (targetState) { + case LocationEffect.Empty _ -> setKilledLocationIdentity(invoke, MemoryKill.NO_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); + 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/TesaEngine.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java new file mode 100644 index 000000000000..ec04a463e552 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaEngine.java @@ -0,0 +1,263 @@ +/* + * 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.SubstrateUtil; +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; +import com.oracle.svm.hosted.meta.HostedUniverse; +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}. + */ +@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).")// + public static final HostedOptionKey TransitiveEffectSummaryAnalysis = 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); + } + + /** + * 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 TesaEngine() { + 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(TesaEngine.class); + } + + /** + * Gets the singleton instance of the engine. + */ + public static TesaEngine get() { + return ImageSingletons.lookup(TesaEngine.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(HostedUniverse universe, 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, universe, 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/TesaReverseCallGraph.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java new file mode 100644 index 000000000000..47b02d0ac137 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/TesaReverseCallGraph.java @@ -0,0 +1,181 @@ +/* + * 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 TesaEngine} 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, 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 + * 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) { + Object previous = invokeToCallees.putIfAbsent(key, callees); + assert previous == null : "A race condition occurred when inserting " + key; + } 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 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); + 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/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..fa0c9a36028a --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/LocationEffect.java @@ -0,0 +1,137 @@ +/* + * 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). + * + * @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 { + + @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..73a5b76ab3dc --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/analysis/tesa/effect/TesaEffect.java @@ -0,0 +1,71 @@ +/* + * 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.TesaEngine; + +/** + * 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 + * 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 TesaEngine + * + */ +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..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 @@ -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.TesaEngine; 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 (TesaEngine.enabled()) { + TesaEngine.get().applyResults(universe, 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..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 @@ -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(TesaPrinter.class)) { + ImageSingletons.lookup(TesaPrinter.class).printTesaResults(out); + } + } + static final String INDENT = " "; } public interface TimerCollectionPrinter { void printTimerStats(PrintWriter out); } + + public interface TesaPrinter { + 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); }