diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/gen/DebugInfoBuilder.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/gen/DebugInfoBuilder.java index a56657b90244..351c1135a2af 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/gen/DebugInfoBuilder.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/gen/DebugInfoBuilder.java @@ -28,12 +28,14 @@ import java.util.Arrays; import java.util.Queue; -import jdk.graal.compiler.debug.CounterKey; -import jdk.graal.compiler.debug.DebugContext; -import jdk.graal.compiler.debug.GraalError; import org.graalvm.collections.EconomicMap; import org.graalvm.collections.Equivalence; + import jdk.graal.compiler.core.common.spi.MetaAccessExtensionProvider; +import jdk.graal.compiler.debug.Assertions; +import jdk.graal.compiler.debug.CounterKey; +import jdk.graal.compiler.debug.DebugContext; +import jdk.graal.compiler.debug.GraalError; import jdk.graal.compiler.lir.ConstantValue; import jdk.graal.compiler.lir.ImplicitLIRFrameState; import jdk.graal.compiler.lir.LIRFrameState; @@ -49,8 +51,6 @@ import jdk.graal.compiler.nodes.virtual.VirtualBoxingNode; import jdk.graal.compiler.nodes.virtual.VirtualObjectNode; import jdk.graal.compiler.nodes.virtual.VirtualObjectState; -import jdk.graal.compiler.debug.Assertions; - import jdk.vm.ci.code.BytecodeFrame; import jdk.vm.ci.code.RegisterValue; import jdk.vm.ci.code.VirtualObject; @@ -269,6 +269,7 @@ protected BytecodeFrame computeFrameForState(NodeWithState node, FrameState stat assert state.bci != BytecodeFrame.AFTER_EXCEPTION_BCI || state.locksSize() == 0 : Assertions.errorMessageContext("node", node, "state", state); assert !(state.getMethod().isSynchronized() && state.bci != BytecodeFrame.BEFORE_BCI && state.bci != BytecodeFrame.AFTER_BCI && state.bci != BytecodeFrame.AFTER_EXCEPTION_BCI) || + !state.isValidForDeoptimization() || state.locksSize() > 0 : Assertions.errorMessageContext("state", state, "node", node, "bci", state.bci); assert state.verify(); diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md index ed6f0fb33a32..7a000f12ff3d 100644 --- a/substratevm/CHANGELOG.md +++ b/substratevm/CHANGELOG.md @@ -16,6 +16,7 @@ This changelog summarizes major changes to GraalVM Native Image. * (GR-47832) Experimental support for upcalls from foreign code and other improvements to our implementation of the [Foreign Function & Memory API](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md) (part of "Project Panama", [JEP 454](https://openjdk.org/jeps/454)) on AMD64. Must be enabled with `-H:+ForeignAPISupport` (requiring `-H:+UnlockExperimentalVMOptions`). * (GR-52314) `-XX:MissingRegistrationReportingMode` can now be used on program invocation instead of as a build option, to avoid a rebuild when debugging missing registration errors. * (GR-51086) Introduce a new `--static-nolibc` API option as a replacement for the experimental `-H:±StaticExecutableWithDynamicLibC` option. +* (GR-52732) Introduce a new `ReduceImplicitExceptionStackTraceInformation` hosted option that reduces image size by reducing the runtime metadata for implicit exceptions, at the cost of stack trace precision. The option is diabled by default, but enabled with optimization level 3 and profile guided optimizations. * (GR-52534) Change the digest (used e.g. for symbol names) from SHA-1 encoded as a hex string (40 bytes) to 128-bit Murmur3 as a Base-62 string (22 bytes). * (GR-52578) Print information about embedded resources into `embedded-resources.json` using the `-H:+GenerateEmbeddedResourcesFile` option. * (GR-51172) Add support to catch OutOfMemoryError exceptions on native image if there is no memory left. diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 402265135de4..b930b5f55b30 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -285,6 +285,14 @@ protected void onValueUpdate(EconomicMap, Object> values, String ol SubstrateOptions.IncludeNodeSourcePositions.update(values, newLevel == OptimizationLevel.O0); SubstrateOptions.SourceLevelDebug.update(values, newLevel == OptimizationLevel.O0); SubstrateOptions.AOTTrivialInline.update(values, newLevel != OptimizationLevel.O0); + + /* + * We do not want to enable this optimization yet by default, because it reduces the + * precision of implicit stack traces. But for optimized release builds, including pgo + * builds, it is a valuable image size reduction. + */ + SubstrateOptions.ReduceImplicitExceptionStackTraceInformation.update(values, newLevel == OptimizationLevel.O3); + GraalOptions.OptimizeLongJumps.update(values, !newLevel.isOneOf(OptimizationLevel.O0, OptimizationLevel.BUILD_TIME)); if (optimizeValueUpdateHandler != null) { optimizeValueUpdateHandler.onValueUpdate(values, newLevel); @@ -1167,4 +1175,8 @@ public static class TruffleStableOptions { "If there is no native-image-resources.filelist file in the language home directory or the file is empty, then no resources are copied.", type = User, stability = OptionStability.STABLE)// public static final HostedOptionKey CopyLanguageResources = new HostedOptionKey<>(true); } + + @Option(help = "Reduce the amount of metadata in the image for implicit exceptions by removing inlining information from the stack trace. " + + "This makes the image smaller, but also the stack trace of implicit exceptions less precise.", type = OptionType.Expert)// + public static final HostedOptionKey ReduceImplicitExceptionStackTraceInformation = new HostedOptionKey<>(false); } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/snippets/ImplicitExceptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/snippets/ImplicitExceptions.java index 7befcc109a3b..f787b34ef89f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/snippets/ImplicitExceptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/snippets/ImplicitExceptions.java @@ -35,6 +35,7 @@ import com.oracle.svm.core.heap.RestrictHeapAccess; import com.oracle.svm.core.jdk.InternalVMMethod; import com.oracle.svm.core.jdk.StackTraceUtils; +import com.oracle.svm.core.option.SubstrateOptionsParser; import com.oracle.svm.core.snippets.SnippetRuntime.SubstrateForeignCallDescriptor; import com.oracle.svm.core.threadlocal.FastThreadLocalFactory; import com.oracle.svm.core.threadlocal.FastThreadLocalInt; @@ -106,6 +107,20 @@ public class ImplicitExceptions { public static final SubstrateForeignCallDescriptor THROW_NEW_ASSERTION_ERROR_NULLARY = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwNewAssertionErrorNullary", NO_SIDE_EFFECT); public static final SubstrateForeignCallDescriptor THROW_NEW_ASSERTION_ERROR_OBJECT = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwNewAssertionErrorObject", NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor CREATE_OPT_NULL_POINTER_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "createOptNullPointerException", HAS_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor CREATE_OPT_OUT_OF_BOUNDS_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "createOptOutOfBoundsException", HAS_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor CREATE_OPT_CLASS_CAST_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "createOptClassCastException", HAS_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor CREATE_OPT_ARRAY_STORE_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "createOptArrayStoreException", HAS_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor CREATE_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "createOptIncompatibleClassChangeError", + HAS_SIDE_EFFECT); + + public static final SubstrateForeignCallDescriptor THROW_OPT_NULL_POINTER_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwOptNullPointerException", NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor THROW_OPT_OUT_OF_BOUNDS_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwOptOutOfBoundsException", NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor THROW_OPT_CLASS_CAST_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwOptClassCastException", NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor THROW_OPT_ARRAY_STORE_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwOptArrayStoreException", NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor THROW_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "throwOptIncompatibleClassChangeError", + NO_SIDE_EFFECT); + public static final SubstrateForeignCallDescriptor GET_CACHED_NULL_POINTER_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "getCachedNullPointerException", HAS_SIDE_EFFECT); public static final SubstrateForeignCallDescriptor GET_CACHED_OUT_OF_BOUNDS_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "getCachedOutOfBoundsException", HAS_SIDE_EFFECT); public static final SubstrateForeignCallDescriptor GET_CACHED_CLASS_CAST_EXCEPTION = SnippetRuntime.findForeignCall(ImplicitExceptions.class, "getCachedClassCastException", HAS_SIDE_EFFECT); @@ -150,6 +165,9 @@ public class ImplicitExceptions { THROW_CACHED_NULL_POINTER_EXCEPTION, THROW_CACHED_OUT_OF_BOUNDS_EXCEPTION, THROW_CACHED_CLASS_CAST_EXCEPTION, THROW_CACHED_ARRAY_STORE_EXCEPTION, THROW_CACHED_INCOMPATIBLE_CLASS_CHANGE_ERROR, THROW_CACHED_ILLEGAL_ARGUMENT_EXCEPTION, THROW_CACHED_NEGATIVE_ARRAY_SIZE_EXCEPTION, THROW_CACHED_ARITHMETIC_EXCEPTION, THROW_CACHED_ASSERTION_ERROR, + CREATE_OPT_NULL_POINTER_EXCEPTION, CREATE_OPT_OUT_OF_BOUNDS_EXCEPTION, CREATE_OPT_CLASS_CAST_EXCEPTION, CREATE_OPT_ARRAY_STORE_EXCEPTION, + CREATE_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR, + THROW_OPT_NULL_POINTER_EXCEPTION, THROW_OPT_OUT_OF_BOUNDS_EXCEPTION, THROW_OPT_CLASS_CAST_EXCEPTION, THROW_OPT_ARRAY_STORE_EXCEPTION, THROW_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR }; private static final FastThreadLocalInt implicitExceptionsAreFatal = FastThreadLocalFactory.createInt("ImplicitExceptions.implicitExceptionsAreFatal"); @@ -406,6 +424,79 @@ private static void throwNewAssertionErrorObject(Object detailMessage) { throw new AssertionError(detailMessage); } + private static final String IMPRECISE_STACK_MSG = "Stack trace is imprecise, the top frames are missing and/or have wrong line numbers. To get precise stack traces, build the image with option " + + SubstrateOptionsParser.commandArgument(SubstrateOptions.ReduceImplicitExceptionStackTraceInformation, "-"); + + /** Foreign call: {@link #CREATE_OPT_NULL_POINTER_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static NullPointerException createOptNullPointerException() { + vmErrorIfImplicitExceptionsAreFatal(false); + return new NullPointerException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #CREATE_OPT_OUT_OF_BOUNDS_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static ArrayIndexOutOfBoundsException createOptOutOfBoundsException() { + vmErrorIfImplicitExceptionsAreFatal(false); + return new ArrayIndexOutOfBoundsException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #CREATE_OPT_CLASS_CAST_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static ClassCastException createOptClassCastException() { + vmErrorIfImplicitExceptionsAreFatal(false); + return new ClassCastException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #CREATE_OPT_ARRAY_STORE_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static ArrayStoreException createOptArrayStoreException() { + vmErrorIfImplicitExceptionsAreFatal(false); + return new ArrayStoreException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #CREATE_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static IncompatibleClassChangeError createOptIncompatibleClassChangeError() { + vmErrorIfImplicitExceptionsAreFatal(false); + return new IncompatibleClassChangeError(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #THROW_OPT_NULL_POINTER_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static void throwOptNullPointerException() { + vmErrorIfImplicitExceptionsAreFatal(false); + throw new NullPointerException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #THROW_OPT_OUT_OF_BOUNDS_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static void throwOptOutOfBoundsException() { + vmErrorIfImplicitExceptionsAreFatal(false); + throw new ArrayIndexOutOfBoundsException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #THROW_OPT_CLASS_CAST_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static void throwOptClassCastException() { + vmErrorIfImplicitExceptionsAreFatal(false); + throw new ClassCastException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #THROW_OPT_ARRAY_STORE_EXCEPTION}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static void throwOptArrayStoreException() { + vmErrorIfImplicitExceptionsAreFatal(false); + throw new ArrayStoreException(IMPRECISE_STACK_MSG); + } + + /** Foreign call: {@link #THROW_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR}. */ + @SubstrateForeignCallTarget(stubCallingConvention = true) + private static void throwOptIncompatibleClassChangeError() { + vmErrorIfImplicitExceptionsAreFatal(false); + throw new IncompatibleClassChangeError(IMPRECISE_STACK_MSG); + } + /** Foreign call: {@link #GET_CACHED_NULL_POINTER_EXCEPTION}. */ @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Called to report an implicit exception in code that must not allocate.") @SubstrateForeignCallTarget(stubCallingConvention = true) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/ReduceImplicitExceptionStackTraceInformationPhase.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/ReduceImplicitExceptionStackTraceInformationPhase.java new file mode 100644 index 000000000000..bd72185bdb76 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/phases/ReduceImplicitExceptionStackTraceInformationPhase.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2024, 2024, 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.phases; + +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.graalvm.collections.EconomicMap; + +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.graal.nodes.LoweredDeadEndNode; +import com.oracle.svm.core.graal.nodes.ThrowBytecodeExceptionNode; +import com.oracle.svm.core.graal.phases.RemoveUnwindPhase; +import com.oracle.svm.core.graal.snippets.NonSnippetLowerings; +import com.oracle.svm.core.snippets.ImplicitExceptions; +import com.oracle.svm.core.util.VMError; + +import jdk.graal.compiler.core.common.spi.ForeignCallDescriptor; +import jdk.graal.compiler.nodes.EndNode; +import jdk.graal.compiler.nodes.FixedNode; +import jdk.graal.compiler.nodes.FrameState; +import jdk.graal.compiler.nodes.LoopExitNode; +import jdk.graal.compiler.nodes.MergeNode; +import jdk.graal.compiler.nodes.NodeView; +import jdk.graal.compiler.nodes.StructuredGraph; +import jdk.graal.compiler.nodes.UnwindNode; +import jdk.graal.compiler.nodes.extended.BytecodeExceptionNode; +import jdk.graal.compiler.nodes.extended.ForeignCallNode; +import jdk.graal.compiler.nodes.util.GraphUtil; +import jdk.graal.compiler.phases.BasePhase; +import jdk.graal.compiler.phases.common.FinalCanonicalizerPhase; +import jdk.graal.compiler.phases.common.FrameStateAssignmentPhase; +import jdk.graal.compiler.phases.tiers.LowTierContext; +import jdk.graal.compiler.phases.tiers.Suites; +import jdk.graal.compiler.phases.util.Providers; + +@AutomaticallyRegisteredFeature +class ReduceImplicitExceptionStackTraceInformationFeature implements InternalFeature { + @Override + public void registerGraalPhases(Providers providers, Suites suites, boolean hosted) { + if (hosted && SubstrateOptions.ReduceImplicitExceptionStackTraceInformation.getValue()) { + /* + * Add as late as possible, before the final canonicalization. A canonicalization is + * necessary because this phase can make other nodes unreachable, and the canonicalizer + * cleans that up. + */ + ListIterator> finalCanonicalizer = suites.getLowTier().findPhase(FinalCanonicalizerPhase.class); + if (finalCanonicalizer == null) { + throw VMError.shouldNotReachHere("In a reduced phase plan without a final canonicalization, the " + + SubstrateOptions.ReduceImplicitExceptionStackTraceInformation.getName() + " option must be disabled."); + } + finalCanonicalizer.previous(); + finalCanonicalizer.add(new ReduceImplicitExceptionStackTraceInformationPhase()); + } + } +} + +/** + * This phase reduces the runtime metadata for implicit exceptions, at the cost of stack trace + * precision. + *

+ * Implicit exceptions are represented by {@link BytecodeExceptionNode} after bytecode parsing. The + * {@link RemoveUnwindPhase} already converts {@link BytecodeExceptionNode} that directly lead to an + * {@link UnwindNode} with a {@link ThrowBytecodeExceptionNode}, but that node still has the full + * inlined {@link FrameState} information. Both {@link BytecodeExceptionNode} and + * {@link ThrowBytecodeExceptionNode} are lowered to {@link ForeignCallNode} by + * {@link NonSnippetLowerings}. The foreign calls use various descriptors defined in + * {@link ImplicitExceptions}. + *

+ * This phase is designed to run late in the lower tier of the compilation pipeline, after all of + * the above has happened. This has several advantages: + *

    + *
  • We do not need to find out anymore if the implicit exception can be caught inside the method + * or is always unwound, this is decided by {@link RemoveUnwindPhase}
  • + *
  • We do not need to find out if we are in a method that must not allocate, the lowering already + * picks the proper foreign call descriptor in {@link ImplicitExceptions}
  • + *
  • We do not need to worry about the "state after" for {@link ForeignCallNode} and + * {@link MergeNode} because we are already after the {@link FrameStateAssignmentPhase}
  • + *
  • We do not need to worry about {@link LoopExitNode}. When {@link RemoveUnwindPhase} produces a + * {@link ThrowBytecodeExceptionNode} it removes all {@link LoopExitNode} that are between the + * implicit exception point and the {@link UnwindNode}. The compiler is happy with that because + * control flow terminates at the {@link ThrowBytecodeExceptionNode}. But when this phase inserts a + * {@link MergeNode} for implicit exceptions from different loops, we would need to insert + * {@link LoopExitNode} to make high-tier and mid-tier optimization phases happy. At the end of the + * low tier, just before scheduling, no compiler phase cares about {@link LoopExitNode} + * anymore.
  • + *
+ * + * In this phase, more optimizations are done for {@link ForeignCallNode} that come from + * {@link ThrowBytecodeExceptionNode} (called "throw descriptors" for simplicity) is more complete + * compared to {@link ForeignCallNode} that come from {@link BytecodeExceptionNode} (called "create + * descriptors"): + *

+ * For "throw descriptors", we know that control flow ends, which means that at that point the + * method cannot hold any locks anymore. We can drop all inlining information, and reduce the frame + * state to an all-empty state of just the root compilation unit. All such "throw descriptors" for + * the same exception class can be merged to a single foreign call. + *

+ * For "create descriptors", locking and escape analysis complicate the handling: We cannot drop any + * frame state that has locking information. While we can drop all local variables still in that + * frame, all caller frame states remain unchanged, and therefore also need virtual object + * information for escape analyzed objects. Merging multiple "create descriptors" for the same + * exception class is therefore too complicated to be worth it. Since in a typical application there + * are more than 5x as many "throw descriptors" than "create descriptors", that does not lead to too + * much loss of optimization. + *

+ * For both "throw descriptors" and "create descriptors", the foreign call is changed to a variant + * that does not take any arguments. That means in addition to inlining information we also loose + * detailed information for the exception message about which array element cannot be accessed, + * which object can not be cast, ... Removing this information is a major part of the code size + * reduction achieved by this phase, because it avoids the machine code to move all these values + * into the proper argument registers. + */ +class ReduceImplicitExceptionStackTraceInformationPhase extends BasePhase { + + private static final Map optimizedCreateDescriptors = Map.ofEntries( + Map.entry(ImplicitExceptions.CREATE_NULL_POINTER_EXCEPTION, ImplicitExceptions.CREATE_OPT_NULL_POINTER_EXCEPTION), + Map.entry(ImplicitExceptions.CREATE_OUT_OF_BOUNDS_EXCEPTION, ImplicitExceptions.CREATE_OPT_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.CREATE_INTRINSIC_OUT_OF_BOUNDS_EXCEPTION, ImplicitExceptions.CREATE_OPT_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.CREATE_CLASS_CAST_EXCEPTION, ImplicitExceptions.CREATE_OPT_CLASS_CAST_EXCEPTION), + Map.entry(ImplicitExceptions.CREATE_ARRAY_STORE_EXCEPTION, ImplicitExceptions.CREATE_OPT_ARRAY_STORE_EXCEPTION), + Map.entry(ImplicitExceptions.CREATE_INCOMPATIBLE_CLASS_CHANGE_ERROR, ImplicitExceptions.CREATE_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR), + /* + * The remaining descriptors are not changed by this phase. But frame states are + * still cleared, so there is still a benefit. + */ + Map.entry(ImplicitExceptions.GET_CACHED_NULL_POINTER_EXCEPTION, ImplicitExceptions.GET_CACHED_NULL_POINTER_EXCEPTION), + Map.entry(ImplicitExceptions.GET_CACHED_OUT_OF_BOUNDS_EXCEPTION, ImplicitExceptions.GET_CACHED_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.GET_CACHED_CLASS_CAST_EXCEPTION, ImplicitExceptions.GET_CACHED_CLASS_CAST_EXCEPTION), + Map.entry(ImplicitExceptions.GET_CACHED_ARRAY_STORE_EXCEPTION, ImplicitExceptions.GET_CACHED_ARRAY_STORE_EXCEPTION), + Map.entry(ImplicitExceptions.GET_CACHED_INCOMPATIBLE_CLASS_CHANGE_ERROR, ImplicitExceptions.GET_CACHED_INCOMPATIBLE_CLASS_CHANGE_ERROR)); + private static final Map optimizedThrowDescriptors = Map.ofEntries( + Map.entry(ImplicitExceptions.THROW_NEW_NULL_POINTER_EXCEPTION, ImplicitExceptions.THROW_OPT_NULL_POINTER_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_OUT_OF_BOUNDS_EXCEPTION_WITH_ARGS, ImplicitExceptions.THROW_OPT_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_INTRINSIC_OUT_OF_BOUNDS_EXCEPTION, ImplicitExceptions.THROW_OPT_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_CLASS_CAST_EXCEPTION_WITH_ARGS, ImplicitExceptions.THROW_OPT_CLASS_CAST_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_CLASS_CAST_EXCEPTION, ImplicitExceptions.THROW_OPT_CLASS_CAST_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_ARRAY_STORE_EXCEPTION_WITH_ARGS, ImplicitExceptions.THROW_OPT_ARRAY_STORE_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_ARRAY_STORE_EXCEPTION, ImplicitExceptions.THROW_OPT_ARRAY_STORE_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_NEW_INCOMPATIBLE_CLASS_CHANGE_ERROR, ImplicitExceptions.THROW_OPT_INCOMPATIBLE_CLASS_CHANGE_ERROR), + /* + * The remaining descriptors are not changed by this phase. But frame states are + * still cleared, and multiple usages in the same method are merged to a single + * one, so there is still a benefit. + */ + Map.entry(ImplicitExceptions.THROW_CACHED_NULL_POINTER_EXCEPTION, ImplicitExceptions.THROW_CACHED_NULL_POINTER_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_CACHED_OUT_OF_BOUNDS_EXCEPTION, ImplicitExceptions.THROW_CACHED_OUT_OF_BOUNDS_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_CACHED_CLASS_CAST_EXCEPTION, ImplicitExceptions.THROW_CACHED_CLASS_CAST_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_CACHED_ARRAY_STORE_EXCEPTION, ImplicitExceptions.THROW_CACHED_ARRAY_STORE_EXCEPTION), + Map.entry(ImplicitExceptions.THROW_CACHED_INCOMPATIBLE_CLASS_CHANGE_ERROR, ImplicitExceptions.THROW_CACHED_INCOMPATIBLE_CLASS_CHANGE_ERROR)); + + @Override + protected void run(StructuredGraph graph, LowTierContext context) { + EconomicMap optimizedThrowReplacements = EconomicMap.create(); + + /* We need a snapshot so that new ForeignCallNode added by this phase are not processed. */ + for (var node : graph.getNodes().filter(ForeignCallNode.class).snapshot()) { + if (optimizedCreateDescriptors.containsKey(node.getDescriptor())) { + clearFrameStateForOptimizedCreate(node); + } else if (optimizedThrowDescriptors.containsKey(node.getDescriptor())) { + combineForeignCallForOptimizedThrow(node, optimizedThrowReplacements); + } + } + } + + /** + * Graal IR before this method: a {@link ForeignCallNode} with a frame state that has inlining + * and local variables filled. + * + * GraalIR after this method: a new {@link ForeignCallNode} with a different call target + * descriptor. The frame state has all local variables cleared, but all inlining is preserved. + */ + private static void clearFrameStateForOptimizedCreate(ForeignCallNode originalForeignCall) { + var graph = originalForeignCall.graph(); + var newDescriptor = optimizedCreateDescriptors.get(originalForeignCall.getDescriptor()); + + FrameState originalState = originalForeignCall.stateDuring(); + /* + * Local variables are cleared, but inlining information and locks are preserved. For outer + * states and locks we also need to preserve virtual object mappings. Some of the virtual + * object mappings might be unnecessary, but it would be tedious to find and filter them. + */ + FrameState newState = graph.add(new FrameState(originalState.outerFrameState(), originalState.getCode(), 0, + originalState.values().subList(0, originalState.locksSize()), + originalState.localsSize(), 0, originalState.locksSize(), FrameState.StackState.AfterPop, false, + originalState.monitorIds(), originalState.virtualObjectMappings(), null)); + + ForeignCallNode newForeignCall = graph.add(new ForeignCallNode(newDescriptor, originalForeignCall.stamp(NodeView.DEFAULT), List.of())); + newForeignCall.setStateDuring(newState); + graph.replaceFixedWithFixed(originalForeignCall, newForeignCall); + } + + /** + * GraalIR before this method: a {@link ForeignCallNode} that never returns, i.e., control flow + * after it is dead. + * + * GraalIR after this method:a new {@link ForeignCallNode} that never returns, with all local + * variables cleared and no inlining; or a control flow merge to such a previously inserted + * {@link ForeignCallNode}. + */ + private static void combineForeignCallForOptimizedThrow(ForeignCallNode originalForeignCall, EconomicMap replacements) { + var graph = originalForeignCall.graph(); + var newDescriptor = optimizedThrowDescriptors.get(originalForeignCall.getDescriptor()); + var existingReplacement = replacements.get(newDescriptor); + + ForeignCallNode newForeignCall; + FixedNode newSuccessor; + if (existingReplacement == null) { + /* + * First occurrence of that exception class. Replace the foreign call with a new foreign + * call to the optimized runtime method, with a frame state that is top-level only and + * has all local variables cleared. + */ + FrameState outermostState = originalForeignCall.stateDuring(); + while (outermostState.outerFrameState() != null) { + outermostState = outermostState.outerFrameState(); + } + /* Drop all inlining, all local variables, and all locking. */ + var newStateDuring = graph.add(new FrameState(null, outermostState.getCode(), 0, + List.of(), + outermostState.localsSize(), 0, 0, FrameState.StackState.AfterPop, false, + null, null, null)); + + newForeignCall = graph.add(new ForeignCallNode(newDescriptor, originalForeignCall.stamp(NodeView.DEFAULT), List.of())); + newForeignCall.setStateDuring(newStateDuring); + /* + * The foreign call does not return, and the exception it throws is unwound to the + * caller frame. + */ + newForeignCall.setNext(graph.add(new LoweredDeadEndNode())); + replacements.put(newDescriptor, newForeignCall); + newSuccessor = newForeignCall; + + } else { + MergeNode replacementMerge; + if (existingReplacement instanceof ForeignCallNode) { + /* + * Second occurrence of that exception class. We already have the optimized foreign + * call from the first occurrence, now we need to insert a control flow merge so + * that we can use it from more than one place. + */ + newForeignCall = (ForeignCallNode) existingReplacement; + replacementMerge = graph.add(new MergeNode()); + replacements.put(newDescriptor, replacementMerge); + + EndNode firstEnd = graph.add(new EndNode()); + newForeignCall.replaceAtPredecessor(firstEnd); + replacementMerge.addForwardEnd(firstEnd); + replacementMerge.setNext(newForeignCall); + + } else { + /* + * Third or more occurrence of that exception class. We already have the foreign + * call and the control flow merge, we only need to add a new edge to that merge. + */ + replacementMerge = (MergeNode) existingReplacement; + newForeignCall = (ForeignCallNode) replacementMerge.next(); + } + + EndNode newEnd = graph.add(new EndNode()); + replacementMerge.addForwardEnd(newEnd); + newSuccessor = newEnd; + } + /* + * Replace the original foreign call with the replacement one, and remove the original + * foreign call. + */ + originalForeignCall.replaceAtUsages(newForeignCall); + originalForeignCall.replaceAtPredecessor(newSuccessor); + /* + * Usually this just kills the LoweredDeadEndNode that is the successor of the original + * foreign call. But there are corner cases where more dead control flow gets killed. + */ + GraphUtil.killCFG(originalForeignCall); + } +}