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 934259f4c1c2..721da019df69 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 @@ -24,7 +24,6 @@ */ package com.oracle.svm.core; -import static com.oracle.svm.core.SubstrateOptions.DeprecatedOptions.TearDownFailureNanos; import static com.oracle.svm.core.option.RuntimeOptionKey.RuntimeOptionKeyFlag.Immutable; import static com.oracle.svm.core.option.RuntimeOptionKey.RuntimeOptionKeyFlag.RegisterForIsolateArgumentParser; import static com.oracle.svm.core.option.RuntimeOptionKey.RuntimeOptionKeyFlag.RelevantForCompilationIsolates; @@ -618,17 +617,13 @@ protected void onValueUpdate(EconomicMap, Object> values, Boolean o } }; - public static final String TEAR_DOWN_WARNING_NANOS_ERROR = "Can't set both TearDownWarningSeconds and TearDownWarningNanos at the same time. Use TearDownWarningSeconds."; - @Option(help = "The number of nanoseconds before and between which tearing down an isolate gives a warning message. 0 implies no warning.", // + @Option(help = "The number of nanoseconds that the isolate teardown can take before warnings are printed. Disabled if less or equal to 0.", // deprecated = true, deprecationMessage = "Use -XX:TearDownWarningSeconds= instead")// - public static final RuntimeOptionKey TearDownWarningNanos = new RuntimeOptionKey<>(0L, - (key) -> UserError.guarantee(!(key.hasBeenSet() && TearDownWarningSeconds.hasBeenSet()), TEAR_DOWN_WARNING_NANOS_ERROR), - RelevantForCompilationIsolates); + public static final RuntimeOptionKey TearDownWarningNanos = new RuntimeOptionKey<>(0L, RelevantForCompilationIsolates); - @Option(help = "The number of nanoseconds before tearing down an isolate gives a failure message and returns from a tear-down call. 0 implies no message.", // - deprecated = true, deprecationMessage = "This call leaks resources. Instead, terminate java threads cooperatively, or use System#exit")// + @Option(help = "The number of nanoseconds that the isolate teardown can take before a fatal error is thrown. Disabled if less or equal to 0.", // + deprecated = true, deprecationMessage = "Use -XX:TearDownFailureSeconds= instead")// public static final RuntimeOptionKey TearDownFailureNanos = new RuntimeOptionKey<>(0L, RelevantForCompilationIsolates); - } @LayerVerifiedOption(kind = Kind.Changed, severity = Severity.Error)// @@ -870,24 +865,18 @@ private static void validateZapNativeMemory(HostedOptionKey optionKey) } } - /* - * Isolate tear down options. - */ - @Option(help = "The number of seconds before and between which tearing down an isolate gives a warning message. 0 implies no warning.")// - public static final RuntimeOptionKey TearDownWarningSeconds = new RuntimeOptionKey<>(0L, RelevantForCompilationIsolates); - public static long getTearDownWarningNanos() { - if (TearDownWarningSeconds.hasBeenSet() && DeprecatedOptions.TearDownWarningNanos.hasBeenSet()) { - throw new IllegalArgumentException(DeprecatedOptions.TEAR_DOWN_WARNING_NANOS_ERROR); - } - if (DeprecatedOptions.TearDownWarningNanos.hasBeenSet()) { - return DeprecatedOptions.TearDownWarningNanos.getValue(); + if (ConcealedOptions.TearDownWarningSeconds.getValue() != 0) { + return TimeUtils.secondsToNanos(ConcealedOptions.TearDownWarningSeconds.getValue()); } - return TearDownWarningSeconds.getValue() * TimeUtils.nanosPerSecond; + return DeprecatedOptions.TearDownWarningNanos.getValue(); } public static long getTearDownFailureNanos() { - return TearDownFailureNanos.getValue(); + if (ConcealedOptions.TearDownFailureSeconds.getValue() != 0) { + return TimeUtils.secondsToNanos(ConcealedOptions.TearDownFailureSeconds.getValue()); + } + return DeprecatedOptions.TearDownFailureNanos.getValue(); } @Option(help = "Define the maximum number of stores for which the loop that zeroes out objects is unrolled.")// @@ -1264,6 +1253,15 @@ protected void onValueUpdate(EconomicMap, Object> values, Integer o @Option(help = "Avoid linker relocations for code and instead emit address computations.", type = OptionType.Expert) // @LayerVerifiedOption(severity = Severity.Error, kind = Kind.Changed, positional = false) // public static final HostedOptionKey RelativeCodePointers = new HostedOptionKey<>(false, SubstrateOptions::validateRelativeCodePointers); + + /** Use {@link SubstrateOptions#getTearDownWarningNanos()} instead. */ + @Option(help = "The number of seconds that the isolate teardown can take before warnings are printed. Disabled if less or equal to 0.")// + public static final RuntimeOptionKey TearDownWarningSeconds = new RuntimeOptionKey<>(0L, RelevantForCompilationIsolates); + + /** Use {@link SubstrateOptions#getTearDownFailureNanos()} instead. */ + @Option(help = "The number of seconds that the isolate teardown can take before a fatal error is thrown. Disabled if less or equal to 0.")// + public static final RuntimeOptionKey TearDownFailureSeconds = new RuntimeOptionKey<>(0L, RelevantForCompilationIsolates); + } @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/graal/snippets/CEntryPointSnippets.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/CEntryPointSnippets.java index 5d00a0363cf8..ed7da2af7dcb 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/CEntryPointSnippets.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/CEntryPointSnippets.java @@ -671,14 +671,17 @@ public static int tearDownIsolateSnippet() { private static int tearDownIsolate() { try { /* Execute interruptible code. */ - if (!initiateTearDownIsolateInterruptibly()) { - return CEntryPointErrors.UNSPECIFIED; - } + initiateTearDownIsolateInterruptibly(); /* After threadExit(), only uninterruptible code may be executed. */ RecurringCallbackSupport.suspendCallbackTimer("Execution of arbitrary code is prohibited during the last teardown steps."); - /* Shut down VM thread. */ + /* Wait until the reference handler thread detaches (it was already stopped earlier). */ + if (ReferenceHandler.useDedicatedThread()) { + ReferenceHandlerThread.waitUntilDetached(); + } + + /* Shut down VM operation thread. */ if (VMOperationControl.useDedicatedVMOperationThread()) { VMOperationControl.shutdownAndDetachVMOperationThread(); } @@ -717,15 +720,20 @@ private static int tearDownIsolate() { } } - @Uninterruptible(reason = "Tear-down in progress - still safe to execute interruptible Java code.", calleeMustBe = false) - private static boolean initiateTearDownIsolateInterruptibly() { + @Uninterruptible(reason = "Tear-down in progress - still safe to execute interruptible Java code.", callerMustBe = true, calleeMustBe = false) + private static void initiateTearDownIsolateInterruptibly() { RuntimeSupport.executeTearDownHooks(); - if (!PlatformThreads.tearDownOtherThreads()) { - return false; + PlatformThreads.tearDownOtherThreads(); + /* + * At this point, only the current thread, the VM operation thread, and the reference + * handler thread are still running. + */ + if (ReferenceHandler.useDedicatedThread()) { + ReferenceHandlerThread.initiateShutdown(); } VMThreads.singleton().threadExit(); - return true; + /* After threadExit(), only uninterruptible code may be executed. */ } @Snippet(allowMissingProbabilities = true) @@ -802,7 +810,7 @@ static boolean runtimeAssertionsEnabled() { @SubstrateForeignCallTarget(stubCallingConvention = false) private static int verifyIsolateThread(IsolateThread thread) { VMError.guarantee(CurrentIsolate.getCurrentThread() == thread, "Threads must match for the call below"); - if (!VMThreads.singleton().verifyIsCurrentThread(thread) || !VMThreads.singleton().verifyThreadIsAttached(thread)) { + if (!VMThreads.singleton().verifyIsCurrentThread(thread) || !VMThreads.isAttached(thread)) { throw VMError.shouldNotReachHere("A call from native code to Java code provided the wrong JNI environment or the wrong IsolateThread. " + "The JNI environment / IsolateThread is a thread-local data structure and must not be shared between threads."); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandler.java index 24790edf9de1..d643fdd6d0bf 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandler.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandler.java @@ -24,18 +24,22 @@ */ package com.oracle.svm.core.heap; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; + import java.lang.ref.Reference; import com.oracle.svm.core.IsolateArgumentParser; 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.stack.StackOverflowCheck; import com.oracle.svm.core.util.VMError; import jdk.internal.ref.CleanerFactory; public final class ReferenceHandler { + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public static boolean useDedicatedThread() { int automaticReferenceHandling = IsolateArgumentParser.getOptionIndex(SubstrateOptions.ConcealedOptions.AutomaticReferenceHandling); return ReferenceHandlerThread.isSupported() && IsolateArgumentParser.singleton().getBooleanOptionValue(automaticReferenceHandling); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandlerThread.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandlerThread.java index c9febee64a3e..58fca8fb8ef3 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandlerThread.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/ReferenceHandlerThread.java @@ -41,10 +41,12 @@ import com.oracle.svm.core.util.VMError; import jdk.graal.compiler.api.replacements.Fold; +import jdk.graal.compiler.word.Word; public final class ReferenceHandlerThread implements Runnable { private final Thread thread; private volatile IsolateThread isolateThread; + private volatile boolean stopped; @Platforms(Platform.HOSTED_ONLY.class) ReferenceHandlerThread() { @@ -54,24 +56,48 @@ public final class ReferenceHandlerThread implements Runnable { } public static void start() { - if (isSupported()) { - singleton().thread.start(); + if (!isSupported()) { + return; } + + singleton().thread.start(); + /* Wait until the isolateThread field is initialized. */ + while (singleton().isolateThread.isNull()) { + Thread.yield(); + } + } + + public static void initiateShutdown() { + if (!isSupported()) { + return; + } + + singleton().stopped = true; + Heap.getHeap().wakeUpReferencePendingListWaiters(); + } + + @Uninterruptible(reason = "Executed during teardown after VMThreads#threadExit") + public static void waitUntilDetached() { + if (!isSupported()) { + return; + } + + VMThreads.waitInNativeUntilDetached(singleton().isolateThread); + singleton().isolateThread = Word.nullPointer(); } @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) public static boolean isReferenceHandlerThread() { - if (isSupported()) { - return CurrentIsolate.getCurrentThread() == singleton().isolateThread; - } - return false; + return isReferenceHandlerThread(CurrentIsolate.getCurrentThread()); + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static boolean isReferenceHandlerThread(IsolateThread other) { + return isSupported() && other == singleton().isolateThread; } public static boolean isReferenceHandlerThread(Thread other) { - if (isSupported()) { - return other == singleton().thread; - } - return false; + return isSupported() && other == singleton().thread; } @Override @@ -80,19 +106,13 @@ public void run() { this.isolateThread = CurrentIsolate.getCurrentThread(); try { - while (true) { + while (!stopped) { ReferenceInternals.waitForPendingReferences(); ReferenceInternals.processPendingReferences(); ReferenceHandler.processCleaners(); } - } catch (InterruptedException e) { - VMError.guarantee(VMThreads.isTearingDown(), "Reference Handler should only be interrupted during tear-down"); } catch (Throwable t) { - if (t instanceof OutOfMemoryError && VMThreads.isTearingDown()) { - // Likely failed to allocate the InterruptedException, ignore either way. - } else { - throw VMError.shouldNotReachHere("Reference processing and cleaners must handle all potential exceptions", t); - } + throw VMError.shouldNotReachHere("Reference processing and cleaners must handle all potential exceptions", t); } } 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 6d41c5aa31cc..71cebeff0ca1 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 @@ -44,10 +44,8 @@ import java.util.concurrent.ForkJoinWorkerThread; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import jdk.graal.compiler.word.Word; import org.graalvm.nativeimage.CurrentIsolate; import org.graalvm.nativeimage.ImageInfo; import org.graalvm.nativeimage.ImageSingletons; @@ -89,6 +87,7 @@ import com.oracle.svm.core.heap.VMOperationInfos; import com.oracle.svm.core.jdk.StackTraceUtils; import com.oracle.svm.core.jdk.UninterruptibleUtils; +import com.oracle.svm.core.locks.VMCondition; import com.oracle.svm.core.locks.VMMutex; import com.oracle.svm.core.log.Log; import com.oracle.svm.core.memory.NativeMemory; @@ -109,6 +108,7 @@ import jdk.graal.compiler.api.replacements.Fold; import jdk.graal.compiler.core.common.SuppressFBWarnings; +import jdk.graal.compiler.word.Word; import jdk.internal.misc.Unsafe; /** @@ -544,9 +544,13 @@ public void closeOSThreadHandle(OSThreadHandle threadHandle) { FORK_JOIN_POOL_TRY_TERMINATE_METHOD = ReflectionUtil.lookupMethod(ForkJoinPool.class, "tryTerminate", boolean.class, boolean.class); } - /** Have each thread, except this one, tear itself down. */ - public static boolean tearDownOtherThreads() { - final Log trace = Log.noopLog().string("[PlatformThreads.tearDownPlatformThreads:").newline().flush(); + /** + * Interrupts all threads except for the current thread and any threads that require custom + * teardown logic (see {@link #isVMInternalThread(IsolateThread)}). Waits until the interrupted + * threads detach. + */ + public static void tearDownOtherThreads() { + Log trace = Log.noopLog().string("[PlatformThreads.tearDownPlatformThreads:").newline().flush(); /* * Set tear-down flag for new Java threads that have already been started on an OS level, @@ -557,9 +561,9 @@ public static boolean tearDownOtherThreads() { */ VMThreads.setTearingDown(); - /* Fetch all running application threads and interrupt them. */ + /* Fetch threads and interrupt them. */ ArrayList threads = new ArrayList<>(); - FetchApplicationThreadsOperation operation = new FetchApplicationThreadsOperation(threads); + FetchThreadsForTeardownOperation operation = new FetchThreadsForTeardownOperation(threads); operation.enqueue(); Set pools = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -621,53 +625,43 @@ public static boolean tearDownOtherThreads() { trace.string(" shutdown initiated: ").object(pool).newline().flush(); } - final boolean result = waitForTearDown(); - trace.string(" returns: ").bool(result).string("]").newline().flush(); - return result; + waitForTearDown(); } - /** Wait (im)patiently for the VMThreads list to drain. */ - private static boolean waitForTearDown() { - assert isApplicationThread(CurrentIsolate.getCurrentThread()) : "we count the application threads until only the current one remains"; + /** Wait (im)patiently for the thread list to drain. */ + private static void waitForTearDown() { + assert !isVMInternalThread(CurrentIsolate.getCurrentThread()) : "we count the threads until only the current one remains"; - final Log trace = Log.noopLog().string("[PlatformThreads.waitForTearDown:").newline(); - final long warningNanos = SubstrateOptions.getTearDownWarningNanos(); - final String warningMessage = "PlatformThreads.waitForTearDown is taking too long."; - final long failureNanos = SubstrateOptions.getTearDownFailureNanos(); - final String failureMessage = "PlatformThreads.waitForTearDown took too long."; - final long startNanos = System.nanoTime(); - long loopNanos = startNanos; - final AtomicBoolean printLaggards = new AtomicBoolean(false); - final Log counterLog = ((warningNanos == 0) ? trace : Log.log()); - final CheckReadyForTearDownOperation operation = new CheckReadyForTearDownOperation(counterLog, printLaggards); + CheckReadyForTearDownOperation operation = new CheckReadyForTearDownOperation(); + long warningConfiguredNanos = SubstrateOptions.getTearDownWarningNanos(); + long failureConfiguredNanos = SubstrateOptions.getTearDownFailureNanos(); + long startNanos = System.nanoTime(); + long previousReportNanos = startNanos; - for (; /* return */;) { - final long previousLoopNanos = loopNanos; + while (true) { operation.enqueue(); if (operation.isReadyForTearDown()) { - trace.string(" returns true]").newline(); - return true; + return; } - loopNanos = TimeUtils.doNotLoopTooLong(startNanos, loopNanos, warningNanos, warningMessage); - final boolean fatallyTooLong = TimeUtils.maybeFatallyTooLong(startNanos, failureNanos, failureMessage); - if (fatallyTooLong) { - trace.string("Took too long to tear down the VM.").newline(); - /* - * Debugging tip: Insert a `BreakpointNode.breakpoint()` here to stop in gdb or get - * a core file with the thread stacks. Be careful about believing the stack traces, - * though. - */ - return false; + + long sinceStartNanos = TimeUtils.nanoSecondsSince(startNanos); + if (failureConfiguredNanos > 0 && TimeUtils.nanoTimeLessThan(failureConfiguredNanos, sinceStartNanos)) { + throw VMError.shouldNotReachHere("Took too long to tear down the VM."); } - /* If I took too long, print the laggards next time around. */ - printLaggards.set(previousLoopNanos != loopNanos); + + long sinceReportNanos = TimeUtils.nanoSecondsSince(previousReportNanos); + if (warningConfiguredNanos > 0 && TimeUtils.nanoTimeLessThan(warningConfiguredNanos, sinceReportNanos)) { + operation.enablePrintLaggards(); + previousReportNanos += sinceReportNanos; + } + /* Loop impatiently waiting for threads to exit. */ Thread.yield(); } } - private static boolean isApplicationThread(IsolateThread isolateThread) { - return !VMOperationControl.isDedicatedVMOperationThread(isolateThread); + public static boolean isVMInternalThread(IsolateThread thread) { + return VMOperationControl.isDedicatedVMOperationThread(thread) || ReferenceHandlerThread.isReferenceHandlerThread(thread); } @SuppressFBWarnings(value = "NN", justification = "notifyAll is necessary for Java semantics, no shared state needs to be modified beforehand") @@ -762,7 +756,10 @@ static void incrementNonDaemonThreads() { assert numThreads > 0; } - /** A caller must call THREAD_LIST_CONDITION.broadcast() manually. */ + /** + * Callers must manually invoke {@link VMCondition#broadcast()} on + * {@link VMThreads#THREAD_LIST_CONDITION} to notify any threads waiting for changes. + */ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) private static void decrementNonDaemonThreads() { int numThreads = nonDaemonThreads.decrementAndGet(); @@ -1127,14 +1124,15 @@ protected void operate() { } /** - * Builds a list of all application threads. This must be done in a VM operation because only - * there we are allowed to allocate Java memory while holding the {@link VMThreads#THREAD_MUTEX} + * Builds a list of all threads that don't need any custom teardown logic. This must be done in + * a VM operation because only there we are allowed to allocate Java memory while holding the + * {@link VMThreads#THREAD_MUTEX}. */ - private static class FetchApplicationThreadsOperation extends JavaVMOperation { + private static class FetchThreadsForTeardownOperation extends JavaVMOperation { private final List list; - FetchApplicationThreadsOperation(List list) { - super(VMOperationInfos.get(FetchApplicationThreadsOperation.class, "Fetch application threads", SystemEffect.NONE)); + FetchThreadsForTeardownOperation(List list) { + super(VMOperationInfos.get(FetchThreadsForTeardownOperation.class, "Fetch threads for teardown", SystemEffect.NONE)); this.list = list; } @@ -1144,11 +1142,13 @@ public void operate() { VMMutex lock = VMThreads.THREAD_MUTEX.lock(); try { for (IsolateThread isolateThread = VMThreads.firstThread(); isolateThread.isNonNull(); isolateThread = VMThreads.nextThread(isolateThread)) { - if (isApplicationThread(isolateThread)) { - final Thread thread = PlatformThreads.fromVMThread(isolateThread); - if (thread != null) { - list.add(thread); - } + if (isVMInternalThread(isolateThread)) { + continue; + } + + Thread thread = PlatformThreads.fromVMThread(isolateThread); + if (thread != null) { + list.add(thread); } } } finally { @@ -1164,14 +1164,15 @@ public void operate() { * holding the {@link VMThreads#THREAD_MUTEX}. */ private static class CheckReadyForTearDownOperation extends JavaVMOperation { - private final Log trace; - private final AtomicBoolean printLaggards; + private boolean printLaggards; private boolean readyForTearDown; - CheckReadyForTearDownOperation(Log trace, AtomicBoolean printLaggards) { + CheckReadyForTearDownOperation() { super(VMOperationInfos.get(CheckReadyForTearDownOperation.class, "Check ready for teardown", SystemEffect.NONE)); - this.trace = trace; - this.printLaggards = printLaggards; + } + + void enablePrintLaggards() { + printLaggards = true; } boolean isReadyForTearDown() { @@ -1180,45 +1181,82 @@ boolean isReadyForTearDown() { @Override public void operate() { - int attachedCount = 0; - int unattachedStartedCount; VMMutex lock = VMThreads.THREAD_MUTEX.lock(); try { - for (IsolateThread isolateThread = VMThreads.firstThread(); isolateThread.isNonNull(); isolateThread = VMThreads.nextThread(isolateThread)) { - if (isApplicationThread(isolateThread)) { - attachedCount++; - if (printLaggards.get() && trace.isEnabled() && isolateThread != queuingThread) { - trace.string(" laggard isolateThread: ").hex(isolateThread); - final Thread thread = PlatformThreads.fromVMThread(isolateThread); - if (thread != null) { - final String name = thread.getName(); - final Thread.State status = thread.getState(); - final boolean interruptedStatus = JavaThreads.isInterrupted(thread); - trace.string(" thread.getName(): ").string(name) - .string(" interruptedStatus: ").bool(interruptedStatus) - .string(" getState(): ").string(status.name()).newline(); - for (StackTraceElement e : thread.getStackTrace()) { - trace.string(e.toString()).newline(); - } - } - trace.newline().flush(); - } + readyForTearDown = isReadyForTeardown(); + } finally { + lock.unlock(); + } + } + + private boolean isReadyForTeardown() { + int attachedCount = 0; + boolean printed = false; + + for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) { + if (isVMInternalThread(thread)) { + continue; + } + + attachedCount++; + + /* Print some information about slow threads. */ + if (printLaggards && thread != queuingThread) { + if (!printed) { + printed = true; + Log.log().string("Teardown is taking too long").redent(true); } + + printThreadInfo(Log.log(), thread); } + } - /* - * Note: our counter for unattached started threads is not guarded by the threads - * mutex and its count could change or have changed within this block. Still, it is - * important that we hold the threads mutex when querying the counter value: a - * thread might start another thread and exit immediately after. By holding the - * threads lock, we prevent the exiting thread from detaching, and/or the starting - * thread from attaching, so we will never consider being ready for tear-down. - */ - unattachedStartedCount = singleton().unattachedStartedThreads.get(); - } finally { - lock.unlock(); + if (printed) { + Log.log().indent(false); + } + + /* + * Note: our counter for unattached started threads is not guarded by the threads mutex + * and its count could change or have changed within this block. Still, it is important + * that we hold the threads mutex when querying the counter value: a thread might start + * another thread and exit immediately after. By holding the threads lock, we prevent + * the exiting thread from detaching, and/or the starting thread from attaching, so we + * will never consider being ready for tear-down. + */ + int unattachedStartedCount = singleton().unattachedStartedThreads.get(); + + printLaggards = false; + return (attachedCount == 1 && unattachedStartedCount == 0); + } + + private static void printThreadInfo(Log log, IsolateThread thread) { + log.newline().zhex(thread).spaces(1).string(StatusSupport.getStatusString(thread)); + + int safepointBehavior = VMThreads.SafepointBehavior.getSafepointBehaviorVolatile(thread); + log.string(" (").string(VMThreads.SafepointBehavior.toString(safepointBehavior)).string(")"); + + Thread threadObj = PlatformThreads.fromVMThread(thread); + if (threadObj == null) { + log.string(" null"); + } else { + log.string(" \"").string(threadObj.getName()).string("\" - ").zhex(Word.objectToUntrackedPointer(threadObj)); + + Thread.State status = threadObj.getState(); + log.string(" (").string(status.name()).string(")"); + + if (threadObj.isDaemon()) { + log.string(", daemon"); + } + if (JavaThreads.isInterrupted(threadObj)) { + log.string(", interrupted"); + } + } + + log.indent(true); + for (StackTraceElement e : threadObj.getStackTrace()) { + log.string(e.toString()).newline(); } - readyForTearDown = (attachedCount == 1 && unattachedStartedCount == 0); + log.redent(false); } } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMOperationControl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMOperationControl.java index 05a18c0bcdd3..8f79801625d7 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMOperationControl.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMOperationControl.java @@ -48,8 +48,6 @@ import com.oracle.svm.core.locks.VMCondition; import com.oracle.svm.core.locks.VMMutex; import com.oracle.svm.core.log.Log; -import com.oracle.svm.core.nodes.CFunctionEpilogueNode; -import com.oracle.svm.core.nodes.CFunctionPrologueNode; import com.oracle.svm.core.stack.StackOverflowCheck; import com.oracle.svm.core.thread.VMThreads.SafepointBehavior; import com.oracle.svm.core.thread.VMThreads.StatusSupport; @@ -72,7 +70,7 @@ * is enabled, a dedicated VM operation thread is spawned during isolate startup and used for the * execution of all VM operations. * - * + *

* It is possible that the execution of a VM operation triggers another VM operation explicitly or * implicitly (e.g. a GC). Such recursive VM operations are executed immediately (see * {@link #immediateQueues}). @@ -144,12 +142,13 @@ public static void shutdownAndDetachVMOperationThread() { data.setNativeVMOperation(operation); /* * Note we don't call enqueueFromNonJavaThread b/c this thread still has characteristics of - * a Java thread even though VMTheads#threadExit has already been called. + * a Java thread even though VMThreads#threadExit has already been called. */ get().mainQueues.enqueueUninterruptibly(operation, data); - waitUntilVMOperationThreadDetached(); + VMThreads.waitInNativeUntilDetached(get().dedicatedVMOperationThread.getIsolateThread()); assert get().mainQueues.isEmpty(); + assert VMThreads.firstThread().isNull() : "the VM operation thread must detach last"; } private final NativeStopVMOperationThread stopVMOperationThreadOperation = new NativeStopVMOperationThread(); @@ -166,32 +165,6 @@ protected void operate(NativeVMOperationData data) { } } - @NeverInline("Must not be inlined in a caller that has an exception handler: We only support InvokeNode and not InvokeWithExceptionNode between a CFunctionPrologueNode and CFunctionEpilogueNode.") - @Uninterruptible(reason = "Executed during teardown after VMThreads#threadExit") - private static void waitUntilVMOperationThreadDetached() { - CFunctionPrologueNode.cFunctionPrologue(StatusSupport.STATUS_IN_NATIVE); - waitUntilVMOperationThreadDetachedInNative(); - CFunctionEpilogueNode.cFunctionEpilogue(StatusSupport.STATUS_IN_NATIVE); - } - - /** - * Wait until the VM operation thread reached a point where it detached from SVM and is - * therefore no longer executing Java code. - */ - @Uninterruptible(reason = "Must not stop while in native.") - @NeverInline("Provide a return address for the Java frame anchor.") - private static void waitUntilVMOperationThreadDetachedInNative() { - // this method may only access data in the image heap - VMThreads.THREAD_MUTEX.lockNoTransition(); - try { - while (VMThreads.nextThread(VMThreads.firstThread()).isNonNull()) { - VMThreads.THREAD_LIST_CONDITION.blockNoTransition(); - } - } finally { - VMThreads.THREAD_MUTEX.unlock(); - } - } - @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static boolean isDedicatedVMOperationThread() { return isDedicatedVMOperationThread(CurrentIsolate.getCurrentThread()); @@ -199,10 +172,10 @@ public static boolean isDedicatedVMOperationThread() { @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) public static boolean isDedicatedVMOperationThread(IsolateThread thread) { - if (useDedicatedVMOperationThread()) { - return thread == get().dedicatedVMOperationThread.getIsolateThread(); + if (!useDedicatedVMOperationThread()) { + return false; } - return false; + return thread == get().dedicatedVMOperationThread.getIsolateThread(); } @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java index e742fd4429b9..78353dfe00ba 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/VMThreads.java @@ -24,6 +24,7 @@ */ package com.oracle.svm.core.thread; +import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; import static com.oracle.svm.core.graal.nodes.WriteCurrentVMThreadNode.writeCurrentVMThread; import java.util.EnumSet; @@ -272,6 +273,8 @@ public IsolateThread allocateIsolateThread(int isolateThreadSize) { IsolateThread isolateThread = (IsolateThread) UnsignedUtils.roundUp(memory, alignment); unalignedIsolateThreadMemoryTL.set(isolateThread, memory); + /* Set to the sentinel value denoting the thread is detached. */ + nextTL.set(isolateThread, isolateThread); return isolateThread; } @@ -518,6 +521,59 @@ public void threadExit() { PlatformThreads.afterThreadExit(CurrentIsolate.getCurrentThread()); } + /** + * Waits in native code until the given thread is detached and therefore no longer executing any + * Java code. This method may only be used while a teardown is in progress. Otherwise, races + * like the following can happen: + *

    + *
  • thread A detaches
  • + *
  • thread B attaches and reuses the native memory of {@link IsolateThread} A for its own + * {@link IsolateThread} data structure
  • + *
  • thread C waits until thread A detaches, sees {@link IsolateThread} B in the thread list, + * and assumes that it is thread A
  • + *
+ */ + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + public static void waitInNativeUntilDetached(IsolateThread thread) { + assert thread.isNonNull(); + assert thread != CurrentIsolate.getCurrentThread(); + assert isTearingDown(); + waitInNativeUntilDetached0(thread); + } + + @Uninterruptible(reason = "Must not stop while in native.") + @NeverInline("Must not be inlined in a caller that has an exception handler: We only support InvokeNode and not InvokeWithExceptionNode between a CFunctionPrologueNode and CFunctionEpilogueNode.") + private static void waitInNativeUntilDetached0(IsolateThread thread) { + CFunctionPrologueNode.cFunctionPrologue(StatusSupport.STATUS_IN_NATIVE); + waitInNativeUntilDetached1(thread); + CFunctionEpilogueNode.cFunctionEpilogue(StatusSupport.STATUS_IN_NATIVE); + } + + @Uninterruptible(reason = "Must not stop while in native.") + @NeverInline("Provide a return address for the Java frame anchor.") + private static void waitInNativeUntilDetached1(IsolateThread detachingThread) { + // this method may only access native memory or data in the image heap + VMThreads.THREAD_MUTEX.lockNoTransition(); + try { + while (contains(detachingThread)) { + VMThreads.THREAD_LIST_CONDITION.blockNoTransition(); + } + } finally { + VMThreads.THREAD_MUTEX.unlock(); + } + } + + @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true) + private static boolean contains(IsolateThread thread) { + assert THREAD_MUTEX.isOwner(); + for (IsolateThread t = VMThreads.firstThread(); t.isNonNull(); t = VMThreads.nextThread(t)) { + if (t == thread) { + return true; + } + } + return false; + } + @Uninterruptible(reason = "Only uninterruptible code may be executed after VMThreads#threadExit.") public void waitUntilDetachedThreadsExitedOnOSLevel() { cleanupExitedOsThreads(); @@ -585,8 +641,13 @@ public boolean supportsNativeYieldAndSleep() { return false; } + /** + * Be careful with this method. Usually, the {@link IsolateThread} will be freed once the thread + * detaches (so, its memory can contain garbage or might not be accessible at all). + */ @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true) - public boolean verifyThreadIsAttached(IsolateThread thread) { + public static boolean isAttached(IsolateThread thread) { + /* For a detached thread, next points to itself. */ return nextThread(thread) != thread; } diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/TimeUtils.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/TimeUtils.java index 6d940b9d0847..06d88620502e 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/TimeUtils.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/util/TimeUtils.java @@ -26,12 +26,12 @@ import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE; -import jdk.graal.compiler.word.Word; import org.graalvm.word.UnsignedWord; import com.oracle.svm.core.Uninterruptible; import com.oracle.svm.core.jdk.UninterruptibleUtils; -import com.oracle.svm.core.log.Log; + +import jdk.graal.compiler.word.Word; public class TimeUtils { @@ -185,38 +185,6 @@ public static long multiplyOrMaxValue(long x, long y) { return r; } - /** Have I looped for too long? If so, complain, but reset the wait. */ - public static long doNotLoopTooLong(long startNanos, long loopNanos, long warningNanos, String message) { - long result = loopNanos; - final long waitedNanos = TimeUtils.nanoSecondsSince(loopNanos); - if ((0 < warningNanos) && TimeUtils.nanoTimeLessThan(warningNanos, waitedNanos)) { - Log.log().string("[TimeUtils.doNotLoopTooLong:") - .string(" startNanos: ").signed(startNanos) - .string(" warningNanos: ").signed(warningNanos).string(" < ").string(" waitedNanos: ").signed(waitedNanos) - .string(" reason: ").string(message) - .string("]").newline(); - result = System.nanoTime(); - } - return result; - } - - /** Have I taken too long? Returns true if I have, false otherwise. */ - public static boolean maybeFatallyTooLong(long startNanos, long failureNanos, String reason) { - if (0 < failureNanos) { - /* If a promptness limit was set. */ - final long nanosSinceStart = TimeUtils.nanoSecondsSince(startNanos); - if (TimeUtils.nanoTimeLessThan(failureNanos, nanosSinceStart)) { - /* If the promptness limit was exceeded. */ - Log.log().string("[TimeUtils.maybeFatallyTooLong:") - .string(" startNanos: ").signed(startNanos) - .string(" failureNanos: ").signed(failureNanos).string(" < nanosSinceStart: ").signed(nanosSinceStart) - .string(" reason: ").string(reason).string("]").newline(); - return true; - } - } - return false; - } - /** * For measuring elapsed time, {@link System#nanoTime()} should be used because * {@link System#currentTimeMillis()} is affected by adjustment of the system time.