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 a5a048fe2f0b..b633103911ba 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 @@ -317,8 +317,21 @@ protected void onValueUpdate(EconomicMap, Object> values, Boolean o @Option(help = "The size of each internal thread stack, in bytes.", type = OptionType.Expert)// public static final HostedOptionKey InternalThreadStackSize = new HostedOptionKey<>(2L * 1024 * 1024); - @Option(help = "The maximum number of lines in the stack trace for Java exceptions (0 means all)", type = OptionType.User)// - public static final RuntimeOptionKey MaxJavaStackTraceDepth = new RuntimeOptionKey<>(1024); + /** + * Cached value of {@link ConcealedOptions#MaxJavaStackTraceDepth}. Also used as default value. + */ + private static int maxJavaStackTraceDepth = 1024; + + /** + * Cached accessor for {@link ConcealedOptions#MaxJavaStackTraceDepth}. + */ + public static int maxJavaStackTraceDepth() { + return maxJavaStackTraceDepth; + } + + public static void updateMaxJavaStackTraceDepth(EconomicMap, Object> runtimeValues, int newValue) { + ConcealedOptions.MaxJavaStackTraceDepth.update(runtimeValues, newValue); + } /* Same option name and specification as the Java HotSpot VM. */ @Option(help = "Maximum total size of NIO direct-buffer allocations")// @@ -750,6 +763,16 @@ public Boolean getValue(OptionValues values) { /** Use {@link com.oracle.svm.core.jvmstat.PerfManager#usePerfData()} instead. */ @Option(help = "Flag to disable jvmstat instrumentation for performance testing.")// public static final RuntimeOptionKey UsePerfData = new RuntimeOptionKey<>(true, Immutable); + + /** Use {@link SubstrateOptions#maxJavaStackTraceDepth()} instead. */ + @Option(help = "The maximum number of lines in the stack trace for Java exceptions (0 means all)", type = OptionType.User)// + public static final RuntimeOptionKey MaxJavaStackTraceDepth = new RuntimeOptionKey<>(maxJavaStackTraceDepth) { + @Override + protected void onValueUpdate(EconomicMap, Object> values, Integer oldValue, Integer newValue) { + super.onValueUpdate(values, oldValue, newValue); + maxJavaStackTraceDepth = newValue; + } + }; } @Option(help = "Overwrites the available number of processors provided by the OS. Any value <= 0 means using the processor count from the OS.")// diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BacktraceDecoder.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BacktraceDecoder.java new file mode 100644 index 000000000000..a81355d934d2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/BacktraceDecoder.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023, 2023, 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.core.jdk; + +import org.graalvm.nativeimage.c.function.CodePointer; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.code.CodeInfo; +import com.oracle.svm.core.code.CodeInfoAccess; +import com.oracle.svm.core.code.CodeInfoDecoder; +import com.oracle.svm.core.code.CodeInfoTable; +import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.UntetheredCodeInfo; +import com.oracle.svm.core.heap.RestrictHeapAccess; +import com.oracle.svm.core.util.VMError; + +public abstract class BacktraceDecoder { + + /** + * Visits the backtrace stored in {@code Throwable#backtrace}. + * + * @param backtrace internal backtrace stored in {@link Target_java_lang_Throwable#backtrace} + * @param maxFramesProcessed the maximum number of frames that should be + * {@linkplain #processFrameInfo processed} + * @param maxFramesDecode the maximum number of frames that should be decoded + * @return the number of decoded frames + */ + protected final int visitBacktrace(Object backtrace, int maxFramesProcessed, int maxFramesDecode) { + int framesDecoded = 0; + if (backtrace != null) { + final long[] trace = (long[]) backtrace; + for (long address : trace) { + if (address == 0) { + break; + } + CodePointer ip = WordFactory.pointer(address); + framesDecoded = visitCodePointer(ip, framesDecoded, maxFramesProcessed, maxFramesDecode); + if (framesDecoded == maxFramesDecode) { + break; + } + } + } + return framesDecoded - maxFramesProcessed; + } + + @Uninterruptible(reason = "Prevent the GC from freeing the CodeInfo object.") + private int visitCodePointer(CodePointer ip, int oldFramesDecoded, int maxFramesProcessed, int maxFramesDecode) { + int framesDecoded = oldFramesDecoded; + UntetheredCodeInfo untetheredInfo = CodeInfoTable.lookupCodeInfo(ip); + if (untetheredInfo.isNull()) { + /* Unknown frame. Must not happen for AOT-compiled code. */ + VMError.shouldNotReachHere("Stack walk must walk only frames of known code."); + } + + Object tether = CodeInfoAccess.acquireTether(untetheredInfo); + try { + CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); + framesDecoded = visitFrame(ip, tetheredCodeInfo, framesDecoded, maxFramesProcessed, maxFramesDecode); + } finally { + CodeInfoAccess.releaseTether(untetheredInfo, tether); + } + return framesDecoded; + } + + private final CodeInfoDecoder.FrameInfoCursor frameInfoCursor = new CodeInfoDecoder.FrameInfoCursor(); + + @Uninterruptible(reason = "Wraps the now safe call to the possibly interruptible visitor.", callerMustBe = true, calleeMustBe = false) + private int visitFrame(CodePointer ip, CodeInfo tetheredCodeInfo, int oldFramesDecoded, int maxFramesProcessed, int maxFramesDecode) { + int framesDecoded = oldFramesDecoded; + frameInfoCursor.initialize(tetheredCodeInfo, ip); + while (frameInfoCursor.advance()) { + FrameInfoQueryResult frameInfo = frameInfoCursor.get(); + if (!StackTraceUtils.shouldShowFrame(frameInfo, false, true, false)) { + /* Always ignore the frame. It is an internal frame of the VM. */ + continue; + } + if (framesDecoded == 0 && Throwable.class.isAssignableFrom(frameInfo.getSourceClass())) { + /* + * We are still in the constructor invocation chain at the beginning of the stack + * trace, which is also filtered by the Java HotSpot VM. + */ + continue; + } + if (framesDecoded < maxFramesProcessed) { + processFrameInfo(frameInfo); + } + framesDecoded++; + if (framesDecoded == maxFramesDecode) { + break; + } + } + return framesDecoded; + } + + @RestrictHeapAccess(access = RestrictHeapAccess.Access.UNRESTRICTED, reason = "Some implementations allocate.") + protected abstract void processFrameInfo(FrameInfoQueryResult frameInfo); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKUtils.java index c584daf3c838..de81832c0cd1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JDKUtils.java @@ -25,6 +25,8 @@ package com.oracle.svm.core.jdk; import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.deopt.DeoptimizationSupport; +import com.oracle.svm.core.util.VMError; public final class JDKUtils { @@ -37,7 +39,40 @@ public static String getRawMessage(Throwable ex) { return SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).detailMessage; } + /** + * Gets the materialized {@link StackTraceElement} array stored in a {@link Throwable} object. + * Must only be called if {@link #isStackTraceValid} returns (or would return) {@code true}. + */ public static StackTraceElement[] getRawStackTrace(Throwable ex) { - return SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).stackTrace; + VMError.guarantee(isStackTraceValid(ex)); + StackTraceElement[] stackTrace = SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).stackTrace; + if (!DeoptimizationSupport.enabled() || (stackTrace != Target_java_lang_Throwable.UNASSIGNED_STACK && stackTrace != null)) { + return stackTrace; + } + /* Runtime compilation and deoptimized frames are not yet optimized (GR-45765). */ + return (StackTraceElement[]) SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).backtrace; + } + + /** + * Returns {@code true} if the {@linkplain #getRawStackTrace stack trace} stored in a + * {@link Throwable} object is valid. If not, {@link #getBacktrace} must be used to access the + * Java stack trace frames. + */ + public static boolean isStackTraceValid(Throwable ex) { + StackTraceElement[] stackTrace = SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).stackTrace; + if (stackTrace != Target_java_lang_Throwable.UNASSIGNED_STACK && stackTrace != null) { + return true; + } + /* Runtime compilation and deoptimized frames are not yet optimized (GR-45765). */ + return DeoptimizationSupport.enabled() && SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).backtrace instanceof StackTraceElement[]; + } + + /** + * Gets the internal backtrace of a {@link Throwable} object. Must only be called if + * {@link #isStackTraceValid} returns (or would return) {@code false}. + */ + public static Object getBacktrace(Throwable ex) { + VMError.guarantee(!isStackTraceValid(ex)); + return SubstrateUtil.cast(ex, Target_java_lang_Throwable.class).backtrace; } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java index 94f55aefdc2c..a90726b10b8f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/JavaLangSubstitutions.java @@ -70,12 +70,14 @@ import com.oracle.svm.core.annotate.Substitute; import com.oracle.svm.core.annotate.TargetClass; import com.oracle.svm.core.annotate.TargetElement; +import com.oracle.svm.core.deopt.DeoptimizationSupport; import com.oracle.svm.core.hub.ClassForNameSupport; import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.jdk.JavaLangSubstitutions.ClassValueSupport; import com.oracle.svm.core.monitor.MonitorSupport; import com.oracle.svm.core.snippets.SubstrateForeignCallTarget; import com.oracle.svm.core.thread.JavaThreads; +import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.util.VMError; import com.oracle.svm.util.ReflectionUtil; @@ -279,27 +281,145 @@ final class Target_java_lang_StringUTF16 { final class Target_java_lang_Throwable { @Alias @RecomputeFieldValue(kind = Reset)// - private Object backtrace; + Object backtrace; - @Alias @RecomputeFieldValue(kind = Reset)// + @Alias @RecomputeFieldValue(kind = Kind.Custom, declClass = ThrowableStackTraceFieldValueTransformer.class)// StackTraceElement[] stackTrace; @Alias String detailMessage; + // Checkstyle: stop + @Alias// + static StackTraceElement[] UNASSIGNED_STACK; + // Checkstyle: resume + + /** + * Fills in the execution stack trace. {@link Throwable#fillInStackTrace()} cannot be + * {@code synchronized}, because it might be called in a {@link VMOperation} (via one of the + * {@link Throwable} constructors), where we are not allowed to block. To work around that, we + * do the following: + *
    + *
  • If we are not in a {@link VMOperation}, it executes {@link #fillInStackTrace(int)} in a + * block {@code synchronized} by the supplied {@link Throwable}. This is the default case. + *
  • If we are in a {@link VMOperation}, it checks if the {@link Throwable} is currently + * locked. If not, {@link #fillInStackTrace(int)} is called without synchronization, which is + * safe in a {@link VMOperation}. If it is locked, we do not do any filling (and thus do not + * collect the stack trace). + *
+ */ @Substitute @NeverInline("Starting a stack walk in the caller frame") - private Object fillInStackTrace() { - stackTrace = JavaThreads.getStackTrace(true, Thread.currentThread()); + public Target_java_lang_Throwable fillInStackTrace() { + if (VMOperation.isInProgress()) { + if (MonitorSupport.singleton().isLockedByAnyThread(this)) { + /* + * The Throwable is locked. We cannot safely fill in the stack trace. Do nothing and + * accept that we will not get a stack track. + */ + } else { + /* + * The Throwable is not locked. We can safely fill the stack trace without + * synchronization because we VMOperation is single threaded. + */ + + /* Copy of `Throwable#fillInStackTrace()` */ + if (stackTrace != null || backtrace != null) { + fillInStackTrace(0); + stackTrace = UNASSIGNED_STACK; + } + } + } else { + synchronized (this) { + /* Copy of `Throwable#fillInStackTrace()` */ + if (stackTrace != null || backtrace != null) { + fillInStackTrace(0); + stackTrace = UNASSIGNED_STACK; + } + } + } + return this; + } + + /** + * Records the execution stack in an internal format. The information is transformed into a + * {@link StackTraceElement} array in + * {@link Target_java_lang_StackTraceElement#of(Object, int)}. + * + * @param dummy to change signature + */ + @Substitute + @NeverInline("Starting a stack walk in the caller frame") + Target_java_lang_Throwable fillInStackTrace(int dummy) { + /* + * Start out by clearing the backtrace for this object, in case the VM runs out of memory + * while allocating the stack trace. + */ + backtrace = null; + + if (DeoptimizationSupport.enabled()) { + /* + * Runtime compilation and deoptimized frames are not yet optimized (GR-45765). Eagerly + * construct a stack trace and store it in backtrace. We cannot directly use + * `stackTrace` because it is overwritten by the caller. + */ + backtrace = JavaThreads.getStackTrace(true, Thread.currentThread()); + return this; + } + + BacktraceVisitor visitor = new BacktraceVisitor(); + JavaThreads.visitCurrentStackFrames(visitor); + backtrace = visitor.getArray(); return this; } +} + +final class ThrowableStackTraceFieldValueTransformer implements FieldValueTransformer { + + private static final StackTraceElement[] UNASSIGNED_STACK = ReflectionUtil.readStaticField(Throwable.class, "UNASSIGNED_STACK"); + @Override + public Object transform(Object receiver, Object originalValue) { + if (originalValue == null) { // Immutable stack + return null; + } + return UNASSIGNED_STACK; + } +} + +@TargetClass(java.lang.StackTraceElement.class) +@Platforms(InternalPlatform.NATIVE_ONLY.class) +final class Target_java_lang_StackTraceElement { + /** + * Constructs the {@link StackTraceElement} array from a backtrace. + * + * @param x backtrace stored in {@link Target_java_lang_Throwable#backtrace} + * @param depth ignored + */ @Substitute - private StackTraceElement[] getOurStackTrace() { - if (stackTrace != null) { + @TargetElement(onlyWith = JDK19OrLater.class) + static StackTraceElement[] of(Object x, int depth) { + if (x instanceof StackTraceElement[] stackTrace) { + /* Stack trace eagerly created. */ + return stackTrace; + } + return StackTraceBuilder.build(x); + } + + /** + * Constructs the {@link StackTraceElement} array from a {@link Throwable}. + * + * @param t the {@link Throwable} object + * @param depth ignored + */ + @Substitute + @TargetElement(onlyWith = JDK17OrEarlier.class) + static StackTraceElement[] of(Target_java_lang_Throwable t, int depth) { + Object x = t.backtrace; + if (x instanceof StackTraceElement[] stackTrace) { + /* Stack trace eagerly created. */ return stackTrace; - } else { - return new StackTraceElement[0]; } + return StackTraceBuilder.build(x); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/StackTraceUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/StackTraceUtils.java index 3302cc6017db..7f9086051c38 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/StackTraceUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/StackTraceUtils.java @@ -30,21 +30,30 @@ import java.security.AccessController; import java.security.ProtectionDomain; import java.util.ArrayList; +import java.util.Arrays; -import com.oracle.svm.core.hub.DynamicHub; import org.graalvm.nativeimage.AnnotationAccess; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.c.function.CodePointer; import org.graalvm.word.Pointer; +import org.graalvm.word.WordFactory; import com.oracle.svm.core.NeverInline; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.SubstrateUtil; +import com.oracle.svm.core.Uninterruptible; +import com.oracle.svm.core.code.CodeInfo; +import com.oracle.svm.core.code.CodeInfoAccess; +import com.oracle.svm.core.code.CodeInfoTable; import com.oracle.svm.core.code.FrameInfoQueryResult; +import com.oracle.svm.core.code.UntetheredCodeInfo; +import com.oracle.svm.core.deopt.DeoptimizedFrame; import com.oracle.svm.core.heap.VMOperationInfos; +import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.core.stack.JavaStackFrameVisitor; import com.oracle.svm.core.stack.JavaStackWalker; +import com.oracle.svm.core.stack.StackFrameVisitor; import com.oracle.svm.core.thread.JavaThreads; import com.oracle.svm.core.thread.JavaVMOperation; import com.oracle.svm.core.thread.LoomSupport; @@ -53,6 +62,7 @@ import com.oracle.svm.core.thread.Target_jdk_internal_vm_Continuation; import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.thread.VirtualThreads; +import com.oracle.svm.core.util.VMError; import jdk.vm.ci.meta.MetaAccessProvider; import jdk.vm.ci.meta.ResolvedJavaMethod; @@ -67,21 +77,25 @@ public class StackTraceUtils { * Captures the stack trace of the current thread. In almost any context, calling * {@link JavaThreads#getStackTrace} for {@link Thread#currentThread()} is preferable. * - * Captures at most {@link SubstrateOptions#MaxJavaStackTraceDepth} stack trace elements if max - * depth > 0, or all if max depth <= 0. + * Captures at most {@link SubstrateOptions#maxJavaStackTraceDepth()} stack trace elements if + * max depth > 0, or all if max depth <= 0. */ public static StackTraceElement[] getStackTrace(boolean filterExceptions, Pointer startSP, Pointer endSP) { - BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(filterExceptions, SubstrateOptions.MaxJavaStackTraceDepth.getValue()); - JavaStackWalker.walkCurrentThread(startSP, endSP, visitor); + BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(filterExceptions, SubstrateOptions.maxJavaStackTraceDepth()); + visitCurrentThreadStackFrames(startSP, endSP, visitor); return visitor.trace.toArray(NO_ELEMENTS); } + public static void visitCurrentThreadStackFrames(Pointer startSP, Pointer endSP, StackFrameVisitor visitor) { + JavaStackWalker.walkCurrentThread(startSP, endSP, visitor); + } + /** * Captures the stack trace of a thread (potentially the current thread) while stopped at a * safepoint. Used by {@link Thread#getStackTrace()} and {@link Thread#getAllStackTraces()}. * - * Captures at most {@link SubstrateOptions#MaxJavaStackTraceDepth} stack trace elements if max - * depth > 0, or all if max depth <= 0. + * Captures at most {@link SubstrateOptions#maxJavaStackTraceDepth()} stack trace elements if + * max depth > 0, or all if max depth <= 0. */ @NeverInline("Potentially starting a stack walk in the caller frame") public static StackTraceElement[] getStackTraceAtSafepoint(Thread thread) { @@ -94,14 +108,14 @@ public static StackTraceElement[] getStackTraceAtSafepoint(Thread thread) { public static StackTraceElement[] getThreadStackTraceAtSafepoint(IsolateThread isolateThread, Pointer endSP) { assert VMOperation.isInProgressAtSafepoint(); - BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(false, SubstrateOptions.MaxJavaStackTraceDepth.getValue()); + BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(false, SubstrateOptions.maxJavaStackTraceDepth()); JavaStackWalker.walkThread(isolateThread, endSP, visitor, null); return visitor.trace.toArray(NO_ELEMENTS); } public static StackTraceElement[] getThreadStackTraceAtSafepoint(Pointer startSP, Pointer endSP, CodePointer startIP) { assert VMOperation.isInProgressAtSafepoint(); - BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(false, SubstrateOptions.MaxJavaStackTraceDepth.getValue()); + BuildStackTraceVisitor visitor = new BuildStackTraceVisitor(false, SubstrateOptions.maxJavaStackTraceDepth()); JavaStackWalker.walkThreadAtSafepoint(startSP, endSP, startIP, visitor); return visitor.trace.toArray(NO_ELEMENTS); } @@ -236,6 +250,109 @@ protected void operate() { } } } + +} + +/** + * Visits the stack frames and collects a backtrace in an internal format to be stored in + * {@link Target_java_lang_Throwable#backtrace}. + */ +final class BacktraceVisitor extends StackFrameVisitor { + + private int index = 0; + private final int limit = SubstrateOptions.maxJavaStackTraceDepth(); + + /* + * Empirical data suggests that most stack traces tend to be relatively short (<100). We choose + * the initial size so that these cases do not need to reallocate the array. + */ + private static final int INITIAL_TRACE_SIZE = 80; + private long[] trace = new long[INITIAL_TRACE_SIZE]; + + @Uninterruptible(reason = "Prevent the GC from freeing the CodeInfo object.") + private static boolean decodeCodePointer(BuildStackTraceVisitor visitor, CodePointer ip) { + UntetheredCodeInfo untetheredInfo = CodeInfoTable.lookupCodeInfo(ip); + if (untetheredInfo.isNull()) { + /* Unknown frame. Must not happen for AOT-compiled code. */ + throw VMError.shouldNotReachHere("Stack walk must walk only frames of known code."); + } + + Object tether = CodeInfoAccess.acquireTether(untetheredInfo); + try { + CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); + if (!visitFrame(visitor, ip, tetheredCodeInfo)) { + return true; + } + } finally { + CodeInfoAccess.releaseTether(untetheredInfo, tether); + } + return false; + } + + @Uninterruptible(reason = "Wraps the now safe call to the possibly interruptible visitor.", callerMustBe = true, calleeMustBe = false) + private static boolean visitFrame(BuildStackTraceVisitor visitor, CodePointer ip, CodeInfo tetheredCodeInfo) { + return visitor.visitFrame(WordFactory.nullPointer(), ip, tetheredCodeInfo, null); + } + + @Override + protected boolean visitFrame(Pointer sp, CodePointer ip, CodeInfo codeInfo, DeoptimizedFrame deoptimizedFrame) { + if (index >= limit) { + // cutoff + return false; + } + VMError.guarantee(deoptimizedFrame == null, "Deoptimization not supported"); + long rawValue = ip.rawValue(); + VMError.guarantee(rawValue != 0, "Unexpected code pointer: 0"); + add(rawValue); + return true; + } + + private void add(long value) { + if (index == trace.length) { + trace = Arrays.copyOf(trace, Math.min(trace.length * 2, limit)); + } + trace[index++] = value; + } + + /** + * Gets the backtrace array. + * + * Tradeoff question: should we make a copy of the trace array to trim it to length index? + *
    + *
  • Benefit: lower memory footprint for exceptions that are long-lived. + *
  • Downside: more work for copying for every exception. + *
+ * Currently, we do not trim the array. The assumption is that most exception stack traces are + * short-lived and are never moved by the GC. + */ + long[] getArray() { + VMError.guarantee(trace != null, "Already acquired"); + VMError.guarantee(index == trace.length || trace[index] == 0, "Unterminated trace?"); + long[] tmp = trace; + trace = null; + return tmp; + } +} + +/** + * Decodes the internal backtrace stored in {@link Target_java_lang_Throwable#backtrace} and creates + * the corresponding {@link StackTraceElement} array. + */ +final class StackTraceBuilder extends BacktraceDecoder { + + static StackTraceElement[] build(Object backtrace) { + var stackTraceBuilder = new StackTraceBuilder(); + stackTraceBuilder.visitBacktrace(backtrace, Integer.MAX_VALUE, SubstrateOptions.maxJavaStackTraceDepth()); + return stackTraceBuilder.trace.toArray(new StackTraceElement[0]); + } + + private final ArrayList trace = new ArrayList<>(); + + @Override + protected void processFrameInfo(FrameInfoQueryResult frameInfo) { + StackTraceElement sourceReference = frameInfo.getSourceReference(); + trace.add(sourceReference); + } } class BuildStackTraceVisitor extends JavaStackFrameVisitor { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/log/RealLog.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/log/RealLog.java index b68efc1635d2..b99fce5d4e8f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/log/RealLog.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/log/RealLog.java @@ -39,12 +39,17 @@ import org.graalvm.word.WordFactory; import com.oracle.svm.core.NeverInline; +import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.c.NonmovableArrays; +import com.oracle.svm.core.code.FrameInfoQueryResult; import com.oracle.svm.core.graal.stackvalue.UnsafeStackValue; import com.oracle.svm.core.heap.Heap; import com.oracle.svm.core.heap.RestrictHeapAccess; +import com.oracle.svm.core.jdk.BacktraceDecoder; import com.oracle.svm.core.jdk.JDKUtils; +import com.oracle.svm.core.locks.VMMutex; +import com.oracle.svm.core.thread.VMOperation; import com.oracle.svm.core.util.VMError; public class RealLog extends Log { @@ -626,25 +631,78 @@ public Log exception(Throwable t, int maxFrames) { * is better than printing nothing. */ String detailMessage = JDKUtils.getRawMessage(t); - StackTraceElement[] stackTrace = JDKUtils.getRawStackTrace(t); string(t.getClass().getName()).string(": ").string(detailMessage); - if (stackTrace != null) { - int i; - for (i = 0; i < stackTrace.length && i < maxFrames; i++) { - StackTraceElement element = stackTrace[i]; - if (element != null) { - newline(); - string(" at ").string(element.getClassName()).string(".").string(element.getMethodName()); - string("(").string(element.getFileName()).string(":").signed(element.getLineNumber()).string(")"); + if (!JDKUtils.isStackTraceValid(t)) { + /* + * We accept that there might be a race with concurrent calls to + * `Throwable#fillInStackTrace`, which changes `Throwable#backtrace`. We accept that and + * the code can deal with that. Worst case we don't get a stack trace. + */ + int remaining = printBacktraceLocked(t, maxFrames); + printRemainingFramesCount(remaining); + } else { + StackTraceElement[] stackTrace = JDKUtils.getRawStackTrace(t); + if (stackTrace != null) { + int i; + for (i = 0; i < stackTrace.length && i < maxFrames; i++) { + StackTraceElement element = stackTrace[i]; + if (element != null) { + printJavaFrame(element.getClassName(), element.getMethodName(), element.getFileName(), element.getLineNumber()); + } } - } - int remaining = stackTrace.length - i; - if (remaining > 0) { - newline().string(" ... ").unsigned(remaining).string(" more"); + int remaining = stackTrace.length - i; + printRemainingFramesCount(remaining); } } newline(); return this; } + + private static final VMMutex BACKTRACE_PRINTER_MUTEX = new VMMutex("RealLog.backTracePrinterMutex"); + private final BacktracePrinter backtracePrinter = new BacktracePrinter(); + + private int printBacktraceLocked(Throwable t, int maxFrames) { + if (VMOperation.isInProgress()) { + if (BACKTRACE_PRINTER_MUTEX.hasOwner()) { + /* + * The FrameInfoCursor is locked. We cannot safely print the stack trace. Do nothing + * and accept that we will not get a stack track. + */ + return 0; + } + } + BACKTRACE_PRINTER_MUTEX.lock(); + try { + Object backtrace = JDKUtils.getBacktrace(t); + return backtracePrinter.printBacktrace(backtrace, maxFrames); + } finally { + BACKTRACE_PRINTER_MUTEX.unlock(); + } + } + + private void printJavaFrame(String className, String methodName, String fileName, int lineNumber) { + newline(); + string(" at ").string(className).string(".").string(methodName); + string("(").string(fileName).string(":").signed(lineNumber).string(")"); + } + + private void printRemainingFramesCount(int remaining) { + if (remaining > 0) { + newline().string(" ... ").unsigned(remaining).string(" more"); + } + } + + private class BacktracePrinter extends BacktraceDecoder { + + protected final int printBacktrace(Object backtrace, int maxFramesProcessed) { + return visitBacktrace(backtrace, maxFramesProcessed, SubstrateOptions.maxJavaStackTraceDepth()); + } + + @Override + @RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Must not allocate when logging.") + protected void processFrameInfo(FrameInfoQueryResult frameInfo) { + printJavaFrame(frameInfo.getSourceClassName(), frameInfo.getSourceMethodName(), frameInfo.getSourceFileName(), frameInfo.getSourceLineNumber()); + } + } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java index df5fda7bc9cc..422108a169d4 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/sampler/SamplerBuffersAccess.java @@ -246,8 +246,8 @@ private static int visitFrame(JfrNativeEventWriterData data, long address) { } Object tether = CodeInfoAccess.acquireTether(untetheredInfo); - CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); try { + CodeInfo tetheredCodeInfo = CodeInfoAccess.convert(untetheredInfo, tether); return visitFrame(data, tetheredCodeInfo, ip); } finally { CodeInfoAccess.releaseTether(untetheredInfo, tether); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/stack/JavaStackWalker.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/stack/JavaStackWalker.java index 10bd6811884c..0d5c0324705d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/stack/JavaStackWalker.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/stack/JavaStackWalker.java @@ -324,8 +324,8 @@ static boolean doWalk(JavaStackWalk walk, ParameterizedStackFrameVisitor visitor } Object tether = CodeInfoAccess.acquireTether(untetheredInfo); - CodeInfo tetheredInfo = CodeInfoAccess.convert(untetheredInfo, tether); try { + CodeInfo tetheredInfo = CodeInfoAccess.convert(untetheredInfo, tether); // now the value in walk.getIPCodeInfo() can be passed to interruptible code if (!callVisitor(walk, tetheredInfo, visitor, data)) { return false; diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java index eef075349e4d..511ccebd4696 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java @@ -49,6 +49,7 @@ import com.oracle.svm.core.jdk.JDK19OrLater; import com.oracle.svm.core.jfr.events.ThreadSleepEventJDK17; import com.oracle.svm.core.snippets.KnownIntrinsics; +import com.oracle.svm.core.stack.StackFrameVisitor; import com.oracle.svm.util.ReflectionUtil; /** @@ -215,6 +216,22 @@ public static StackTraceElement[] getStackTrace(boolean filterExceptions, Thread return PlatformThreads.getStackTrace(filterExceptions, thread, callerSP); } + @NeverInline("Starting a stack walk in the caller frame") + public static void visitCurrentStackFrames(StackFrameVisitor visitor) { + /* + * If our own thread's stack was requested, we can walk it without a VMOperation using a + * stack pointer. It is intentional that we use the caller stack pointer: the calling + * Thread.getStackTrace method itself needs to be included in the result. + */ + Pointer callerSP = KnownIntrinsics.readCallerStackPointer(); + + if (supportsVirtual()) { // NOTE: also for platform threads! + VirtualThreads.singleton().visitCurrentVirtualOrPlatformThreadStackFrames(callerSP, visitor); + } else { + PlatformThreads.visitCurrentStackFrames(callerSP, visitor); + } + } + /** If there is an uncaught exception handler, call it. */ public static void dispatchUncaughtException(Thread thread, Throwable throwable) { /* Get the uncaught exception handler for the Thread, or the default one. */ diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/LoomVirtualThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/LoomVirtualThreads.java index 69c3d0f4484c..89c1e010e0c6 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/LoomVirtualThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/LoomVirtualThreads.java @@ -41,6 +41,7 @@ import com.oracle.svm.core.SubstrateUtil; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.jdk.StackTraceUtils; +import com.oracle.svm.core.stack.StackFrameVisitor; import com.oracle.svm.core.util.VMError; /** @@ -160,6 +161,15 @@ public StackTraceElement[] getVirtualOrPlatformThreadStackTrace(boolean filterEx return asyncGetStackTrace(cast(thread)); } + @Override + public void visitCurrentVirtualOrPlatformThreadStackFrames(Pointer callerSP, StackFrameVisitor visitor) { + if (!isVirtual(Thread.currentThread())) { + visitCurrentPlatformThreadStackFrames(callerSP, visitor); + return; + } + visitCurrentVirtualThreadStackFrames(callerSP, visitor); + } + @Override public StackTraceElement[] getVirtualOrPlatformThreadStackTraceAtSafepoint(Thread thread, Pointer callerSP) { if (!isVirtual(thread)) { @@ -184,6 +194,18 @@ private static StackTraceElement[] getVirtualThreadStackTrace(boolean filterExce return StackTraceUtils.getThreadStackTraceAtSafepoint(PlatformThreads.getIsolateThread(carrier), endSP); } + private static void visitCurrentVirtualThreadStackFrames(Pointer callerSP, StackFrameVisitor visitor) { + Thread carrier = cast(Thread.currentThread()).carrierThread; + if (carrier == null) { + return; + } + Pointer endSP = getCarrierSPOrElse(carrier, WordFactory.nullPointer()); + if (endSP.isNull()) { + return; + } + StackTraceUtils.visitCurrentThreadStackFrames(callerSP, endSP, visitor); + } + private static Pointer getCarrierSPOrElse(Thread carrier, Pointer other) { Target_jdk_internal_vm_Continuation cont = JavaThreads.toTarget(carrier).cont; while (cont != null) { @@ -225,6 +247,11 @@ private static StackTraceElement[] getPlatformThreadStackTrace(boolean filterExc return StackTraceUtils.asyncGetStackTrace(thread); } + private static void visitCurrentPlatformThreadStackFrames(Pointer callerSP, StackFrameVisitor visitor) { + Pointer startSP = getCarrierSPOrElse(Thread.currentThread(), callerSP); + StackTraceUtils.visitCurrentThreadStackFrames(startSP, WordFactory.nullPointer(), visitor); + } + private static StackTraceElement[] getPlatformThreadStackTraceAtSafepoint(Thread thread, Pointer callerSP) { Pointer carrierSP = getCarrierSPOrElse(thread, WordFactory.nullPointer()); IsolateThread isolateThread = PlatformThreads.getIsolateThread(thread); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java index 3b6076c42a21..e89f6333bdd1 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/PlatformThreads.java @@ -90,6 +90,7 @@ import com.oracle.svm.core.monitor.MonitorSupport; import com.oracle.svm.core.nodes.CFunctionEpilogueNode; import com.oracle.svm.core.nodes.CFunctionPrologueNode; +import com.oracle.svm.core.stack.StackFrameVisitor; import com.oracle.svm.core.stack.StackOverflowCheck; import com.oracle.svm.core.thread.VMThreads.StatusSupport; import com.oracle.svm.core.threadlocal.FastThreadLocal; @@ -841,6 +842,11 @@ static StackTraceElement[] getStackTrace(boolean filterExceptions, Thread thread return StackTraceUtils.asyncGetStackTrace(thread); } + static void visitCurrentStackFrames(Pointer callerSP, StackFrameVisitor visitor) { + assert !isVirtual(Thread.currentThread()); + StackTraceUtils.visitCurrentThreadStackFrames(callerSP, WordFactory.nullPointer(), visitor); + } + public static StackTraceElement[] getStackTraceAtSafepoint(Thread thread, Pointer callerSP) { assert !isVirtual(thread); IsolateThread isolateThread = getIsolateThread(thread); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SubstrateVirtualThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SubstrateVirtualThreads.java index 1e98199ea31d..27b65e54b4ea 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SubstrateVirtualThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SubstrateVirtualThreads.java @@ -44,6 +44,7 @@ import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.jdk.StackTraceUtils; import com.oracle.svm.core.option.HostedOptionKey; +import com.oracle.svm.core.stack.StackFrameVisitor; import com.oracle.svm.core.util.UserError; import com.oracle.svm.core.util.VMError; @@ -247,6 +248,17 @@ public StackTraceElement[] getVirtualOrPlatformThreadStackTrace(boolean filterEx return StackTraceUtils.getStackTrace(false, callerSP, endSP); } + @Override + public void visitCurrentVirtualOrPlatformThreadStackFrames(Pointer callerSP, StackFrameVisitor visitor) { + if (!isVirtual(Thread.currentThread())) { + PlatformThreads.visitCurrentStackFrames(callerSP, visitor); + return; + } + Pointer endSP = current().getBaseSP(); + VMError.guarantee(endSP.isNonNull(), "unexpected null endSP"); + StackTraceUtils.visitCurrentThreadStackFrames(callerSP, endSP, visitor); + } + @Override public StackTraceElement[] getVirtualOrPlatformThreadStackTraceAtSafepoint(Thread thread, Pointer callerSP) { return PlatformThreads.getStackTraceAtSafepoint(thread, callerSP); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VirtualThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VirtualThreads.java index 1c70c54b5221..e57976061249 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VirtualThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VirtualThreads.java @@ -31,6 +31,8 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.word.Pointer; +import com.oracle.svm.core.stack.StackFrameVisitor; + /** Operations on virtual threads. */ public interface VirtualThreads { @Fold @@ -82,5 +84,7 @@ static boolean isSupported() { */ StackTraceElement[] getVirtualOrPlatformThreadStackTrace(boolean filterExceptions, Thread thread, Pointer callerSP); + void visitCurrentVirtualOrPlatformThreadStackFrames(Pointer callerSP, StackFrameVisitor visitor); + StackTraceElement[] getVirtualOrPlatformThreadStackTraceAtSafepoint(Thread thread, Pointer callerSP); }