Skip to content

Commit

Permalink
[GR-51330] Support per-thread suspend/resume.
Browse files Browse the repository at this point in the history
PullRequest: graal/16838
  • Loading branch information
christianhaeubl committed Feb 10, 2024
2 parents f602e10 + 1ee6cf3 commit 680090f
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 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
Expand Down Expand Up @@ -49,6 +49,7 @@
import com.oracle.svm.core.jfr.JfrTicks;
import com.oracle.svm.core.jfr.events.SafepointBeginEvent;
import com.oracle.svm.core.jfr.events.SafepointEndEvent;
import com.oracle.svm.core.locks.VMCondition;
import com.oracle.svm.core.log.Log;
import com.oracle.svm.core.nodes.CFunctionEpilogueNode;
import com.oracle.svm.core.nodes.CFunctionPrologueNode;
Expand Down Expand Up @@ -173,11 +174,11 @@ public static class Options {
}

private static long getSafepointPromptnessWarningNanos() {
return Options.SafepointPromptnessWarningNanos.getValue().longValue();
return Options.SafepointPromptnessWarningNanos.getValue();
}

private static long getSafepointPromptnessFailureNanos() {
return Options.SafepointPromptnessFailureNanos.getValue().longValue();
return Options.SafepointPromptnessFailureNanos.getValue();
}

