diff --git a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/Threading.java b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/Threading.java
index 8e029e8faaea..114263d94dc2 100644
--- a/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/Threading.java
+++ b/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/Threading.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
@@ -55,13 +55,22 @@ private Threading() {
}
/**
+ * This method is intended for expert users.
+ *
* Registers a {@link RecurringCallback callback handler} that is called by the current thread
- * approximately at the provided interval. Only one callback can be active per thread. Each
- * thread can have its own callback with a different interval (or none at all). No guarantees
- * are made about the actual interval. For example, when the thread is waiting for a lock or
- * executing native code, no callback can be done. Exceptions that are thrown during the
- * execution of the callback are caught and ignored, unless they are thrown via a call to
- * {@link RecurringCallbackAccess#throwException(Throwable)}.
+ * approximately at the provided interval. This functionality is only supported if the native
+ * binary is built with {@code -H:+SupportRecurringCallback}. Note that only carefully crafted,
+ * uninterruptible code can execute safely in a recurring callback. Executing any other code
+ * easily results in deadlocks, crashes, and difficult-to-debug anomalies.
+ *
+ * Only one callback can be active per thread. Each thread can have its own callback with a
+ * different interval (or none at all). No guarantees are made about the actual interval. For
+ * example, when the thread is waiting for a lock or executing native code, no callback can be
+ * done.
+ *
+ * Exceptions that are thrown during the execution of the callback and that are not caught in
+ * the callback are ignored. {@link RecurringCallbackAccess#throwException} can be used to
+ * explicitly throw an exception that is not ignored.
*
* Specifying {@code null} for {@code callback} clears the current thread's callback (in which
* case, the values of {@code interval} and {@code unit} are ignored).
diff --git a/substratevm/CHANGELOG.md b/substratevm/CHANGELOG.md
index 177a5dcbccf0..de826434cb8c 100644
--- a/substratevm/CHANGELOG.md
+++ b/substratevm/CHANGELOG.md
@@ -14,6 +14,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-58659) (GR-58660) Support for FFM API ("Panama") has been added for darwin-aarch64 and linux-aarch64.
* (GR-49525) Introduced `--future-defaults=[all||none]` that enables options that are planned to become defaults in future releases. The enabled options are:
1. `run-time-initialized-jdk` shifts away from build-time initialization of the JDK, instead initializing most of it at run time. This transition is gradual, with individual components of the JDK becoming run-time initialized in each release. This process should complete with JDK 29 when this option should not be needed anymore. Unless you store classes from the JDK in the image heap, this option should not affect you. In case this option breaks your build, follow the suggestions in the error messages.
+* (GR-63494) Recurring callback support is no longer enabled by default. If this feature is needed, please specify `-H:+SupportRecurringCallback` at image build-time.
## GraalVM for JDK 24 (Internal Version 24.2.0)
* (GR-59717) Added `DuringSetupAccess.registerObjectReachabilityHandler` to allow registering a callback that is executed when an object of a specified type is marked as reachable during heap scanning.
diff --git a/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64SafepointCheckOp.java b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64SafepointCheckOp.java
index 5ddbaffcd269..65cc01a16ddd 100644
--- a/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64SafepointCheckOp.java
+++ b/substratevm/src/com.oracle.svm.core.graal.aarch64/src/com/oracle/svm/core/graal/aarch64/AArch64SafepointCheckOp.java
@@ -26,10 +26,10 @@
import com.oracle.svm.core.ReservedRegisters;
import com.oracle.svm.core.nodes.SafepointCheckNode;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.Safepoint;
import com.oracle.svm.core.thread.SafepointCheckCounter;
import com.oracle.svm.core.thread.SafepointSlowpath;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
import jdk.graal.compiler.asm.aarch64.AArch64Address;
import jdk.graal.compiler.asm.aarch64.AArch64Assembler;
@@ -60,7 +60,7 @@ public void emitCode(CompilationResultBuilder crb, AArch64MacroAssembler masm) {
try (ScratchRegister scratchRegister = masm.getScratchRegister()) {
Register scratch = scratchRegister.getRegister();
masm.ldr(safepointSize, scratch, safepointAddress);
- if (ThreadingSupportImpl.isRecurringCallbackSupported()) {
+ if (RecurringCallbackSupport.isEnabled()) {
/* Before subtraction, the counter is compared against 1. */
masm.subs(safepointSize, scratch, scratch, 1);
masm.str(safepointSize, scratch, safepointAddress);
diff --git a/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64SafepointCheckOp.java b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64SafepointCheckOp.java
index 3fc7c946d944..34b2d3a27b60 100644
--- a/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64SafepointCheckOp.java
+++ b/substratevm/src/com.oracle.svm.core.graal.amd64/src/com/oracle/svm/core/graal/amd64/AMD64SafepointCheckOp.java
@@ -26,8 +26,8 @@
import com.oracle.svm.core.ReservedRegisters;
import com.oracle.svm.core.nodes.SafepointCheckNode;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.SafepointCheckCounter;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
import jdk.graal.compiler.asm.amd64.AMD64Address;
import jdk.graal.compiler.asm.amd64.AMD64Assembler;
@@ -53,7 +53,7 @@ public AMD64SafepointCheckOp() {
public void emitCode(CompilationResultBuilder crb, AMD64MacroAssembler masm) {
int counterOffset = SafepointCheckCounter.getThreadLocalOffset();
AMD64Address counter = new AMD64Address(ReservedRegisters.singleton().getThreadRegister(), counterOffset);
- if (ThreadingSupportImpl.isRecurringCallbackSupported()) {
+ if (RecurringCallbackSupport.isEnabled()) {
masm.subl(counter, 1);
} else {
masm.cmpl(counter, 0);
diff --git a/substratevm/src/com.oracle.svm.core.graal.llvm/src/com/oracle/svm/core/graal/llvm/NodeLLVMBuilder.java b/substratevm/src/com.oracle.svm.core.graal.llvm/src/com/oracle/svm/core/graal/llvm/NodeLLVMBuilder.java
index 1186f016f10e..42e50b9d2bc3 100644
--- a/substratevm/src/com.oracle.svm.core.graal.llvm/src/com/oracle/svm/core/graal/llvm/NodeLLVMBuilder.java
+++ b/substratevm/src/com.oracle.svm.core.graal.llvm/src/com/oracle/svm/core/graal/llvm/NodeLLVMBuilder.java
@@ -63,8 +63,8 @@
import com.oracle.svm.core.meta.SharedField;
import com.oracle.svm.core.meta.SubstrateObjectConstant;
import com.oracle.svm.core.nodes.SafepointCheckNode;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.SafepointCheckCounter;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.util.VMError;
import com.oracle.svm.shadowed.org.bytedeco.llvm.LLVM.LLVMBasicBlockRef;
@@ -360,7 +360,7 @@ private LLVMValueRef emitCondition(LogicNode condition) {
threadData = builder.buildIntToPtr(threadData, builder.rawPointerType());
LLVMValueRef safepointCounterAddr = builder.buildGEP(threadData, builder.constantInt(SafepointCheckCounter.getThreadLocalOffset()));
LLVMValueRef safepointCount = builder.buildLoad(safepointCounterAddr, builder.intType());
- if (ThreadingSupportImpl.isRecurringCallbackSupported()) {
+ if (RecurringCallbackSupport.isEnabled()) {
safepointCount = builder.buildSub(safepointCount, builder.constantInt(1));
builder.buildStore(safepointCount, builder.buildBitcast(safepointCounterAddr, builder.pointerType(builder.intType())));
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/BuildPhaseProvider.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/BuildPhaseProvider.java
index b4dc12f60770..d9d9b4a431fe 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/BuildPhaseProvider.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/BuildPhaseProvider.java
@@ -34,6 +34,7 @@
public final class BuildPhaseProvider {
private boolean featureRegistrationFinished;
+ private boolean setupFinished;
private boolean analysisFinished;
private boolean hostedUniverseBuilt;
private boolean readyForCompilation;
@@ -60,6 +61,14 @@ public static boolean isFeatureRegistrationFinished() {
return ImageSingletons.contains(BuildPhaseProvider.class) && singleton().featureRegistrationFinished;
}
+ public static void markSetupFinished() {
+ singleton().setupFinished = true;
+ }
+
+ public static boolean isSetupFinished() {
+ return ImageSingletons.contains(BuildPhaseProvider.class) && singleton().setupFinished;
+ }
+
public static void markAnalysisFinished() {
singleton().analysisFinished = true;
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java
index 93182c4753ea..49fb1c9bb875 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java
@@ -76,7 +76,7 @@
import com.oracle.svm.core.log.Log;
import com.oracle.svm.core.thread.JavaThreads;
import com.oracle.svm.core.thread.PlatformThreads;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.thread.VMThreads.OSThreadHandle;
import com.oracle.svm.core.util.UserError;
@@ -268,7 +268,7 @@ private static int runCore0() {
@Uninterruptible(reason = "The caller initialized the thread state, so the callees do not need to be uninterruptible.", calleeMustBe = false)
private static void runShutdown() {
- ThreadingSupportImpl.pauseRecurringCallback("Recurring callbacks can't be executed during shutdown.");
+ RecurringCallbackSupport.suspendCallbackTimer("Recurring callbacks can't be executed during shutdown.");
runShutdown0();
}
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 83646883feb7..934f4176973c 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
@@ -95,9 +95,9 @@
import com.oracle.svm.core.snippets.SubstrateForeignCallTarget;
import com.oracle.svm.core.stack.StackOverflowCheck;
import com.oracle.svm.core.thread.PlatformThreads;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.ThreadListenerSupport;
import com.oracle.svm.core.thread.ThreadStatusTransition;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
import com.oracle.svm.core.thread.VMOperationControl;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.thread.VMThreads.SafepointBehavior;
@@ -666,7 +666,7 @@ private static int tearDownIsolate() {
}
/* After threadExit(), only uninterruptible code may be executed. */
- ThreadingSupportImpl.pauseRecurringCallback("Execution of arbitrary code is prohibited during the last teardown steps.");
+ RecurringCallbackSupport.suspendCallbackTimer("Execution of arbitrary code is prohibited during the last teardown steps.");
/* Shut down VM thread. */
if (VMOperationControl.useDedicatedVMOperationThread()) {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/DeoptTester.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/DeoptTester.java
index ba2d359cb394..b474cf6c5ba3 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/DeoptTester.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/DeoptTester.java
@@ -49,7 +49,7 @@
import com.oracle.svm.core.stack.JavaStackWalker;
import com.oracle.svm.core.stack.StackFrameVisitor;
import com.oracle.svm.core.thread.PlatformThreads;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMOperation;
import com.oracle.svm.core.thread.VMThreads.SafepointBehavior;
import com.oracle.svm.core.threadlocal.FastThreadLocalFactory;
@@ -118,7 +118,7 @@ public static void deoptTest() {
try {
if (Heap.getHeap().isAllocationDisallowed() ||
!CEntryPointSnippets.isIsolateInitialized() ||
- ThreadingSupportImpl.isRecurringCallbackPaused() ||
+ (RecurringCallbackSupport.isEnabled() && RecurringCallbackSupport.isCallbackTimerSuspended()) ||
VMOperation.isInProgress() ||
SafepointBehavior.ignoresSafepoints() ||
!PlatformThreads.isCurrentAssigned()) {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/StackOverflowCheckImpl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/StackOverflowCheckImpl.java
index d11e230c60dc..ee9ceb6e2974 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/StackOverflowCheckImpl.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/graal/snippets/StackOverflowCheckImpl.java
@@ -32,7 +32,6 @@
import java.util.Map;
import java.util.function.Predicate;
-import jdk.graal.compiler.word.Word;
import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
@@ -56,7 +55,7 @@
import com.oracle.svm.core.snippets.SubstrateForeignCallTarget;
import com.oracle.svm.core.stack.StackOverflowCheck;
import com.oracle.svm.core.thread.PlatformThreads;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.threadlocal.FastThreadLocal;
import com.oracle.svm.core.threadlocal.FastThreadLocalFactory;
@@ -91,6 +90,7 @@
import jdk.graal.compiler.replacements.SnippetTemplate.Arguments;
import jdk.graal.compiler.replacements.SnippetTemplate.SnippetInfo;
import jdk.graal.compiler.replacements.Snippets;
+import jdk.graal.compiler.word.Word;
import jdk.vm.ci.meta.ResolvedJavaMethod;
public final class StackOverflowCheckImpl implements StackOverflowCheck {
@@ -201,7 +201,7 @@ private static void onYellowZoneMadeAvailable(int oldState, int newState) {
* a recurring callback in the yellow zone is dangerous because a stack overflow in the
* recurring callback would then lead to a fatal error.
*/
- ThreadingSupportImpl.pauseRecurringCallback("Recurring callbacks are considered user code and must not run in yellow zone");
+ RecurringCallbackSupport.suspendCallbackTimer("Recurring callbacks are considered user code and must not run in yellow zone");
stackBoundaryTL.set(stackBoundaryTL.get().subtract(Options.StackYellowZoneSize.getValue()));
}
@@ -241,7 +241,7 @@ private static void onYellowZoneProtected(int oldState, int newState) {
VMError.guarantee(newState < oldState && newState >= STATE_YELLOW_ENABLED, "StackOverflowCheckImpl.onYellowZoneProtected: Illegal state");
if (newState == STATE_YELLOW_ENABLED) {
- ThreadingSupportImpl.resumeRecurringCallbackAtNextSafepoint();
+ RecurringCallbackSupport.resumeCallbackTimerAtNextSafepointCheck();
stackBoundaryTL.set(stackBoundaryTL.get().add(Options.StackYellowZoneSize.getValue()));
}
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 d1ebc30bc27b..c9febee64a3e 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
@@ -36,7 +36,7 @@
import com.oracle.svm.core.Uninterruptible;
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
import com.oracle.svm.core.feature.InternalFeature;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.util.VMError;
@@ -76,7 +76,7 @@ public static boolean isReferenceHandlerThread(Thread other) {
@Override
public void run() {
- ThreadingSupportImpl.pauseRecurringCallback("An exception in a recurring callback must not interrupt pending reference processing because it could result in a memory leak.");
+ RecurringCallbackSupport.suspendCallbackTimer("An exception in a recurring callback must not interrupt pending reference processing because it could result in a memory leak.");
this.isolateThread = CurrentIsolate.getCurrentThread();
try {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpWriter.java
index dcb2e9647555..c019150f8342 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpWriter.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/heap/dump/HeapDumpWriter.java
@@ -83,7 +83,7 @@
import com.oracle.svm.core.stack.JavaStackWalker;
import com.oracle.svm.core.stack.StackFrameVisitor;
import com.oracle.svm.core.thread.PlatformThreads;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMOperation;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.threadlocal.VMThreadLocalSupport;
@@ -421,7 +421,7 @@ public HeapDumpWriter(HeapDumpMetadata metadata) {
public boolean dumpHeap(RawFileDescriptor fd) {
assert VMOperation.isInProgressAtSafepoint();
- assert ThreadingSupportImpl.isRecurringCallbackPaused();
+ assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended();
noAllocationVerifier.open();
try {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java
index 4ec94068c78a..85e807666fed 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/JfrChunkFileWriter.java
@@ -29,8 +29,6 @@
import java.nio.charset.StandardCharsets;
-import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository;
-import jdk.graal.compiler.word.Word;
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
@@ -38,6 +36,7 @@
import com.oracle.svm.core.Uninterruptible;
import com.oracle.svm.core.heap.VMOperationInfos;
+import com.oracle.svm.core.jfr.oldobject.JfrOldObjectRepository;
import com.oracle.svm.core.jfr.sampler.JfrExecutionSampler;
import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler;
import com.oracle.svm.core.jfr.traceid.JfrTraceIdEpoch;
@@ -48,13 +47,14 @@
import com.oracle.svm.core.os.RawFileOperationSupport.RawFileDescriptor;
import com.oracle.svm.core.sampler.SamplerBuffersAccess;
import com.oracle.svm.core.thread.JavaVMOperation;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
import com.oracle.svm.core.thread.VMOperation;
import com.oracle.svm.core.thread.VMOperationControl;
import com.oracle.svm.core.thread.VMThreads;
import jdk.graal.compiler.api.replacements.Fold;
import jdk.graal.compiler.core.common.NumUtil;
+import jdk.graal.compiler.word.Word;
/**
* This class is used when writing the in-memory JFR data to a file. For all operations, except
@@ -673,7 +673,7 @@ private void changeEpoch() {
@Uninterruptible(reason = "Prevent JFR recording.")
private static void processSamplerBuffers() {
assert VMOperation.isInProgressAtSafepoint();
- assert ThreadingSupportImpl.isRecurringCallbackPaused();
+ assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended();
JfrExecutionSampler.singleton().disallowThreadsInSamplerCode();
try {
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java
index deba2e7e368b..d4ce1b9f7b5a 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jfr/sampler/JfrRecurringCallbackExecutionSampler.java
@@ -40,6 +40,7 @@
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.word.Pointer;
+import com.oracle.svm.core.BuildPhaseProvider;
import com.oracle.svm.core.NeverInline;
import com.oracle.svm.core.Uninterruptible;
import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature;
@@ -47,12 +48,15 @@
import com.oracle.svm.core.jfr.JfrExecutionSamplerSupported;
import com.oracle.svm.core.jfr.JfrFeature;
import com.oracle.svm.core.jfr.SubstrateJVM;
+import com.oracle.svm.core.thread.RecurringCallbackSupport;
+import com.oracle.svm.core.thread.RecurringCallbackSupport.RecurringCallbackTimer;
import com.oracle.svm.core.thread.ThreadListenerSupport;
-import com.oracle.svm.core.thread.ThreadingSupportImpl;
-import com.oracle.svm.core.thread.ThreadingSupportImpl.RecurringCallbackTimer;
import com.oracle.svm.core.thread.VMOperation;
import com.oracle.svm.core.thread.VMThreads;
import com.oracle.svm.core.util.TimeUtils;
+import com.oracle.svm.core.util.VMError;
+
+import jdk.graal.compiler.api.replacements.Fold;
public final class JfrRecurringCallbackExecutionSampler extends AbstractJfrExecutionSampler {
private static final ExecutionSampleCallback CALLBACK = new ExecutionSampleCallback();
@@ -61,6 +65,16 @@ public final class JfrRecurringCallbackExecutionSampler extends AbstractJfrExecu
JfrRecurringCallbackExecutionSampler() {
}
+ @Fold
+ public static boolean isPresent() {
+ VMError.guarantee(BuildPhaseProvider.isSetupFinished(), "JfrRecurringCallbackExecutionSampler.isPresent() must not be called too early");
+ if (ImageSingletons.contains(JfrExecutionSampler.class)) {
+ JfrExecutionSampler sampler = ImageSingletons.lookup(JfrExecutionSampler.class);
+ return sampler instanceof JfrRecurringCallbackExecutionSampler;
+ }
+ return false;
+ }
+
@Override
protected void startSampling() {
assert VMOperation.isInProgressAtSafepoint();
@@ -90,7 +104,8 @@ protected void stopSampling() {
}
private RecurringCallbackTimer createRecurringCallbackTimer() {
- return ThreadingSupportImpl.createRecurringCallbackTimer(TimeUtils.millisToNanos(newIntervalMillis), CALLBACK);
+ /* Allocates an object, so this can only be done in interruptible code. */
+ return RecurringCallbackSupport.createCallbackTimer(TimeUtils.millisToNanos(newIntervalMillis), CALLBACK);
}
@Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
@@ -98,10 +113,10 @@ private static void install(IsolateThread thread, RecurringCallbackTimer callbac
assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
if (ExecutionSamplerInstallation.isAllowed(thread)) {
- Threading.RecurringCallback currentCallback = ThreadingSupportImpl.getRecurringCallback(thread);
+ Threading.RecurringCallback currentCallback = RecurringCallbackSupport.getCallback(thread);
if (currentCallback == null) {
ExecutionSamplerInstallation.installed(thread);
- ThreadingSupportImpl.setRecurringCallback(thread, callbackTimer);
+ RecurringCallbackSupport.installCallback(thread, callbackTimer);
}
}
}
@@ -112,9 +127,9 @@ protected void uninstall(IsolateThread thread) {
assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
if (ExecutionSamplerInstallation.isInstalled(thread)) {
- Threading.RecurringCallback currentCallback = ThreadingSupportImpl.getRecurringCallback(thread);
+ Threading.RecurringCallback currentCallback = RecurringCallbackSupport.getCallback(thread);
if (currentCallback == CALLBACK) {
- ThreadingSupportImpl.removeRecurringCallback(thread);
+ RecurringCallbackSupport.uninstallCallback(thread);
}
ExecutionSamplerInstallation.uninstalled(thread);
}
@@ -122,15 +137,15 @@ protected void uninstall(IsolateThread thread) {
@Override
public void beforeThreadRun() {
- RecurringCallbackTimer callbackTimer = createRecurringCallbackTimer();
- beforeThreadRun0(callbackTimer);
+ RecurringCallbackTimer callback = createRecurringCallbackTimer();
+ beforeThreadRun0(callback);
}
@Uninterruptible(reason = "Prevent VM operations that modify the execution sampler or the recurring callbacks.")
- private void beforeThreadRun0(RecurringCallbackTimer callbackTimer) {
+ private void beforeThreadRun0(RecurringCallbackTimer callback) {
if (isSampling()) {
SubstrateJVM.getSamplerBufferPool().adjustBufferCount();
- install(CurrentIsolate.getCurrentThread(), callbackTimer);
+ install(CurrentIsolate.getCurrentThread(), callback);
}
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/RecurringCallbackSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/RecurringCallbackSupport.java
new file mode 100644
index 000000000000..169d311fefe2
--- /dev/null
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/RecurringCallbackSupport.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.svm.core.thread;
+
+import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE;
+import static com.oracle.svm.core.heap.RestrictHeapAccess.Access.NO_ALLOCATION;
+
+import java.io.Serial;
+
+import org.graalvm.nativeimage.CurrentIsolate;
+import org.graalvm.nativeimage.IsolateThread;
+import org.graalvm.nativeimage.Threading.RecurringCallback;
+import org.graalvm.nativeimage.Threading.RecurringCallbackAccess;
+
+import com.oracle.svm.core.Uninterruptible;
+import com.oracle.svm.core.heap.RestrictHeapAccess;
+import com.oracle.svm.core.jdk.UninterruptibleUtils;
+import com.oracle.svm.core.jfr.sampler.JfrRecurringCallbackExecutionSampler;
+import com.oracle.svm.core.option.HostedOptionKey;
+import com.oracle.svm.core.threadlocal.FastThreadLocalFactory;
+import com.oracle.svm.core.threadlocal.FastThreadLocalInt;
+import com.oracle.svm.core.threadlocal.FastThreadLocalObject;
+import com.oracle.svm.core.util.VMError;
+
+import jdk.graal.compiler.api.replacements.Fold;
+import jdk.graal.compiler.options.Option;
+
+/**
+ * Recurring callbacks are per-thread timers that invoke a specific callback method at regular
+ * (best-effort) time intervals. These callbacks are implemented on top of the safepoint mechanism,
+ * see {@link SafepointSlowpath} for more details.
+ *
+ * Recurring callbacks are executed at the end of the safepoint slowpath, so only carefully designed
+ * {@link Uninterruptible} Java code may be executed. Running normal Java code, or even worse JDK
+ * code, may result in deadlocks or crashes. Problems typically happen because the recurring
+ * callback execution has some unexpected side effect on the execution state of the application or
+ * VM. Even allocating Java objects can already be enough to cause problems.
+ */
+public class RecurringCallbackSupport {
+ public static class ConcealedOptions {
+ @Option(help = "Support a per-thread timer that is called at a specific interval.") //
+ public static final HostedOptionKey SupportRecurringCallback = new HostedOptionKey<>(false);
+
+ @Option(help = "Test whether a thread's recurring callback is pending on each transition from native code to Java.") //
+ static final HostedOptionKey CheckRecurringCallbackOnNativeToJavaTransition = new HostedOptionKey<>(false);
+ }
+
+ /**
+ * The value of this thread-local can change at any safepoint (another thread may for example
+ * install a recurring callback for all active threads in a VM operation). We need to take this
+ * into account when accessing the value.
+ */
+ private static final FastThreadLocalObject timerTL = FastThreadLocalFactory.createObject(RecurringCallbackTimer.class, "RecurringCallbackSupport.timer");
+ private static final FastThreadLocalInt suspendedTL = FastThreadLocalFactory.createInt("RecurringCallbackSupport.suspended");
+
+ @Fold
+ public static boolean isEnabled() {
+ return ConcealedOptions.SupportRecurringCallback.getValue() || JfrRecurringCallbackExecutionSampler.isPresent();
+ }
+
+ public static RecurringCallbackTimer createCallbackTimer(long intervalNanos, RecurringCallback callback) {
+ assert isEnabled();
+ assert callback != null;
+
+ return new RecurringCallbackTimer(intervalNanos, callback);
+ }
+
+ @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
+ public static void installCallback(IsolateThread thread, RecurringCallbackTimer timer, boolean overwriteExisting) {
+ if (overwriteExisting) {
+ uninstallCallback(thread);
+ }
+ installCallback(thread, timer);
+ }
+
+ @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
+ public static void installCallback(IsolateThread thread, RecurringCallbackTimer timer) {
+ assert isEnabled();
+ assert timer != null;
+ assert timer.targetIntervalNanos > 0;
+ assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
+ assert timerTL.get(thread) == null : "only one callback can be installed at a time";
+
+ timerTL.set(thread, timer);
+ SafepointCheckCounter.setVolatile(thread, timer.requestedChecks);
+ }
+
+ @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
+ public static void uninstallCallback(IsolateThread thread) {
+ assert isEnabled();
+ assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
+
+ timerTL.set(thread, null);
+ }
+
+ @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.", callerMustBe = true)
+ public static RecurringCallback getCallback(IsolateThread thread) {
+ assert isEnabled();
+ assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
+
+ RecurringCallbackTimer value = timerTL.get(thread);
+ if (value != null) {
+ return value.callback;
+ }
+ return null;
+ }
+
+ /**
+ * Updates the statistics that are used to compute how frequently this thread needs to enter the
+ * safepoint slowpath and executes the callback if necessary. This also resets the
+ * {@link SafepointCheckCounter} so that some time can pass before this thread enters the
+ * safepoint slowpath again.
+ *
+ * Note that the callback execution may throw exceptions, so this is NOT necessarily fully
+ * uninterruptible.
+ */
+ @Uninterruptible(reason = "Must not contain safepoint checks.")
+ static void maybeExecuteCallback() {
+ assert VMThreads.StatusSupport.isStatusJava() : "must only be executed when the thread is in Java state";
+
+ RecurringCallbackTimer timer = isEnabled() ? timerTL.get() : null;
+ if (timer != null) {
+ timer.evaluate();
+ } else {
+ SafepointCheckCounter.setVolatile(SafepointCheckCounter.MAX_VALUE);
+ }
+ }
+
+ @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.", callerMustBe = true)
+ static boolean isCallbackInstalled(IsolateThread thread) {
+ return isEnabled() && timerTL.get(thread) != null;
+ }
+
+ static boolean needsNativeToJavaSlowpath() {
+ return isEnabled() && ConcealedOptions.CheckRecurringCallbackOnNativeToJavaTransition.getValue() && timerTL.get() != null && !isCallbackTimerSuspended();
+ }
+
+ /**
+ * Suspends the execution of recurring callbacks for the current thread. In some code parts
+ * (e.g., when executing VM operations), we need to suspend the recurring callback execution
+ * temporarily because we can't deal with arbitrary code execution or thrown exceptions.
+ */
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static void suspendCallbackTimer(@SuppressWarnings("unused") String reason) {
+ if (!isEnabled()) {
+ return;
+ }
+
+ /*
+ * Even if no callback is installed at the moment, we still need to increment the counter
+ * because a callback could be installed at a later point.
+ */
+ incrementSuspended();
+ }
+
+ /**
+ * Resumes the execution of recurring callbacks for the current thread. The callback execution
+ * might be triggered at the next safepoint check.
+ */
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static void resumeCallbackTimerAtNextSafepointCheck() {
+ if (!isEnabled()) {
+ return;
+ }
+
+ decrementSuspended();
+ if (!isCallbackTimerSuspended()) {
+ RecurringCallbackTimer timer = timerTL.get();
+ if (timer != null) {
+ timer.updateStatistics();
+ timer.setCounter(1);
+ }
+ }
+ }
+
+ /**
+ * Like {@link #resumeCallbackTimerAtNextSafepointCheck()} but with the difference that this
+ * method may trigger the execution of the recurring callback right away.
+ */
+ public static void resumeCallbackTimer() {
+ if (!isEnabled()) {
+ return;
+ }
+
+ decrementSuspended();
+ if (!isCallbackTimerSuspended()) {
+ resumeCallbackTimer0();
+ }
+ }
+
+ @Uninterruptible(reason = "Prevent unexpected recurring callback execution (pending exception must not be destroyed).")
+ private static void resumeCallbackTimer0() {
+ try {
+ maybeExecuteCallback();
+ } catch (SafepointException e) {
+ /* Callers cannot declare `throws Throwable`. */
+ throwUnchecked(RecurringCallbackTimer.getAndClearPendingException());
+ }
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static boolean isCallbackUnsupportedOrTimerSuspended() {
+ return !isEnabled() || isCallbackTimerSuspended();
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static boolean isCallbackTimerSuspended() {
+ assert isEnabled();
+ return suspendedTL.get() != 0;
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ private static void incrementSuspended() {
+ assert isEnabled();
+ assert suspendedTL.get() >= 0;
+ suspendedTL.set(suspendedTL.get() + 1);
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ private static void decrementSuspended() {
+ assert isEnabled();
+ int newValue = suspendedTL.get() - 1;
+ assert newValue >= 0;
+ suspendedTL.set(newValue);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ private static void throwUnchecked(Throwable exception) throws T {
+ throw (T) exception; // T is inferred as RuntimeException, but doesn't have to be
+ }
+
+ /**
+ * The timer starts with an initial {@link #INITIAL_CHECKS number of safepoint check operations}
+ * for the interval and then measures how frequently check operations occur to determine the
+ * period of check operations for the requested time intervals. The timer uses an exponentially
+ * weighted moving average (EWMA) to adapt to a changing frequency of safepoint checks in the
+ * code that the thread executes.
+ */
+ public static class RecurringCallbackTimer {
+ private static final FastThreadLocalObject EXCEPTION_TL = FastThreadLocalFactory.createObject(Throwable.class, "RecurringCallbackTimer.exception");
+ private static final RecurringCallbackAccess CALLBACK_ACCESS = new RecurringCallbackAccessImpl();
+
+ /**
+ * Weight of the newest sample in {@link #ewmaChecksPerNano}. Older samples have a total
+ * weight of 1 - {@link #EWMA_LAMBDA}.
+ */
+ private static final double EWMA_LAMBDA = 0.3;
+ private static final double TARGET_INTERVAL_FLEXIBILITY = 0.95;
+ private static final int INITIAL_CHECKS = 100;
+ private static final long MINIMUM_INTERVAL_NANOS = 1_000;
+
+ private final long targetIntervalNanos;
+ private final long flexibleTargetIntervalNanos;
+ private final RecurringCallback callback;
+
+ private int requestedChecks;
+ private double ewmaChecksPerNano;
+ private long lastCapture;
+ private long lastCallbackExecution;
+
+ private volatile boolean isExecuting = false;
+
+ RecurringCallbackTimer(long targetIntervalNanos, RecurringCallback callback) {
+ this.targetIntervalNanos = Math.max(MINIMUM_INTERVAL_NANOS, targetIntervalNanos);
+ this.flexibleTargetIntervalNanos = (long) (targetIntervalNanos * TARGET_INTERVAL_FLEXIBILITY);
+ this.callback = callback;
+
+ long now = System.nanoTime();
+ this.lastCapture = now;
+ this.lastCallbackExecution = now;
+ this.requestedChecks = INITIAL_CHECKS;
+ }
+
+ @Uninterruptible(reason = "Prevent recurring callback execution.", callerMustBe = true)
+ public static Throwable getAndClearPendingException() {
+ Throwable t = EXCEPTION_TL.get();
+ VMError.guarantee(t != null, "There must be a recurring callback exception pending.");
+ EXCEPTION_TL.set(null);
+ return t;
+ }
+
+ @Uninterruptible(reason = "Must not contain safepoint checks.")
+ void evaluate() {
+ updateStatistics();
+ try {
+ maybeExecuteCallback();
+ } finally {
+ updateCounter();
+ }
+ }
+
+ @Uninterruptible(reason = "Must be uninterruptible to avoid races with the safepoint code.")
+ void updateStatistics() {
+ long now = System.nanoTime();
+ long elapsedNanos = now - lastCapture;
+
+ int skippedChecks = getSkippedChecks(CurrentIsolate.getCurrentThread());
+ int executedChecks = requestedChecks - skippedChecks;
+ assert executedChecks >= 0;
+ if (elapsedNanos > 0 && executedChecks > 0) {
+ double checksPerNano = executedChecks / (double) elapsedNanos;
+ if (ewmaChecksPerNano == 0) { // initialization
+ ewmaChecksPerNano = checksPerNano;
+ } else {
+ ewmaChecksPerNano = EWMA_LAMBDA * checksPerNano + (1 - EWMA_LAMBDA) * ewmaChecksPerNano;
+ }
+ lastCapture = now;
+ }
+ }
+
+ @Uninterruptible(reason = "Must be uninterruptible to avoid races with the safepoint code.")
+ private static int getSkippedChecks(IsolateThread thread) {
+ int rawValue = SafepointCheckCounter.getVolatile(thread);
+ return rawValue >= 0 ? rawValue : -rawValue;
+ }
+
+ @Uninterruptible(reason = "Must not contain safepoint checks.")
+ private void maybeExecuteCallback() {
+ if (isCallbackDisabled()) {
+ return;
+ }
+
+ isExecuting = true;
+ try {
+ /*
+ * Allow the callback to trigger a bit early - otherwise, it can happen that we
+ * enter the slowpath multiple times while closing in on the deadline.
+ */
+ if (System.nanoTime() >= lastCallbackExecution + flexibleTargetIntervalNanos) {
+ /*
+ * Before executing the callback, reset the safepoint requested counter as we
+ * don't want to trigger another callback execution in the near future.
+ */
+ setCounter(SafepointCheckCounter.MAX_VALUE);
+ try {
+ invokeCallback();
+ /*
+ * The callback is allowed to throw an exception (e.g., to stop or interrupt
+ * long-running code). All code that must run to reinitialize the recurring
+ * callback state must therefore be in a finally-block.
+ */
+ } finally {
+ lastCallbackExecution = System.nanoTime();
+ updateStatistics();
+ }
+ }
+ } finally {
+ isExecuting = false;
+ }
+ }
+
+ @Uninterruptible(reason = "Must not contain safepoint checks.")
+ private void updateCounter() {
+ long nextDeadline = lastCallbackExecution + targetIntervalNanos;
+ long remainingNanos = nextDeadline - System.nanoTime();
+ if (remainingNanos < 0 && isCallbackDisabled()) {
+ /*
+ * If we are already behind the deadline and recurring callbacks are disabled for
+ * some reason, then we can safely assume that there won't be any need to trigger
+ * recurring callback execution for a long time (reenabling the callbacks triggers
+ * the execution explicitly).
+ */
+ setCounter(SafepointCheckCounter.MAX_VALUE);
+ } else {
+ remainingNanos = UninterruptibleUtils.Math.max(remainingNanos, MINIMUM_INTERVAL_NANOS);
+ double checks = ewmaChecksPerNano * remainingNanos;
+ setCounter(checks > SafepointCheckCounter.MAX_VALUE ? SafepointCheckCounter.MAX_VALUE : ((checks < 1) ? 1 : (int) checks));
+ }
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ void setCounter(int value) {
+ requestedChecks = value;
+ SafepointCheckCounter.setVolatile(value);
+ }
+
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ private boolean isCallbackDisabled() {
+ return isExecuting || isCallbackTimerSuspended();
+ }
+
+ /**
+ * Recurring callbacks may be executed in any method that contains a safepoint check. This
+ * includes methods that need to be allocation free. Therefore, recurring callbacks must not
+ * allocate any Java heap memory.
+ */
+ @Uninterruptible(reason = "Required by caller, but does not apply to callee.", calleeMustBe = false)
+ @RestrictHeapAccess(reason = "Recurring callbacks must not allocate.", access = NO_ALLOCATION)
+ private void invokeCallback() {
+ try {
+ callback.run(CALLBACK_ACCESS);
+ } catch (SafepointException e) {
+ throw e;
+ } catch (Throwable t) {
+ /*
+ * Recurring callbacks are specified to ignore exceptions (except if the exception
+ * is thrown via RecurringCallbackAccess.throwException(), which is handled above).
+ * We cannot even log the exception because that could lead to a StackOverflowError
+ * (especially when the recurring callback failed with a StackOverflowError).
+ */
+ }
+ }
+
+ /**
+ * We need to distinguish between arbitrary exceptions (must be swallowed) and exceptions
+ * that are thrown via {@link RecurringCallbackAccess#throwException} (must be forwarded to
+ * the application). When a recurring callback uses
+ * {@link RecurringCallbackAccess#throwException}, we store the exception in a thread local
+ * (to avoid allocations) and throw a pre-allocated marker exception instead. We catch the
+ * marker exception internally, accesses the thread local, and rethrow that exception.
+ */
+ private static final class RecurringCallbackAccessImpl implements RecurringCallbackAccess {
+ @Override
+ public void throwException(Throwable t) {
+ EXCEPTION_TL.set(t);
+ throw SafepointException.SINGLETON;
+ }
+ }
+ }
+
+ static final class SafepointException extends RuntimeException {
+ public static final SafepointException SINGLETON = new SafepointException();
+
+ @Serial //
+ private static final long serialVersionUID = 1L;
+
+ private SafepointException() {
+ }
+ }
+}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Safepoint.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Safepoint.java
index 081a6af5e1f4..1518d2a50bc7 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Safepoint.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/Safepoint.java
@@ -116,6 +116,13 @@ boolean isInProgress() {
return safepointState == AT_SAFEPOINT;
}
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public boolean isPendingOrInProgress() {
+ /* Only read the state once. */
+ int state = safepointState;
+ return state == SYNCHRONIZING || state == AT_SAFEPOINT;
+ }
+
@Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
public UnsignedWord getSafepointId() {
return safepointId;
@@ -126,11 +133,6 @@ public boolean isMasterThread() {
return requestingThread == CurrentIsolate.getCurrentThread();
}
- @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
- public boolean isPendingOrInProgress() {
- return requestingThread.isNonNull();
- }
-
/**
* Initiates a safepoint.
*
@@ -150,12 +152,13 @@ boolean startSafepoint(String reason) {
THREAD_MUTEX.lock();
}
+ assert requestingThread.isNull();
requestingThread = CurrentIsolate.getCurrentThread();
ImageSingletons.lookup(Heap.class).prepareForSafepoint();
+
safepointState = SYNCHRONIZING;
+ int numJavaThreads = requestThreadsEnterSafepoint(reason);
- int numJavaThreads = requestThreadsEnterSafepoint();
- waitUntilThreadsEnterSafepoint(reason);
safepointState = AT_SAFEPOINT;
safepointId = safepointId.add(1);
SafepointBeginEvent.emit(getSafepointId(), numJavaThreads, startTicks);
@@ -167,46 +170,34 @@ boolean startSafepoint(String reason) {
void endSafepoint(boolean unlock) {
assert VMOperationControl.mayExecuteVmOperations();
long startTicks = JfrTicks.elapsedTicks();
+
safepointState = NOT_AT_SAFEPOINT;
releaseThreadsFromSafepoint();
/* Some Java threads can continue execution now. */
- SafepointEndEvent.emit(getSafepointId(), startTicks);
- ImageSingletons.lookup(Heap.class).endSafepoint();
-
- requestingThread = Word.nullPointer();
if (unlock) {
THREAD_MUTEX.unlock();
}
- /* This can take a moment, so we do it after letting all other threads proceed. */
- VMThreads.singleton().cleanupExitedOsThreads();
- }
-
- /**
- * Ask all other threads to come to a safepoint. There is no guarantee that the threads will
- * comply with the request.
- */
- private static int requestThreadsEnterSafepoint() {
- assert THREAD_MUTEX.isOwner() : "must hold mutex while requesting a safepoint";
- int numJavaThreads = 0;
+ /*
+ * Now that the safepoint was released, we can treat this thread like a normal VM thread and
+ * don't need the special handling in the safepoint slowpath anymore. Other threads also
+ * can't start a new safepoint yet (this code is still executed in the VM operation thread
+ * and only the VM operation thread may start a safepoint).
+ */
+ requestingThread = Word.nullPointer();
- for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) {
- numJavaThreads++;
- if (thread == CurrentIsolate.getCurrentThread()) {
- continue;
- }
- if (SafepointBehavior.ignoresSafepoints(thread)) {
- /* If safepoints are disabled, do not ask it to stop at safepoints. */
- continue;
- }
- requestEnterSafepoint(thread);
- }
- return numJavaThreads;
+ /*
+ * Everything down here can take a moment, so we do it after letting all other threads
+ * proceed.
+ */
+ ImageSingletons.lookup(Heap.class).endSafepoint();
+ SafepointEndEvent.emit(getSafepointId(), startTicks);
+ VMThreads.singleton().cleanupExitedOsThreads();
}
- /** Wait until all other threads reach a safepoint. Re-requests safepoints if necessary. */
- private static void waitUntilThreadsEnterSafepoint(String reason) {
+ /** Blocks until all threads (other than the current thread) have entered the safepoint. */
+ private static int requestThreadsEnterSafepoint(String reason) {
assert THREAD_MUTEX.isOwner() : "must hold mutex while waiting for safepoints";
long startNanos = System.nanoTime();
@@ -216,12 +207,13 @@ private static void waitUntilThreadsEnterSafepoint(String reason) {
long failureNanos = -1;
for (int loopCount = 1; /* return */ ; loopCount++) {
+ int numThreads = 0;
int atSafepoint = 0;
int ignoreSafepoints = 0;
int notAtSafepoint = 0;
- int lostUpdates = 0;
for (IsolateThread thread = VMThreads.firstThread(); thread.isNonNull(); thread = VMThreads.nextThread(thread)) {
+ numThreads++;
if (thread == CurrentIsolate.getCurrentThread()) {
continue;
}
@@ -245,10 +237,9 @@ private static void waitUntilThreadsEnterSafepoint(String reason) {
switch (status) {
case StatusSupport.STATUS_IN_JAVA:
case StatusSupport.STATUS_IN_VM: {
- /* Re-request the safepoint in case of a lost update. */
+ /* Request the safepoint (or re-request in case of a lost update). */
if (SafepointCheckCounter.getVolatile(thread) > 0) {
requestEnterSafepoint(thread);
- lostUpdates++;
}
notAtSafepoint++;
break;
@@ -274,8 +265,8 @@ private static void waitUntilThreadsEnterSafepoint(String reason) {
}
if (notAtSafepoint == 0) {
- /* All threads entered the safepoint. */
- return;
+ /* All relevant threads entered the safepoint. */
+ return numThreads;
}
if (warningNanos == -1 || failureNanos == -1) {
@@ -297,7 +288,6 @@ private static void waitUntilThreadsEnterSafepoint(String reason) {
.string(" atSafepoint: ").signed(atSafepoint)
.string(" ignoreSafepoints: ").signed(ignoreSafepoints)
.string(" notAtSafepoint: ").signed(notAtSafepoint)
- .string(" lostUpdates: ").signed(lostUpdates)
.string("]")
.newline();
@@ -333,6 +323,7 @@ private static void releaseThreadsFromSafepoint() {
continue;
}
+ assert StatusSupport.getStatusVolatile(thread) == StatusSupport.STATUS_IN_SAFEPOINT;
restoreCounter(thread);
/* Skip suspended threads so that they remain in STATUS_IN_SAFEPOINT. */
@@ -361,7 +352,7 @@ private static void releaseThreadsFromSafepoint() {
* value before a safepoint was requested.
*/
private static void requestEnterSafepoint(IsolateThread thread) {
- if (ThreadingSupportImpl.isRecurringCallbackSupported()) {
+ if (RecurringCallbackSupport.isEnabled()) {
int value;
do {
value = SafepointCheckCounter.getVolatile(thread);
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointCheckCounter.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointCheckCounter.java
index 8277d1fd729a..dc4869046812 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointCheckCounter.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointCheckCounter.java
@@ -39,13 +39,17 @@
/**
* Per-thread counter for safepoint checks. If the counter reaches a value less or equal 0, the
* {@link SafepointSlowpath} is entered.
- *
+ *
* Be careful when calling any of the methods that manipulate or set the counter value directly as
* they have the potential to destroy or skew the data that is needed for scheduling the execution
- * of recurring callbacks (i.e., the number of executed safepoints in a period of time). See
- * {@link ThreadingSupportImpl} for more details about recurring callbacks.
- *
- * The safepoint check counter can have one of the following values:
+ * of recurring callbacks (i.e., the number of executed safepoints in a period of time). See class
+ * {@link RecurringCallbackSupport} for more details about recurring callbacks.
+ *
+ * If the recurring callback support is disabled, the safepoint check counter can only be 0
+ * (safepoint requested) or {@link #MAX_VALUE} (normal execution).
+ *
+ * If the recurring callback support is enabled, the safepoint check counter can have one of the
+ * following values:
*
* - value > 0: remaining number of safepoint checks before the safepoint slowpath code is
* executed.
@@ -69,7 +73,7 @@ static boolean compareAndSet(IsolateThread thread, int oldValue, int newValue) {
@Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
static void setVolatile(IsolateThread thread, int value) {
assert CurrentIsolate.getCurrentThread() == thread || VMThreads.StatusSupport.isStatusCreated(thread) || VMOperationControl.mayExecuteVmOperations();
- assert value > 0;
+ assert RecurringCallbackSupport.isEnabled() && value > 0 || !RecurringCallbackSupport.isEnabled() && (value == 0 || value == MAX_VALUE);
valueTL.setVolatile(thread, value);
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointSlowpath.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointSlowpath.java
index d9cc463d4081..010e6903688d 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointSlowpath.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/SafepointSlowpath.java
@@ -26,8 +26,6 @@
import static jdk.graal.compiler.core.common.spi.ForeignCallDescriptor.CallSideEffect.NO_SIDE_EFFECT;
-import org.graalvm.nativeimage.CurrentIsolate;
-
import com.oracle.svm.core.AlwaysInline;
import com.oracle.svm.core.NeverInline;
import com.oracle.svm.core.Uninterruptible;
@@ -40,6 +38,7 @@
import com.oracle.svm.core.snippets.SnippetRuntime.SubstrateForeignCallDescriptor;
import com.oracle.svm.core.snippets.SubstrateForeignCallTarget;
import com.oracle.svm.core.stack.JavaFrameAnchors;
+import com.oracle.svm.core.thread.RecurringCallbackSupport.RecurringCallbackTimer;
import com.oracle.svm.core.thread.VMThreads.ActionOnTransitionToJavaSupport;
import com.oracle.svm.core.thread.VMThreads.StatusSupport;
import com.oracle.svm.core.util.VMError;
@@ -50,11 +49,11 @@
* {@link #slowPathSafepointCheck(int, boolean, boolean) slowpath}. See {@link SafepointCheckNode}
* and {@link SafepointSnippets} for more details.
*
- * Recurring callbacks (see {@link ThreadingSupportImpl}) are implemented on top of the safepoint
- * mechanism. If {@linkplain ThreadingSupportImpl.Options#SupportRecurringCallback supported}, each
- * safepoint check decrements {@link SafepointCheckCounter} before the comparison. So, threads enter
- * the safepoint slowpath periodically, even if no safepoint is pending. At the end of the slowpath,
- * the recurring callback is executed if the timer has expired.
+ * {@link RecurringCallbackSupport Recurring callbacks} are implemented on top of the safepoint
+ * mechanism. If {@linkplain RecurringCallbackSupport#isEnabled supported}, each safepoint check
+ * decrements {@link SafepointCheckCounter} before the comparison. So, threads enter the safepoint
+ * slowpath periodically, even if no safepoint is pending. At the end of the slowpath, the recurring
+ * callback is executed if the timer has expired.
*/
public class SafepointSlowpath {
/*
@@ -134,9 +133,9 @@ private static void enterSlowPathTransitionFromNativeToNewStatus(int newStatus,
private static void slowPathSafepointCheck(int newStatus, boolean callerHasJavaFrameAnchor, boolean popFrameAnchor) throws Throwable {
try {
slowPathSafepointCheck0(newStatus, callerHasJavaFrameAnchor, popFrameAnchor);
- } catch (ThreadingSupportImpl.SafepointException e) {
+ } catch (RecurringCallbackSupport.SafepointException e) {
/* This exception is intended to be thrown from safepoint checks, at one's own risk */
- throw ThreadingSupportImpl.RecurringCallbackTimer.getAndClearPendingException();
+ throw RecurringCallbackTimer.getAndClearPendingException();
} catch (Throwable ex) {
/*
* The foreign call from snippets to this method does not have an exception edge. So we
@@ -162,7 +161,7 @@ private static void slowPathSafepointCheck(int newStatus, boolean callerHasJavaF
private static void slowPathSafepointCheck0(int newStatus, boolean callerHasJavaFrameAnchor, boolean popFrameAnchor) {
if (Safepoint.singleton().isMasterThread()) {
/* Must not stop at a safepoint nor trigger a callback. */
- assert !ThreadingSupportImpl.isRecurringCallbackRegistered(CurrentIsolate.getCurrentThread()) || ThreadingSupportImpl.isRecurringCallbackPaused();
+ assert RecurringCallbackSupport.isCallbackUnsupportedOrTimerSuspended();
} else {
do {
if (Safepoint.singleton().isPendingOrInProgress() || ThreadSuspendSupport.isCurrentThreadSuspended()) {
@@ -187,7 +186,7 @@ private static void slowPathSafepointCheck0(int newStatus, boolean callerHasJava
if (newStatus == StatusSupport.STATUS_IN_JAVA) {
ActionOnTransitionToJavaSupport.runPendingActions();
- ThreadingSupportImpl.onSafepointCheckSlowpath();
+ RecurringCallbackSupport.maybeExecuteCallback();
}
}
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadStatusTransition.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadStatusTransition.java
index 91290530c67f..eb7765777ccd 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadStatusTransition.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadStatusTransition.java
@@ -72,7 +72,7 @@ public static void fromNativeToJava(boolean popFrameAnchor) {
int newStatus = StatusSupport.STATUS_IN_JAVA;
if (probability(VERY_FAST_PATH_PROBABILITY, !VMThreads.ActionOnTransitionToJavaSupport.isActionPending()) &&
- probability(VERY_FAST_PATH_PROBABILITY, !ThreadingSupportImpl.needsNativeToJavaSlowpath()) &&
+ probability(VERY_FAST_PATH_PROBABILITY, !RecurringCallbackSupport.needsNativeToJavaSlowpath()) &&
probability(VERY_FAST_PATH_PROBABILITY, StatusSupport.compareAndSetNativeToNewStatus(newStatus))) {
if (popFrameAnchor) {
JavaFrameAnchors.popFrameAnchor();
diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadingSupportImpl.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadingSupportImpl.java
index a6db21a6298c..15fc04d6d68b 100644
--- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadingSupportImpl.java
+++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/ThreadingSupportImpl.java
@@ -24,238 +24,25 @@
*/
package com.oracle.svm.core.thread;
-import static com.oracle.svm.core.heap.RestrictHeapAccess.Access.NO_ALLOCATION;
-import static com.oracle.svm.core.thread.ThreadingSupportImpl.Options.SupportRecurringCallback;
+import static com.oracle.svm.core.Uninterruptible.CALLED_FROM_UNINTERRUPTIBLE_CODE;
-import java.io.Serial;
import java.util.concurrent.TimeUnit;
import org.graalvm.nativeimage.CurrentIsolate;
import org.graalvm.nativeimage.IsolateThread;
-import org.graalvm.nativeimage.Threading.RecurringCallback;
-import org.graalvm.nativeimage.Threading.RecurringCallbackAccess;
+import org.graalvm.nativeimage.Threading;
import org.graalvm.nativeimage.impl.ThreadingSupport;
import com.oracle.svm.core.Uninterruptible;
import com.oracle.svm.core.feature.AutomaticallyRegisteredImageSingleton;
-import com.oracle.svm.core.heap.RestrictHeapAccess;
-import com.oracle.svm.core.jdk.UninterruptibleUtils;
-import com.oracle.svm.core.option.HostedOptionKey;
import com.oracle.svm.core.option.SubstrateOptionsParser;
-import com.oracle.svm.core.thread.VMThreads.StatusSupport;
-import com.oracle.svm.core.threadlocal.FastThreadLocalFactory;
-import com.oracle.svm.core.threadlocal.FastThreadLocalInt;
-import com.oracle.svm.core.threadlocal.FastThreadLocalObject;
-import com.oracle.svm.core.util.VMError;
+import com.oracle.svm.core.thread.RecurringCallbackSupport.RecurringCallbackTimer;
import jdk.graal.compiler.api.replacements.Fold;
-import jdk.graal.compiler.options.Option;
@AutomaticallyRegisteredImageSingleton(ThreadingSupport.class)
public class ThreadingSupportImpl implements ThreadingSupport {
- public static class Options {
- @Option(help = "Support a per-thread timer that is called at a specific interval.") //
- public static final HostedOptionKey SupportRecurringCallback = new HostedOptionKey<>(true);
-
- @Option(help = "Test whether a thread's recurring callback is pending on each transition from native code to Java.") //
- public static final HostedOptionKey CheckRecurringCallbackOnNativeToJavaTransition = new HostedOptionKey<>(false);
- }
-
- /**
- * Implementation of a per-thread timer that uses safepoint check operations to invoke a
- * callback method in regular (best-effort) time intervals. The timer starts with an initial
- * {@link #INITIAL_CHECKS number of check operations} for the interval and then measures how
- * frequently check operations occur to determine the period of check operations for the
- * requested time intervals. The timer uses an exponentially weighted moving average (EWMA) to
- * adapt to a changing frequency of safepoint checks in the code that the thread executes.
- */
- public static class RecurringCallbackTimer {
- private static final FastThreadLocalObject EXCEPTION_TL = FastThreadLocalFactory.createObject(Throwable.class, "RecurringCallbackTimer.exception");
- private static final RecurringCallbackAccess CALLBACK_ACCESS = new RecurringCallbackAccessImpl();
-
- /**
- * Weight of the newest sample in {@link #ewmaChecksPerNano}. Older samples have a total
- * weight of 1 - {@link #EWMA_LAMBDA}.
- */
- private static final double EWMA_LAMBDA = 0.3;
- private static final double TARGET_INTERVAL_FLEXIBILITY = 0.95;
- private static final int INITIAL_CHECKS = 100;
- private static final long MINIMUM_INTERVAL_NANOS = 1_000;
-
- private final long targetIntervalNanos;
- private final long flexibleTargetIntervalNanos;
- private final RecurringCallback callback;
-
- private int requestedChecks;
- private double ewmaChecksPerNano;
- private long lastCapture;
- private long lastCallbackExecution;
-
- private volatile boolean isExecuting = false;
-
- RecurringCallbackTimer(long targetIntervalNanos, RecurringCallback callback) {
- this.targetIntervalNanos = Math.max(MINIMUM_INTERVAL_NANOS, targetIntervalNanos);
- this.flexibleTargetIntervalNanos = (long) (targetIntervalNanos * TARGET_INTERVAL_FLEXIBILITY);
- this.callback = callback;
-
- long now = System.nanoTime();
- this.lastCapture = now;
- this.lastCallbackExecution = now;
- this.requestedChecks = INITIAL_CHECKS;
- }
-
- @Uninterruptible(reason = "Prevent recurring callback execution.", callerMustBe = true)
- public static Throwable getAndClearPendingException() {
- Throwable t = EXCEPTION_TL.get();
- VMError.guarantee(t != null, "There must be a recurring callback exception pending.");
- EXCEPTION_TL.set(null);
- return t;
- }
-
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- void evaluate() {
- updateStatistics();
- try {
- executeCallback();
- } finally {
- updateCounter();
- }
- }
-
- @Uninterruptible(reason = "Must be uninterruptible to avoid races with the safepoint code.")
- void updateStatistics() {
- long now = System.nanoTime();
- long elapsedNanos = now - lastCapture;
-
- int skippedChecks = getSkippedChecks(CurrentIsolate.getCurrentThread());
- int executedChecks = requestedChecks - skippedChecks;
- assert executedChecks >= 0;
- if (elapsedNanos > 0 && executedChecks > 0) {
- double checksPerNano = executedChecks / (double) elapsedNanos;
- if (ewmaChecksPerNano == 0) { // initialization
- ewmaChecksPerNano = checksPerNano;
- } else {
- ewmaChecksPerNano = EWMA_LAMBDA * checksPerNano + (1 - EWMA_LAMBDA) * ewmaChecksPerNano;
- }
- lastCapture = now;
- }
- }
-
- @Uninterruptible(reason = "Must be uninterruptible to avoid races with the safepoint code.")
- private static int getSkippedChecks(IsolateThread thread) {
- int rawValue = SafepointCheckCounter.getVolatile(thread);
- return rawValue >= 0 ? rawValue : -rawValue;
- }
-
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- private void executeCallback() {
- if (isCallbackDisabled()) {
- return;
- }
-
- isExecuting = true;
- try {
- /*
- * Allow the callback to trigger a bit early - otherwise, it can happen that we
- * enter the slowpath multiple times while closing in on the deadline.
- */
- if (System.nanoTime() >= lastCallbackExecution + flexibleTargetIntervalNanos) {
- /*
- * Before executing the callback, reset the safepoint requested counter as we
- * don't want to trigger another callback execution in the near future.
- */
- setCounter(SafepointCheckCounter.MAX_VALUE);
- try {
- invokeCallback();
- /*
- * The callback is allowed to throw an exception (e.g., to stop or interrupt
- * long-running code). All code that must run to reinitialize the recurring
- * callback state must therefore be in a finally-block.
- */
- } finally {
- lastCallbackExecution = System.nanoTime();
- updateStatistics();
- }
- }
- } finally {
- isExecuting = false;
- }
- }
-
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- private void updateCounter() {
- long nextDeadline = lastCallbackExecution + targetIntervalNanos;
- long remainingNanos = nextDeadline - System.nanoTime();
- if (remainingNanos < 0 && isCallbackDisabled()) {
- /*
- * If we are already behind the deadline and recurring callbacks are disabled for
- * some reason, then we can safely assume that there won't be any need to trigger
- * recurring callback execution for a long time (reenabling the callbacks triggers
- * the execution explicitly).
- */
- setCounter(SafepointCheckCounter.MAX_VALUE);
- } else {
- remainingNanos = UninterruptibleUtils.Math.max(remainingNanos, MINIMUM_INTERVAL_NANOS);
- double checks = ewmaChecksPerNano * remainingNanos;
- setCounter(checks > SafepointCheckCounter.MAX_VALUE ? SafepointCheckCounter.MAX_VALUE : ((checks < 1) ? 1 : (int) checks));
- }
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- void setCounter(int value) {
- requestedChecks = value;
- SafepointCheckCounter.setVolatile(value);
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- private boolean isCallbackDisabled() {
- return isExecuting || isRecurringCallbackPaused();
- }
-
- /**
- * Recurring callbacks may be executed in any method that contains a safepoint check. This
- * includes methods that need to be allocation free. Therefore, recurring callbacks must not
- * allocate any Java heap memory.
- */
- @Uninterruptible(reason = "Required by caller, but does not apply to callee.", calleeMustBe = false)
- @RestrictHeapAccess(reason = "Recurring callbacks must not allocate.", access = NO_ALLOCATION)
- private void invokeCallback() {
- try {
- callback.run(CALLBACK_ACCESS);
- } catch (SafepointException e) {
- throw e;
- } catch (Throwable t) {
- /*
- * Recurring callbacks are specified to ignore exceptions (except if the exception
- * is thrown via RecurringCallbackAccess.throwException(), which is handled above).
- * We cannot even log the exception because that could lead to a StackOverflowError
- * (especially when the recurring callback failed with a StackOverflowError).
- */
- }
- }
-
- /**
- * We need to distinguish between arbitrary exceptions (must be swallowed) and exceptions
- * that are thrown via {@link RecurringCallbackAccess#throwException} (must be forwarded to
- * the application). When a recurring callback uses
- * {@link RecurringCallbackAccess#throwException}, we store the exception in a thread local
- * (to avoid allocations) and throw a pre-allocated marker exception instead. We catch the
- * marker exception internally, accesses the thread local, and rethrow that exception.
- */
- private static final class RecurringCallbackAccessImpl implements RecurringCallbackAccess {
- @Override
- public void throwException(Throwable t) {
- EXCEPTION_TL.set(t);
- throw SafepointException.SINGLETON;
- }
- }
- }
-
- private static final FastThreadLocalObject activeTimer = FastThreadLocalFactory.createObject(RecurringCallbackTimer.class, "ThreadingSupportImpl.activeTimer");
-
- private static final FastThreadLocalInt currentPauseDepth = FastThreadLocalFactory.createInt("ThreadingSupportImpl.currentPauseDepth");
-
- private static final String enableSupportOption = SubstrateOptionsParser.commandArgument(SupportRecurringCallback, "+");
+ private static final String ENABLE_SUPPORT_OPTION = SubstrateOptionsParser.commandArgument(RecurringCallbackSupport.ConcealedOptions.SupportRecurringCallback, "+");
/**
* Registers or removes a recurring callback for the current thread. Only one recurring callback
@@ -263,11 +50,11 @@ public void throwException(Throwable t) {
* will be overwritten.
*/
@Override
- public void registerRecurringCallback(long interval, TimeUnit unit, RecurringCallback callback) {
+ public void registerRecurringCallback(long interval, TimeUnit unit, Threading.RecurringCallback callback) {
IsolateThread thread = CurrentIsolate.getCurrentThread();
if (callback != null) {
- if (!SupportRecurringCallback.getValue()) {
- VMError.shouldNotReachHere("Recurring callbacks must be enabled during image build with option " + enableSupportOption);
+ if (!RecurringCallbackSupport.isEnabled()) {
+ throw new UnsupportedOperationException("Recurring callbacks must be enabled during image build with option " + ENABLE_SUPPORT_OPTION);
}
long intervalNanos = unit.toNanos(interval);
@@ -275,176 +62,28 @@ public void registerRecurringCallback(long interval, TimeUnit unit, RecurringCal
throw new IllegalArgumentException("The intervalNanos field is less than one.");
}
- RecurringCallbackTimer timer = createRecurringCallbackTimer(intervalNanos, callback);
- registerRecurringCallback0(thread, timer);
- } else {
- removeRecurringCallback(thread);
+ RecurringCallbackTimer callbackTimer = RecurringCallbackSupport.createCallbackTimer(intervalNanos, callback);
+ RecurringCallbackSupport.installCallback(thread, callbackTimer, true);
+ } else if (RecurringCallbackSupport.isEnabled()) {
+ RecurringCallbackSupport.uninstallCallback(thread);
}
}
- public static RecurringCallbackTimer createRecurringCallbackTimer(long intervalNanos, RecurringCallback callback) {
- assert callback != null;
- return new RecurringCallbackTimer(intervalNanos, callback);
- }
-
- @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
- private static void registerRecurringCallback0(IsolateThread thread, RecurringCallbackTimer timer) {
- removeRecurringCallback(thread);
- setRecurringCallback(thread, timer);
- }
-
- @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.")
- public static void setRecurringCallback(IsolateThread thread, RecurringCallbackTimer timer) {
- assert SupportRecurringCallback.getValue();
- assert timer.targetIntervalNanos > 0;
- assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
-
- activeTimer.set(thread, timer);
- SafepointCheckCounter.setVolatile(thread, timer.requestedChecks);
- }
-
- @Uninterruptible(reason = "Prevent VM operations that modify the recurring callbacks.", callerMustBe = true)
- public static RecurringCallback getRecurringCallback(IsolateThread thread) {
- assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
-
- RecurringCallbackTimer value = activeTimer.get(thread);
- if (value != null) {
- return value.callback;
- }
- return null;
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- public static void removeRecurringCallback(IsolateThread thread) {
- assert thread == CurrentIsolate.getCurrentThread() || VMOperation.isInProgressAtSafepoint();
-
- activeTimer.set(thread, null);
- }
-
- /**
- * Updates the statistics that are used to compute how frequently a thread needs to enter the
- * safepoint slowpath and executes the callback if necessary. This also resets the safepoint
- * requested counter so that some time can pass before this thread enters the safepoint slowpath
- * again.
- *
- * Note that the callback execution can trigger safepoints and throw exceptions, so it is NOT
- * uninterruptible.
- */
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- static void onSafepointCheckSlowpath() {
- assert StatusSupport.isStatusJava() : "must only be executed when the thread is in Java state";
- RecurringCallbackTimer timer = isRecurringCallbackSupported() ? activeTimer.get() : null;
- if (timer != null) {
- timer.evaluate();
- } else {
- SafepointCheckCounter.setVolatile(SafepointCheckCounter.MAX_VALUE);
- }
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- static boolean isRecurringCallbackRegistered(IsolateThread thread) {
- return isRecurringCallbackSupported() && activeTimer.get(thread) != null;
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- static boolean needsNativeToJavaSlowpath() {
- return isRecurringCallbackSupported() && Options.CheckRecurringCallbackOnNativeToJavaTransition.getValue() && activeTimer.get() != null && !isRecurringCallbackPaused();
- }
-
- /**
- * Recurring callbacks execute arbitrary code and may throw exceptions. In some code parts
- * (e.g., when executing VM operations), we can't deal with arbitrary code execution and
- * therefore need to pause the execution of recurring callbacks.
- */
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- public static void pauseRecurringCallback(@SuppressWarnings("unused") String reason) {
- if (!isRecurringCallbackSupported()) {
- return;
- }
-
- /*
- * Even if no callback is registered at the moment, we still need to execute the code below
- * because a callback could be registered while recurring callbacks are paused.
- */
- assert currentPauseDepth.get() >= 0;
- currentPauseDepth.set(currentPauseDepth.get() + 1);
- }
-
- /**
- * Resumes the execution of recurring callbacks. The callback execution might be triggered at
- * the next safepoint check.
- */
- @Uninterruptible(reason = "Must not contain safepoint checks.")
- public static void resumeRecurringCallbackAtNextSafepoint() {
- if (resumeCallbackExecution()) {
- RecurringCallbackTimer timer = activeTimer.get();
- assert timer != null;
- timer.updateStatistics();
- timer.setCounter(1);
- }
- }
-
- /**
- * Like {@link #resumeRecurringCallbackAtNextSafepoint()} but with the difference that this
- * method may trigger the execution of the recurring callback right away.
- */
- public static void resumeRecurringCallback() {
- if (resumeCallbackExecution()) {
- maybeExecuteRecurringCallback();
- }
- }
-
- @Uninterruptible(reason = "Prevent safepoints.")
- private static void maybeExecuteRecurringCallback() {
- try {
- onSafepointCheckSlowpath();
- } catch (SafepointException e) {
- /* Callers cannot declare `throws Throwable`. */
- throwUnchecked(RecurringCallbackTimer.getAndClearPendingException());
- }
- }
-
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- private static boolean resumeCallbackExecution() {
- if (!isRecurringCallbackSupported()) {
- return false;
- }
-
- assert currentPauseDepth.get() > 0;
- currentPauseDepth.set(currentPauseDepth.get() - 1);
- return !isRecurringCallbackPaused() && isRecurringCallbackRegistered(CurrentIsolate.getCurrentThread());
- }
-
- /**
- * Returns true if recurring callbacks are paused. Always returns false if recurring callbacks
- * are {@linkplain #isRecurringCallbackSupported() not supported}.
- */
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- public static boolean isRecurringCallbackPaused() {
- if (!isRecurringCallbackSupported()) {
- return false;
- }
- return currentPauseDepth.get() != 0;
- }
-
+ // GR-63737 only called from legacy code
@Fold
public static boolean isRecurringCallbackSupported() {
- return SupportRecurringCallback.getValue();
+ return RecurringCallbackSupport.isEnabled();
}
- @SuppressWarnings("unchecked")
- @Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
- private static void throwUnchecked(Throwable exception) throws T {
- throw (T) exception; // T is inferred as RuntimeException, but doesn't have to be
+ // GR-63737 only called from legacy code
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static void pauseRecurringCallback(String reason) {
+ RecurringCallbackSupport.suspendCallbackTimer(reason);
}
- static final class SafepointException extends RuntimeException {
- public static final SafepointException SINGLETON = new SafepointException();
-
- @Serial //
- private static final long serialVersionUID = 1L;
-
- private SafepointException() {
- }
+ // GR-63737 only called from legacy code
+ @Uninterruptible(reason = CALLED_FROM_UNINTERRUPTIBLE_CODE, mayBeInlined = true)
+ public static void resumeRecurringCallbackAtNextSafepoint() {
+ RecurringCallbackSupport.resumeCallbackTimerAtNextSafepointCheck();
}
}
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 710452c285f8..05a18c0bcdd3 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
@@ -378,7 +378,7 @@ public void start() {
@RestrictHeapAccess(access = RestrictHeapAccess.Access.NO_ALLOCATION, reason = "Must not allocate while acquiring / holding lock")
@Override
public void run() {
- ThreadingSupportImpl.pauseRecurringCallback("VM operation thread must not execute recurring callbacks.");
+ RecurringCallbackSupport.suspendCallbackTimer("VM operation thread must not execute recurring callbacks.");
this.isolateThread = CurrentIsolate.getCurrentThread();
VMOperationControl control = VMOperationControl.get();
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 c1047ba339b6..72be5bae76a5 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
@@ -69,6 +69,7 @@
import jdk.graal.compiler.api.directives.GraalDirectives;
import jdk.graal.compiler.api.replacements.Fold;
import jdk.graal.compiler.core.common.SuppressFBWarnings;
+import jdk.graal.compiler.nodes.PauseNode;
import jdk.graal.compiler.replacements.ReplacementsUtil;
import jdk.graal.compiler.replacements.nodes.AssertionNode;
import jdk.graal.compiler.word.Word;
@@ -214,6 +215,7 @@ public static boolean ensureInitialized() {
/* Already initialized, or some other thread claimed the initialization lock. */
while (initializationState.get() < STATE_INITIALIZED) {
/* Busy wait until the other thread finishes the initialization. */
+ PauseNode.pause();
}
}
return result;
@@ -341,7 +343,7 @@ protected int attachThread(IsolateThread thread) {
OSThreadHandleTL.set(thread, getCurrentOSThreadHandle());
/* Set initial safepoint counter value before making the thread visible. */
- assert !ThreadingSupportImpl.isRecurringCallbackRegistered(thread);
+ assert !RecurringCallbackSupport.isCallbackInstalled(thread);
SafepointCheckCounter.setVolatile(thread, SafepointCheckCounter.MAX_VALUE);
THREAD_MUTEX.lockNoTransition();
diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java
index 4a0c3a0822ef..a2806677b400 100644
--- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java
+++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java
@@ -53,7 +53,6 @@
import java.util.function.BooleanSupplier;
import java.util.function.Function;
-import com.oracle.svm.core.FutureDefaultsOptions;
import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.Pair;
import org.graalvm.nativeimage.ImageInfo;
@@ -121,6 +120,7 @@
import com.oracle.svm.core.BuildArtifacts;
import com.oracle.svm.core.BuildPhaseProvider;
import com.oracle.svm.core.ClassLoaderSupport;
+import com.oracle.svm.core.FutureDefaultsOptions;
import com.oracle.svm.core.JavaMainWrapper.JavaMainSupport;
import com.oracle.svm.core.LinkerInvocation;
import com.oracle.svm.core.MissingRegistrationSupport;
@@ -1086,6 +1086,7 @@ protected void setupNativeImage(String imageName, OptionValues options, Map feature.duringSetup(config));
}
+ BuildPhaseProvider.markSetupFinished();
if (ImageLayerBuildingSupport.buildingExtensionLayer()) {
Heap.getHeap().setStartOffset(HostedImageLayerBuildingSupport.singleton().getLoader().getImageHeapSize());