From a51a0bf57feaae0862fd7f3dbf305883d49781a0 Mon Sep 17 00:00:00 2001 From: Jorn Vernee Date: Tue, 4 Nov 2025 15:40:40 +0000 Subject: [PATCH] 8370344: Arbitrary Java frames on stack during scoped access Reviewed-by: pchilanomate, dholmes, liach --- .../share/prims/scopedMemoryAccess.cpp | 55 +++++--- src/hotspot/share/runtime/handshake.cpp | 2 + src/hotspot/share/runtime/javaThread.cpp | 1 + src/hotspot/share/runtime/javaThread.hpp | 18 +++ .../sharedclosejfr/TestSharedCloseJFR.java | 123 ++++++++++++++++ .../foreign/sharedclosejfr/sharedCloseJfr.jfc | 7 + .../TestSharedCloseJvmti.java | 130 +++++++++++++++++ .../sharedclosejvmti/libSharedCloseAgent.cpp | 133 ++++++++++++++++++ .../lang/foreign/SharedCloseStackWalk.java | 92 ++++++++++++ 9 files changed, 545 insertions(+), 16 deletions(-) create mode 100644 test/jdk/java/foreign/sharedclosejfr/TestSharedCloseJFR.java create mode 100644 test/jdk/java/foreign/sharedclosejfr/sharedCloseJfr.jfc create mode 100644 test/jdk/java/foreign/sharedclosejvmti/TestSharedCloseJvmti.java create mode 100644 test/jdk/java/foreign/sharedclosejvmti/libSharedCloseAgent.cpp create mode 100644 test/micro/org/openjdk/bench/java/lang/foreign/SharedCloseStackWalk.java diff --git a/src/hotspot/share/prims/scopedMemoryAccess.cpp b/src/hotspot/share/prims/scopedMemoryAccess.cpp index c1d1b8cd8c0..26ae5bc03f5 100644 --- a/src/hotspot/share/prims/scopedMemoryAccess.cpp +++ b/src/hotspot/share/prims/scopedMemoryAccess.cpp @@ -22,9 +22,11 @@ * */ +#include "classfile/moduleEntry.hpp" #include "classfile/vmSymbols.hpp" #include "jni.h" #include "jvm.h" +#include "jvmtifiles/jvmtiEnv.hpp" #include "logging/logStream.hpp" #include "oops/access.inline.hpp" #include "oops/oop.inline.hpp" @@ -36,7 +38,7 @@ #include "runtime/vframe.inline.hpp" template -static bool for_scoped_method(JavaThread* jt, const Func& func) { +static void for_scoped_methods(JavaThread* jt, bool agents_loaded, const Func& func) { ResourceMark rm; #ifdef ASSERT LogMessage(foreign) msg; @@ -44,12 +46,26 @@ static bool for_scoped_method(JavaThread* jt, const Func& func) { if (ls.is_enabled()) { ls.print_cr("Walking thread: %s", jt->name()); } + + bool would_have_bailed = false; #endif - const int max_critical_stack_depth = 10; - int depth = 0; for (vframeStream stream(jt); !stream.at_end(); stream.next()) { Method* m = stream.method(); + + if (!agents_loaded && + (m->method_holder()->module()->name() != vmSymbols::java_base())) { + // Stop walking if we see a frame outside of java.base. + + // If any JVMTI agents are loaded, we also have to keep walking, since + // agents can add arbitrary Java frames to the stack inside a @Scoped method. +#ifndef ASSERT + return; +#else + would_have_bailed = true; +#endif + } + bool is_scoped = m->is_scoped(); #ifdef ASSERT @@ -60,36 +76,43 @@ static bool for_scoped_method(JavaThread* jt, const Func& func) { #endif if (is_scoped) { - assert(depth < max_critical_stack_depth, "can't have more than %d critical frames", max_critical_stack_depth); - return func(stream); + assert(!would_have_bailed, "would have missed scoped method on release build"); + bool done = func(stream); + if (done || !agents_loaded) { + // We may also have to keep walking after finding a @Scoped method, + // since there may be multiple @Scoped methods active on the stack + // if a JVMTI agent callback runs during a scoped access and calls + // back into Java code that then itself does a scoped access. + return; + } } - depth++; - -#ifndef ASSERT - // On debug builds, just keep searching the stack - // in case we missed an @Scoped method further up - if (depth >= max_critical_stack_depth) { - break; - } -#endif } - return false; } static bool is_accessing_session(JavaThread* jt, oop session, bool& in_scoped) { - return for_scoped_method(jt, [&](vframeStream& stream){ + bool agents_loaded = JvmtiEnv::environments_might_exist(); + if (!agents_loaded && jt->is_throwing_unsafe_access_error()) { + // Ignore this thread. It is in the process of throwing another exception + // already. + return false; + } + + bool is_accessing_session = false; + for_scoped_methods(jt, agents_loaded, [&](vframeStream& stream){ in_scoped = true; StackValueCollection* locals = stream.asJavaVFrame()->locals(); for (int i = 0; i < locals->size(); i++) { StackValue* var = locals->at(i); if (var->type() == T_OBJECT) { if (var->get_obj() == session) { + is_accessing_session = true; return true; } } } return false; }); + return is_accessing_session; } static frame get_last_frame(JavaThread* jt) { diff --git a/src/hotspot/share/runtime/handshake.cpp b/src/hotspot/share/runtime/handshake.cpp index e9a00c2a3bc..f468f27e2c0 100644 --- a/src/hotspot/share/runtime/handshake.cpp +++ b/src/hotspot/share/runtime/handshake.cpp @@ -722,6 +722,8 @@ void HandshakeState::handle_unsafe_access_error() { MutexUnlocker ml(&_lock, Mutex::_no_safepoint_check_flag); // We may be at method entry which requires we save the do-not-unlock flag. UnlockFlagSaver fs(_handshakee); + // Tell code inspecting handshakee's stack what we are doing + ThrowingUnsafeAccessError tuae(_handshakee); Handle h_exception = Exceptions::new_exception(_handshakee, vmSymbols::java_lang_InternalError(), "a fault occurred in an unsafe memory access operation"); if (h_exception()->is_a(vmClasses::InternalError_klass())) { java_lang_InternalError::set_during_unsafe_access(h_exception()); diff --git a/src/hotspot/share/runtime/javaThread.cpp b/src/hotspot/share/runtime/javaThread.cpp index 36544cf1118..b9b4237e7ee 100644 --- a/src/hotspot/share/runtime/javaThread.cpp +++ b/src/hotspot/share/runtime/javaThread.cpp @@ -444,6 +444,7 @@ JavaThread::JavaThread(MemTag mem_tag) : _terminated(_not_terminated), _in_deopt_handler(0), _doing_unsafe_access(false), + _throwing_unsafe_access_error(false), _do_not_unlock_if_synchronized(false), #if INCLUDE_JVMTI _carrier_thread_suspended(false), diff --git a/src/hotspot/share/runtime/javaThread.hpp b/src/hotspot/share/runtime/javaThread.hpp index a6a00bfbd03..c0b25384fac 100644 --- a/src/hotspot/share/runtime/javaThread.hpp +++ b/src/hotspot/share/runtime/javaThread.hpp @@ -317,6 +317,7 @@ class JavaThread: public Thread { jint _in_deopt_handler; // count of deoptimization // handlers thread is in volatile bool _doing_unsafe_access; // Thread may fault due to unsafe access + volatile bool _throwing_unsafe_access_error; // Thread has faulted and is throwing an exception bool _do_not_unlock_if_synchronized; // Do not unlock the receiver of a synchronized method (since it was // never locked) when throwing an exception. Used by interpreter only. #if INCLUDE_JVMTI @@ -622,6 +623,9 @@ private: bool doing_unsafe_access() { return _doing_unsafe_access; } void set_doing_unsafe_access(bool val) { _doing_unsafe_access = val; } + bool is_throwing_unsafe_access_error() { return _throwing_unsafe_access_error; } + void set_throwing_unsafe_access_error(bool val) { _throwing_unsafe_access_error = val; } + bool do_not_unlock_if_synchronized() { return _do_not_unlock_if_synchronized; } void set_do_not_unlock_if_synchronized(bool val) { _do_not_unlock_if_synchronized = val; } @@ -1354,4 +1358,18 @@ class ThreadInClassInitializer : public StackObj { } }; +class ThrowingUnsafeAccessError : public StackObj { + JavaThread* _thread; + bool _prev; +public: + ThrowingUnsafeAccessError(JavaThread* thread) : + _thread(thread), + _prev(thread->is_throwing_unsafe_access_error()) { + _thread->set_throwing_unsafe_access_error(true); + } + ~ThrowingUnsafeAccessError() { + _thread->set_throwing_unsafe_access_error(_prev); + } +}; + #endif // SHARE_RUNTIME_JAVATHREAD_HPP diff --git a/test/jdk/java/foreign/sharedclosejfr/TestSharedCloseJFR.java b/test/jdk/java/foreign/sharedclosejfr/TestSharedCloseJFR.java new file mode 100644 index 00000000000..b59373b5b4e --- /dev/null +++ b/test/jdk/java/foreign/sharedclosejfr/TestSharedCloseJFR.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 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. + * + * 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. + */ + +/* + * @test + * @bug 8370344 + * @requires os.family != "windows" + * @requires vm.flavor != "zero" + * @requires vm.hasJFR + * @summary Test closing a shared scope during faulting access + * + * @library /test/lib + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main jdk.test.lib.FileInstaller sharedCloseJfr.jfc sharedCloseJfr.jfc + * @run main/othervm + * -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * -XX:StartFlightRecording:filename=recording.jfr,dumponexit=true,settings=sharedCloseJfr.jfc + * TestSharedCloseJFR + */ + +import jdk.test.whitebox.WhiteBox; + +import java.io.RandomAccessFile; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +// We are interested in the following scenario: +// When accessing a memory-mapped file that is truncated +// a segmentation fault will occur (see also test/hotspot/jtreg/runtime/Unsafe/InternalErrorTest.java) +// +// This segmentation fault will be caught in the VM's signal handler +// and get turned into an InternalError by a VM handshake operation. +// This handshake operation calls back into Java to the constructor +// of InternalError. This constructor calls super constructors until +// it ends up in the constructor of Throwable, where JFR starts logging +// the Throwable being created. This logging code adds a bunch +// of extra Java frames to the stack. +// +// All of this occurs during the original memory access, i.e. +// while we are inside a @Scoped method call (jdk.internal.misc.ScopedMemoryAccess). +// If at this point a shared arena is closed in another thread, +// the shared scope closure handshake (src/hotspot/share/prims/scopedMemoryAccess.cpp) +// will see all the extra frames added by JFR and the InternalError constructor, +// while walking the stack of the thread doing the faulting access. +// +// This test is here to make sure that the shared scope closure handshake can +// deal with that situation. +public class TestSharedCloseJFR { + + private static final int PAGE_SIZE = WhiteBox.getWhiteBox().getVMPageSize(); + + public static void main(String[] args) throws Throwable { + String fileName = "tmp.txt"; + Path path = Path.of(fileName); + AtomicBoolean stop = new AtomicBoolean(); + + Files.write(path, "1".repeat(PAGE_SIZE + 1000).getBytes()); + try (RandomAccessFile file = new RandomAccessFile(fileName, "rw")) { + FileChannel fileChannel = file.getChannel(); + MemorySegment segment = + fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size(), Arena.ofAuto()); + // truncate file + // this will make the access fault + Files.write(path, "2".getBytes()); + + // start worker thread + CountDownLatch latch = new CountDownLatch(1); + Thread.ofPlatform().start(() -> { + latch.countDown(); + while (!stop.get()) { + Arena.ofShared().close(); // hammer VM with handshakes + } + }); + + // wait util the worker thread has started + latch.await(); + + // access (should fault) + // try it a few times until we get a handshake during JFR reporting + for (int i = 0; i < 50_000; i++) { + try { + segment.get(ValueLayout.JAVA_INT, PAGE_SIZE); + throw new RuntimeException("InternalError was expected"); + } catch (InternalError e) { + // InternalError as expected + if (!e.getMessage().contains("a fault occurred in an unsafe memory access")) { + throw new RuntimeException("Unexpected exception", e); + } + } + } + } finally { + // stop worker + stop.set(true); + } + } +} diff --git a/test/jdk/java/foreign/sharedclosejfr/sharedCloseJfr.jfc b/test/jdk/java/foreign/sharedclosejfr/sharedCloseJfr.jfc new file mode 100644 index 00000000000..13f3406a5de --- /dev/null +++ b/test/jdk/java/foreign/sharedclosejfr/sharedCloseJfr.jfc @@ -0,0 +1,7 @@ + + + + true + true + + diff --git a/test/jdk/java/foreign/sharedclosejvmti/TestSharedCloseJvmti.java b/test/jdk/java/foreign/sharedclosejvmti/TestSharedCloseJvmti.java new file mode 100644 index 00000000000..b7106da24e8 --- /dev/null +++ b/test/jdk/java/foreign/sharedclosejvmti/TestSharedCloseJvmti.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 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. + * + * 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. + */ + +/* + * @test + * @bug 8370344 + * @library /test/lib + * @run junit/native TestSharedCloseJvmti + */ + +import jdk.test.lib.Utils; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import org.junit.jupiter.api.Test; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class TestSharedCloseJvmti { + + private static final String JVMTI_AGENT_LIB = Path.of(Utils.TEST_NATIVE_PATH, System.mapLibraryName("SharedCloseAgent")) + .toAbsolutePath().toString(); + + @Test + void eventDuringScopedAccess() throws Throwable { + List command = new ArrayList<>(List.of( + "-agentpath:" + JVMTI_AGENT_LIB, + "-Xcheck:jni", + EventDuringScopedAccessRunner.class.getName() + )); + + try { + ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(command); + Process process = ProcessTools.startProcess("fork", pb, null, null, 1L, TimeUnit.MINUTES); + OutputAnalyzer output = new OutputAnalyzer(process); + output.shouldHaveExitValue(0); + output.stderrShouldContain("Exception in thread \"Trigger\" jdk.internal.misc.ScopedMemoryAccess$ScopedAccessError: Invalid memory access"); + } catch (TimeoutException e) { + throw new RuntimeException("Timeout while waiting for forked process"); + } + } + + public static class EventDuringScopedAccessRunner { + static final int ADDED_FRAMES = 10; + + static final CountDownLatch MAIN_LATCH = new CountDownLatch(1); + static final CountDownLatch TARGET_LATCH = new CountDownLatch(1); + static final MemorySegment OTHER_SEGMENT = Arena.global().allocate(4); + + static volatile int SINK; + + public static void main(String[] args) throws Throwable { + try (Arena arena = Arena.ofShared()) { + MemorySegment segment = arena.allocate(4); + // run in separate thread so that waiting on + // latch doesn't block main thread + Thread.ofPlatform().name("Trigger").start(() -> { + SINK = segment.get(ValueLayout.JAVA_INT, 0); + }); + // wait until trigger thread is in JVMTI event callback + MAIN_LATCH.await(); + } + // Notify trigger thread that arena was closed + TARGET_LATCH.countDown(); + } + + static boolean reentrant = false; + + // called by jvmti agent + // we get here after checking arena liveness + private static void target() { + String callerName = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk(frames -> + frames.skip(2).findFirst().orElseThrow().getClassName()); + if (!callerName.equals("jdk.internal.misc.ScopedMemoryAccess")) { + return; + } + + if (reentrant) { + // put some frames on the stack, so stack walk does not see @Scoped method + addFrames(0); + } else { + reentrant = true; + SINK = OTHER_SEGMENT.get(ValueLayout.JAVA_INT, 0); + reentrant = false; + } + } + + private static void addFrames(int depth) { + if (depth >= ADDED_FRAMES) { + // notify main thread to close the arena + MAIN_LATCH.countDown(); + try { + // wait here until main thread has closed arena + TARGET_LATCH.await(); + } catch (InterruptedException ex) { + throw new RuntimeException("Unexpected interruption"); + } + return; + } + addFrames(depth + 1); + } + } +} diff --git a/test/jdk/java/foreign/sharedclosejvmti/libSharedCloseAgent.cpp b/test/jdk/java/foreign/sharedclosejvmti/libSharedCloseAgent.cpp new file mode 100644 index 00000000000..6406c46f61c --- /dev/null +++ b/test/jdk/java/foreign/sharedclosejvmti/libSharedCloseAgent.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (c) 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. + * + * 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. + */ + +#include + +#include + +static jclass MAIN_CLS; +static jmethodID TARGET_ID; + +static const char* TARGET_CLASS_NAME = "TestSharedCloseJvmti$EventDuringScopedAccessRunner"; +static const char* TARGET_METHOD_NAME = "target"; +static const char* TARGET_METHOD_SIG = "()V"; + +static const char* INTERCEPT_CLASS_NAME = "Ljdk/internal/foreign/MemorySessionImpl;"; +static const char* INTERCEPT_METHOD_NAME = "checkValidStateRaw"; + +void start(jvmtiEnv *jvmti_env, JNIEnv* jni_env) { + + jclass cls = jni_env->FindClass(TARGET_CLASS_NAME); + if (cls == nullptr) { + jni_env->ExceptionDescribe(); + return; + } + + MAIN_CLS = (jclass) jni_env->NewGlobalRef(cls); + + TARGET_ID = jni_env->GetStaticMethodID(cls, TARGET_METHOD_NAME, TARGET_METHOD_SIG); + if (TARGET_ID == nullptr) { + jni_env->ExceptionDescribe(); + return; + } +} + +void method_exit(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, + jboolean was_popped_by_exception, jvalue return_value) { + char* method_name = nullptr; + jvmtiError err = jvmti_env->GetMethodName(method, &method_name, nullptr, nullptr); + if (err != JVMTI_ERROR_NONE) { + return; + } + + if (strcmp(method_name, INTERCEPT_METHOD_NAME) != 0) { + jvmti_env->Deallocate((unsigned char*) method_name); + return; + } + + jclass cls; + err = jvmti_env->GetMethodDeclaringClass(method, &cls); + if (err != JVMTI_ERROR_NONE) { + jvmti_env->Deallocate((unsigned char*) method_name); + return; + } + + char* class_sig = nullptr; + err = jvmti_env->GetClassSignature(cls, &class_sig, nullptr); + if (err != JVMTI_ERROR_NONE) { + jvmti_env->Deallocate((unsigned char*) method_name); + return; + } + + if (strcmp(class_sig, INTERCEPT_CLASS_NAME) != 0) { + jvmti_env->Deallocate((unsigned char*) method_name); + jvmti_env->Deallocate((unsigned char*) class_sig); + return; + } + + jni_env->CallStaticVoidMethod(MAIN_CLS, TARGET_ID); + if (jni_env->ExceptionOccurred()) { + jni_env->ExceptionDescribe(); + } + + jvmti_env->Deallocate((unsigned char*) method_name); + jvmti_env->Deallocate((unsigned char*) class_sig); +} + +JNIEXPORT jint JNICALL +Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { + jvmtiEnv* env; + jint jni_err = vm->GetEnv((void**) &env, JVMTI_VERSION); + if (jni_err != JNI_OK) { + return jni_err; + } + + jvmtiCapabilities capabilities{}; + capabilities.can_generate_method_exit_events = 1; + + jvmtiError err = env->AddCapabilities(&capabilities); + if (err != JVMTI_ERROR_NONE) { + return err; + } + + jvmtiEventCallbacks callbacks; + callbacks.VMStart = start; + callbacks.MethodExit = method_exit; + + err = env->SetEventCallbacks(&callbacks, (jint) sizeof(callbacks)); + if (err != JVMTI_ERROR_NONE) { + return err; + } + + err = env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, nullptr); + if (err != JVMTI_ERROR_NONE) { + return err; + } + + err = env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_START, nullptr); + if (err != JVMTI_ERROR_NONE) { + return err; + } + + return 0; +} diff --git a/test/micro/org/openjdk/bench/java/lang/foreign/SharedCloseStackWalk.java b/test/micro/org/openjdk/bench/java/lang/foreign/SharedCloseStackWalk.java new file mode 100644 index 00000000000..ead4bcb5089 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/foreign/SharedCloseStackWalk.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 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. + * + * 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 org.openjdk.bench.java.lang.foreign; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.foreign.Arena; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS) +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Fork(value = 1, jvmArgs = { "--enable-native-access=ALL-UNNAMED", "-Djava.library.path=micro/native" }) +public class SharedCloseStackWalk { + + @Param({"1", "10", "100"}) + int numOtherThread; + + @Param({"10", "100", "1000"}) + int extraFrames; + + @Param({"false", "true"}) + boolean virtualThreads; + + private CountDownLatch stop; + + @Setup + public void setup() { + stop = new CountDownLatch(1); + for (int i = 0; i < numOtherThread; i++) { + (virtualThreads + ? Thread.ofVirtual() + : Thread.ofPlatform()).start(() -> recurse(0)); + } + } + + @TearDown + public void teardown() { + stop.countDown(); + } + + @Benchmark + public void sharedOpenClose() { + Arena.ofShared().close(); + } + + private void recurse(int depth) { + if (depth == extraFrames) { + try { + stop.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Don't interrupt me!", e); + } + } else { + recurse(depth + 1); + } + } +}