Skip to content

Commit ccbeb75

Browse files
authored
Merge pull request #9 from roberttoyonaga/bp-GR-73283
Backport [GR-73283] Fix JCMD Thread.dump_to_file
2 parents 6f1de28 + 126e6f9 commit ccbeb75

File tree

6 files changed

+260
-5
lines changed

6 files changed

+260
-5
lines changed

substratevm/mx.substratevm/mx_substratevm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ def _native_junit(native_image, unittest_args, build_args=None, run_args=None, b
672672
build_args.append("-D" + key + "=" + value)
673673

674674
build_args.append('--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED')
675+
build_args.append('--add-exports=java.base/jdk.internal.vm=ALL-UNNAMED')
675676
run_args = run_args or ['--verbose']
676677
junit_native_dir = join(svmbuild_dir(), platform_name(), 'junit')
677678
mx_util.ensure_dir_exists(junit_native_dir)

substratevm/mx.substratevm/suite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@
11311131
"requiresConcealed" : {
11321132
"java.base" : [
11331133
"jdk.internal.misc",
1134+
"jdk.internal.vm",
11341135
"sun.security.jca",
11351136
],
11361137
},
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2026, 2026, IBM Inc. All rights reserved.
4+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5+
*
6+
* This code is free software; you can redistribute it and/or modify it
7+
* under the terms of the GNU General Public License version 2 only, as
8+
* published by the Free Software Foundation. Oracle designates this
9+
* particular file as subject to the "Classpath" exception as provided
10+
* by Oracle in the LICENSE file that accompanied this code.
11+
*
12+
* This code is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15+
* version 2 for more details (a copy is included in the LICENSE file that
16+
* accompanied this code).
17+
*
18+
* You should have received a copy of the GNU General Public License version
19+
* 2 along with this work; if not, write to the Free Software Foundation,
20+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21+
*
22+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23+
* or visit www.oracle.com if you need additional information or have any
24+
* questions.
25+
*/
26+
27+
package com.oracle.svm.core.dcmd;
28+
29+
import java.lang.Thread.State;
30+
import java.util.concurrent.locks.LockSupport;
31+
32+
import com.oracle.svm.core.SubstrateUtil;
33+
import com.oracle.svm.core.annotate.Alias;
34+
import com.oracle.svm.core.annotate.Delete;
35+
import com.oracle.svm.core.annotate.Inject;
36+
import com.oracle.svm.core.annotate.Substitute;
37+
import com.oracle.svm.core.annotate.TargetClass;
38+
import com.oracle.svm.core.heap.VMOperationInfos;
39+
import com.oracle.svm.core.monitor.JavaMonitor;
40+
import com.oracle.svm.core.monitor.JavaMonitorQueuedSynchronizer;
41+
import com.oracle.svm.core.thread.JavaThreads;
42+
import com.oracle.svm.core.thread.JavaVMOperation;
43+
import com.oracle.svm.core.thread.Target_java_lang_VirtualThread;
44+
import com.oracle.svm.core.thread.VMOperation;
45+
import com.oracle.svm.core.util.BasedOnJDKFile;
46+
47+
@TargetClass(className = "jdk.internal.vm.ThreadSnapshot")
48+
final class Target_jdk_internal_vm_ThreadSnapshot {
49+
@Alias //
50+
String name;
51+
/** Replaces {@link #threadStatus}, to avoid unnecessary conversions. */
52+
@Inject //
53+
State state;
54+
@Delete //
55+
private int threadStatus;
56+
@Alias //
57+
Thread carrierThread;
58+
@Alias //
59+
StackTraceElement[] stackTrace;
60+
@Alias //
61+
int blockerTypeOrdinal;
62+
@Alias //
63+
Object blockerObject;
64+
65+
@Alias //
66+
Target_jdk_internal_vm_ThreadSnapshot() {
67+
}
68+
69+
@Substitute
70+
@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+26/src/java.base/share/native/libjava/ThreadSnapshot.c#L32-L36")
71+
@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+36/src/hotspot/share/prims/jvm.cpp#L2964-L2971")
72+
private static Target_jdk_internal_vm_ThreadSnapshot create(Thread thread) {
73+
return ThreadSnapshotUtil.create(thread);
74+
}
75+
76+
@Substitute
77+
State threadState() {
78+
return state;
79+
}
80+
}
81+
82+
@TargetClass(className = "jdk.internal.vm.ThreadSnapshot", innerClass = "BlockerLockType")
83+
final class Target_jdk_internal_vm_ThreadSnapshot_BlockerLockType {
84+
// Checkstyle: stop
85+
@Alias //
86+
static Target_jdk_internal_vm_ThreadSnapshot_BlockerLockType PARK_BLOCKER;
87+
// Checkstyle: resume
88+
}
89+
90+
final class ThreadSnapshotUtil {
91+
/**
92+
* At the moment, this method only computes the most relevant data, i.e., the blocker
93+
* information is incomplete and owned monitors are not supported. It is also slow because it
94+
* often needs a VM operation.
95+
*/
96+
@BasedOnJDKFile("https://github.com/openjdk/jdk/blob/jdk-25+36/src/hotspot/share/services/threadService.cpp#L1437-L1554")
97+
public static Target_jdk_internal_vm_ThreadSnapshot create(Thread thread) {
98+
if (thread == Thread.currentThread()) {
99+
/* No VM operation needed. */
100+
return createSnapshot(thread);
101+
}
102+
103+
/*
104+
* Enqueue a VM operation to get a consistent state. Could use a thread-local handshake in
105+
* the future (see GR-60270).
106+
*/
107+
CreateThreadSnapshotOperation op = new CreateThreadSnapshotOperation(thread);
108+
op.enqueue();
109+
return op.result;
110+
}
111+
112+
private static Target_jdk_internal_vm_ThreadSnapshot createSnapshot(Thread thread) {
113+
assert thread == Thread.currentThread() || VMOperation.isInProgressAtSafepoint();
114+
115+
Target_jdk_internal_vm_ThreadSnapshot snapshot = new Target_jdk_internal_vm_ThreadSnapshot();
116+
snapshot.stackTrace = thread.getStackTrace();
117+
snapshot.name = thread.getName();
118+
snapshot.state = thread.getState();
119+
120+
if (JavaThreads.isVirtual(thread)) {
121+
Target_java_lang_VirtualThread vthread = SubstrateUtil.cast(thread, Target_java_lang_VirtualThread.class);
122+
snapshot.carrierThread = JavaThreads.getVirtualThreadCarrier(vthread);
123+
}
124+
125+
/* Setting the blocker info in cases other than PARK_BLOCKER is non-trivial. */
126+
Object blocker = LockSupport.getBlocker(thread);
127+
if (blocker != null && !(blocker instanceof JavaMonitor) && !(blocker instanceof JavaMonitorQueuedSynchronizer.JavaMonitorConditionObject)) {
128+
snapshot.blockerTypeOrdinal = SubstrateUtil.cast(Target_jdk_internal_vm_ThreadSnapshot_BlockerLockType.PARK_BLOCKER, Enum.class).ordinal();
129+
snapshot.blockerObject = blocker;
130+
}
131+
132+
return snapshot;
133+
}
134+
135+
private static class CreateThreadSnapshotOperation extends JavaVMOperation {
136+
private final Thread thread;
137+
Target_jdk_internal_vm_ThreadSnapshot result;
138+
139+
CreateThreadSnapshotOperation(Thread thread) {
140+
super(VMOperationInfos.get(CreateThreadSnapshotOperation.class, "Create ThreadSnapshot", SystemEffect.SAFEPOINT));
141+
this.thread = thread;
142+
}
143+
144+
@Override
145+
protected void operate() {
146+
result = createSnapshot(thread);
147+
}
148+
}
149+
}

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/monitor/JavaMonitorQueuedSynchronizer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
* </ul>
6161
*/
6262
@BasedOnJDKClass(AbstractQueuedLongSynchronizer.class)
63-
abstract class JavaMonitorQueuedSynchronizer {
63+
public abstract class JavaMonitorQueuedSynchronizer {
6464
// Node status bits, also used as argument and return values
6565
static final int WAITING = 1; // must be 1
6666
static final int CANCELLED = 0x80000000; // must be negative

substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/thread/JavaThreads.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ public static boolean isCurrentThreadVirtualAndPinned() {
214214
return carrier != null && carrier.vthread != null && Target_jdk_internal_vm_Continuation.isPinned(carrier.cont.getScope());
215215
}
216216

217+
/**
218+
* Returns the carrier thread. Note that this method may only be called for the current thread
219+
* or during a VM operation. Otherwise, the result could be stale.
220+
*/
221+
public static Thread getVirtualThreadCarrier(Target_java_lang_VirtualThread thread) {
222+
assert SubstrateUtil.cast(thread, Thread.class) == Thread.currentThread() || VMOperation.isInProgressAtSafepoint() : "otherwise, this information could change at any time";
223+
return thread.carrierThread;
224+
}
225+
217226
@SuppressFBWarnings(value = "BC", justification = "Cast for @TargetClass")
218227
static Target_java_lang_ThreadGroup toTarget(ThreadGroup threadGroup) {
219228
return Target_java_lang_ThreadGroup.class.cast(threadGroup);

substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jcmd/JCmdTest.java

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
2-
* Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
33
* Copyright (c) 2024, 2024, Red Hat Inc. All rights reserved.
4+
* Copyright (c) 2026, 2026, IBM Inc. All rights reserved.
45
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
56
*
67
* This code is free software; you can redistribute it and/or modify it
@@ -39,7 +40,9 @@
3940
import java.util.ArrayList;
4041
import java.util.Arrays;
4142
import java.util.List;
43+
import java.util.concurrent.CountDownLatch;
4244
import java.util.concurrent.atomic.AtomicReference;
45+
import java.util.concurrent.locks.LockSupport;
4346

4447
import org.graalvm.nativeimage.Platform;
4548
import org.junit.BeforeClass;
@@ -49,7 +52,7 @@
4952

5053
public class JCmdTest {
5154
@BeforeClass
52-
public static void checkForJFR() {
55+
public static void checkForJcmd() {
5356
assumeTrue("skipping JCmd tests", VMInspectionOptions.hasJCmdSupport());
5457
}
5558

@@ -76,8 +79,7 @@ public void testBadSocketFile() throws IOException, InterruptedException {
7679
checkJCmdConnection();
7780

7881
/* Delete the socket file. */
79-
String tempDir = System.getProperty("java.io.tmpdir");
80-
Path attachFile = Paths.get(tempDir, ".java_pid" + ProcessHandle.current().pid());
82+
Path attachFile = getTempFilePath(".java_pid", "");
8183
boolean deletedSocketFile = Files.deleteIfExists(attachFile);
8284
assertTrue(deletedSocketFile);
8385

@@ -128,6 +130,91 @@ public void testJfr() throws IOException, InterruptedException {
128130
assertOutputContainsLines(jcmd, "Stopped recording \"JCmdTest\".");
129131
}
130132

133+
@Test
134+
public void testThreadPrint() throws IOException, InterruptedException {
135+
Process jcmd = runJCmd("Thread.print");
136+
assertOutputContainsStrings(jcmd, "Threads dumped.");
137+
}
138+
139+
@Test
140+
public void testThreadDumpToTextFile() throws IOException, InterruptedException {
141+
BlockerHelper blocker = new BlockerHelper();
142+
CountDownLatch parked = new CountDownLatch(1);
143+
144+
Thread parkedThread = new Thread(() -> {
145+
parked.countDown();
146+
LockSupport.park(blocker);
147+
}, "test-park-blocker-thread");
148+
149+
parkedThread.start();
150+
151+
/* Wait until parkedThread is parked. */
152+
parked.await();
153+
while (!parkedThread.getState().equals(Thread.State.WAITING)) {
154+
Thread.sleep(10);
155+
}
156+
157+
Path textFile = getTempFilePath("test_thread_dump", ".txt");
158+
Process jcmd = runJCmd("Thread.dump_to_file", textFile.toString());
159+
assertOutputContainsStrings(jcmd, "Created");
160+
assertTrue(Files.size(textFile) > 0);
161+
checkThreadDump(textFile);
162+
assertTrue(Files.deleteIfExists(textFile));
163+
164+
/* Unpark the thread and wait until it terminates. */
165+
LockSupport.unpark(parkedThread);
166+
parkedThread.join();
167+
}
168+
169+
private static void checkThreadDump(Path textFile) throws IOException {
170+
String dump = Files.readString(textFile);
171+
assertTrue("Dump should contain the blocked thread's name", dump.contains("test-park-blocker-thread"));
172+
assertTrue("Dump should contain the thread's state", dump.contains("WAITING"));
173+
assertTrue("Dump should contain text 'parking to wait for'", dump.contains("parking to wait for"));
174+
assertTrue("Dump should report the blocker object", dump.contains(BlockerHelper.class.getSimpleName()));
175+
assertTrue("Dump should report the process ID", dump.contains(String.valueOf(ProcessHandle.current().pid())));
176+
assertTrue("Dump should report at least some stacktrace lines", dump.contains(" at "));
177+
}
178+
179+
@Test
180+
public void testThreadDumpToJsonFile() throws IOException, InterruptedException {
181+
Path jsonFile = getTempFilePath("test_thread_dump", ".json");
182+
Process jcmd = runJCmd("Thread.dump_to_file", "-format=json", jsonFile.toString());
183+
assertOutputContainsStrings(jcmd, "Created");
184+
assertTrue(Files.size(jsonFile) > 0);
185+
assertTrue(Files.deleteIfExists(jsonFile));
186+
}
187+
188+
@Test
189+
public void testVM() throws IOException, InterruptedException {
190+
Process jcmd = runJCmd("VM.command_line");
191+
assertOutputContainsStrings(jcmd, "VM Arguments:", "java_command:");
192+
193+
jcmd = runJCmd("VM.native_memory");
194+
assertOutputContainsStrings(jcmd, "Native memory tracking");
195+
196+
jcmd = runJCmd("VM.system_properties");
197+
assertOutputContainsStrings(jcmd, ":");
198+
199+
jcmd = runJCmd("VM.uptime");
200+
assertOutputContainsStrings(jcmd, ":");
201+
202+
jcmd = runJCmd("VM.version");
203+
assertOutputContainsStrings(jcmd, "GraalVM");
204+
}
205+
206+
@Test
207+
public void testGC() throws IOException, InterruptedException {
208+
Path dumpFile = getTempFilePath("heap_dump", ".hprof");
209+
Process jcmd = runJCmd("GC.heap_dump", dumpFile.toString());
210+
assertOutputContainsStrings(jcmd, "Dumped to:");
211+
assertTrue(Files.size(dumpFile) > 0);
212+
assertTrue(Files.deleteIfExists(dumpFile));
213+
214+
jcmd = runJCmd("GC.run");
215+
assertOutputContainsStrings(jcmd, "Command executed successfully");
216+
}
217+
131218
private static void checkJCmdConnection() throws IOException, InterruptedException {
132219
Process jcmd = runJCmd("help");
133220
assertOutputContainsLines(jcmd, "help");
@@ -175,4 +262,12 @@ private static String[] getOutput(Process process) throws InterruptedException {
175262
}
176263
return lines;
177264
}
265+
266+
private static Path getTempFilePath(String prefix, String suffix) {
267+
String tempDir = System.getProperty("java.io.tmpdir");
268+
return Paths.get(tempDir, prefix + ProcessHandle.current().pid() + suffix);
269+
}
270+
271+
private static final class BlockerHelper {
272+
}
178273
}

0 commit comments

Comments
 (0)