@Uninterruptible(reason = "Must not contain safepoint checks.")
Expand Down Expand Up @@ -219,7 +220,7 @@ private static void slowPathSafepointCheck0(int newStatus, boolean callerHasJava
assert !ThreadingSupportImpl.isRecurringCallbackRegistered(myself) || ThreadingSupportImpl.isRecurringCallbackPaused();
} else {
do {
if (Master.singleton().getRequestingThread().isNonNull()) {
if (Master.singleton().getRequestingThread().isNonNull() || suspendedTL.getVolatile() > 0) {
Statistics.incFrozen();
freezeAtSafepoint(newStatus, callerHasJavaFrameAnchor);
SafepointListenerSupport.singleton().afterFreezeAtSafepoint();
Expand Down Expand Up @@ -343,6 +344,9 @@ private static void freezeAtSafepoint(int newStatus, boolean callerHasJavaFrameA
@Uninterruptible(reason = "Must not contain safepoint checks.")
private static void notInlinedLockNoTransition() {
VMThreads.THREAD_MUTEX.lockNoTransition();
while (suspendedTL.get() > 0) {
COND_SUSPEND.blockNoTransition();
}
}

/**
Expand Down Expand Up @@ -404,6 +408,16 @@ public static int getThreadLocalSafepointRequestedOffset() {
return VMThreadLocalInfos.getOffset(safepointRequested);
}

/**
* The possible value is {@code 0} (not suspended), or positive (suspended, possibly blocked on
* {@link #COND_SUSPEND}). This counter may only be modified while holding the
* {@link VMThreads#THREAD_MUTEX}.
*/
static final FastThreadLocalInt suspendedTL = FastThreadLocalFactory.createInt("Safepoint.suspended");

/** Condition on which to block when suspended. */
static final VMCondition COND_SUSPEND = new VMCondition(VMThreads.THREAD_MUTEX);

/** Foreign call: {@link #ENTER_SLOW_PATH_SAFEPOINT_CHECK}. */
@SubstrateForeignCallTarget(stubCallingConvention = true)
@Uninterruptible(reason = "Must not contain safepoint checks")
Expand Down Expand Up @@ -581,7 +595,7 @@ public static Master singleton() {
* safepoint and Java allocations are disabled as well.
*/
@RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "The safepoint logic must not allocate.")
protected boolean freeze(String reason) {
boolean freeze(String reason) {
assert VMOperationControl.mayExecuteVmOperations();
long startTicks = JfrTicks.elapsedTicks();

Expand All @@ -607,11 +621,11 @@ protected boolean freeze(String reason) {

/** Let all threads proceed from their safepoint. */
@RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "The safepoint logic must not allocate.")
protected void thaw(String reason, boolean unlock) {
void thaw(boolean unlock) {
assert VMOperationControl.mayExecuteVmOperations();
long startTicks = JfrTicks.elapsedTicks();
safepointState = NOT_AT_SAFEPOINT;
releaseSafepoints(reason);
releaseSafepoints();
SafepointEndEvent.emit(getSafepointId(), startTicks);
ImageSingletons.lookup(Heap.class).endSafepoint();
Statistics.setThawedNanos();
Expand Down Expand Up @@ -819,18 +833,18 @@ private static void waitForSafepoints(String reason) {
}

/** Release each thread at a safepoint. */
private static void releaseSafepoints(String reason) {
final Log trace = Log.noopLog().string("[Safepoint.Master.releaseSafepoints:").string(" reason: ").string(reason).newline();
private static void releaseSafepoints() {
assert VMThreads.THREAD_MUTEX.isOwner() : "Must hold mutex when releasing safepoints.";
// Set all the thread statuses that are at safepoint back to being in native code.
for (IsolateThread vmThread = VMThreads.firstThread(); vmThread.isNonNull(); vmThread = VMThreads.nextThread(vmThread)) {
if (!isMyself(vmThread) && !SafepointBehavior.ignoresSafepoints(vmThread)) {
if (trace.isEnabled()) {
trace.string(" vmThread status: ").string(StatusSupport.getStatusString(vmThread));
}

restoreSafepointRequestedValue(vmThread);

/* Skip suspended threads so that they remain in STATUS_IN_SAFEPOINT. */
if (suspendedTL.get(vmThread) > 0) {
continue;
}

/*
* Release the thread back to native code. Most threads will transition from
* safepoint to native; but some threads will already be in native code if they
Expand All @@ -839,21 +853,17 @@ private static void releaseSafepoints(String reason) {
*/
StatusSupport.setStatusNative(vmThread);
Statistics.incReleased();
if (trace.isEnabled()) {
trace.string(" -> ").string(StatusSupport.getStatusString(vmThread)).newline();
}
}
}
trace.string("]").newline();
}

@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
protected IsolateThread getRequestingThread() {
IsolateThread getRequestingThread() {
return requestingThread;
}

@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
protected boolean isFrozen() {
boolean isFrozen() {
return safepointState == AT_SAFEPOINT;
}

Expand Down Expand Up @@ -1040,7 +1050,7 @@ public static void incSlowPathThawed() {
}
}

public static Log toLog(Log log, boolean newLine, String prefix) {
public static void toLog(Log log, boolean newLine, String prefix) {
if (log.isEnabled() && Options.GatherSafepointStatistics.getValue()) {
if (newLine) {
log.newline();
Expand All @@ -1057,7 +1067,6 @@ public static Log toLog(Log log, boolean newLine, String prefix) {
log.string(" slowPathFrozen: ").signed(getSlowPathFrozen()).newline();
log.string(" slowPathThawed: ").signed(getSlowPathThawed()).string("]").newline();
}
return log;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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.core.thread;

import org.graalvm.nativeimage.IsolateThread;

import com.oracle.svm.core.heap.VMOperationInfos;
import com.oracle.svm.core.thread.VMThreads.StatusSupport;
import com.oracle.svm.core.util.VMError;

/**
* Support for suspending and resuming threads.
* <p>
* Once a thread is suspended, we guarantee that it won't execute any Java code until it is resumed
* explicitly. However, suspended threads may still execute native code (including uninterruptible
* AOT-compiled Native Image code). Such threads will get blocked when they try to change their
* thread status to {@link StatusSupport#STATUS_IN_JAVA} (forced safepoint slow-path).
* <p>
* After a safepoint, suspended threads remain in {@link StatusSupport#STATUS_IN_SAFEPOINT}, even
* though there is no global safepoint at the moment. The suspends and resumes are counted. A thread
* needs to be resumed the same number of times it was suspended to continue again.
* <p>
* Note that there are races possible, for example:
* <ul>
* <li>Thread A starts detaching.</li>
* <li>Thread B suspends thread A.</li>
* <li>Thread A exits without ever being suspended because it is already in uninterruptible code and
* does not enter the safepoint slowpath anymore.</li>
* </ul>
*
* or
*
* <ul>
* <li>Thread A wants to suspend thread B and requests a VM operation.</li>
* <li>Thread B exits before the VM operation starts.</li>
* </ul>
*/
public final class ThreadSuspendSupport {
private ThreadSuspendSupport() {
}

/**
* Suspends a thread. If the thread was already suspended, only the suspension counter will be
* incremented.
*/
public static void suspend(Thread thread) {
VMError.guarantee(!thread.isVirtual(), "must not be called for virtual threads");
ThreadSuspendOperation op = new ThreadSuspendOperation(thread);
op.enqueue();
}

/**
* Decrements the suspension counter of the thread and resumes the thread if the counter reaches
* 0.
*/
public static void resume(Thread thread) {
VMError.guarantee(!thread.isVirtual(), "must not be called for virtual threads");
ThreadResumeOperation op = new ThreadResumeOperation(thread);
op.enqueue();
}

private static void suspend(IsolateThread isolateThread) {
VMThreads.guaranteeOwnsThreadMutex("Must own the THREAD_MUTEX to prevent races.");

int newValue = Safepoint.suspendedTL.get(isolateThread) + 1;
VMError.guarantee(newValue > 0, "Too many thread suspends.");
Safepoint.suspendedTL.set(isolateThread, newValue);
}

private static void resume(IsolateThread isolateThread) {
VMThreads.guaranteeOwnsThreadMutex("Must own the THREAD_MUTEX to prevent races.");

int newValue = Safepoint.suspendedTL.get(isolateThread) - 1;
VMError.guarantee(newValue >= 0, "Only a suspended thread can be resumed.");
Safepoint.suspendedTL.set(isolateThread, newValue);

if (newValue == 0) {
Safepoint.COND_SUSPEND.broadcast();
}
}

private static class ThreadSuspendOperation extends JavaVMOperation {
private final Thread thread;

ThreadSuspendOperation(Thread thread) {
super(VMOperationInfos.get(ThreadSuspendOperation.class, "Thread Suspend", SystemEffect.SAFEPOINT));
this.thread = thread;
}

@Override
protected void operate() {
IsolateThread isolateThread = PlatformThreads.getIsolateThread(thread);
if (isolateThread.isNonNull()) {
ThreadSuspendSupport.suspend(isolateThread);
}
}
}

private static class ThreadResumeOperation extends JavaVMOperation {
private final Thread thread;

ThreadResumeOperation(Thread thread) {
super(VMOperationInfos.get(ThreadResumeOperation.class, "Thread Resume", SystemEffect.SAFEPOINT));
this.thread = thread;
}

@Override
protected void operate() {
IsolateThread isolateThread = PlatformThreads.getIsolateThread(thread);
if (isolateThread.isNonNull()) {
ThreadSuspendSupport.resume(isolateThread);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ private void executeAllQueuedVMOperations() {
drain(javaSafepointOperations);
} finally {
if (startedSafepoint) {
master.thaw(safepointReason, lockedForSafepoint);
master.thaw(lockedForSafepoint);
}
}
}
Expand Down

0 comments on commit 680090f

Please sign in to comment.