742 lines
27 KiB
Java
742 lines
27 KiB
Java
/*
|
|
* Copyright (c) 2021, 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 8284161 8287008 8309406 8356870
|
|
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
|
|
* @requires vm.continuations
|
|
* @modules java.base/jdk.internal.vm jdk.management
|
|
* @library /test/lib
|
|
* @build jdk.test.whitebox.WhiteBox
|
|
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
|
|
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
|
* --enable-native-access=ALL-UNNAMED DumpThreads
|
|
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
|
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads DumpThreads
|
|
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
|
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=true DumpThreads
|
|
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
|
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=false DumpThreads
|
|
*/
|
|
|
|
import java.lang.management.ManagementFactory;
|
|
import java.lang.reflect.Method;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.FileAlreadyExistsException;
|
|
import java.nio.file.Path;
|
|
import java.time.ZonedDateTime;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.ForkJoinPool;
|
|
import java.util.concurrent.ForkJoinWorkerThread;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.concurrent.locks.LockSupport;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
import java.util.regex.Pattern;
|
|
import java.util.regex.Matcher;
|
|
import com.sun.management.HotSpotDiagnosticMXBean;
|
|
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
|
|
import jdk.test.lib.threaddump.ThreadDump;
|
|
import jdk.test.lib.thread.VThreadPinner;
|
|
import jdk.test.lib.thread.VThreadRunner;
|
|
import jdk.test.whitebox.WhiteBox;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.MethodSource;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.junit.jupiter.api.Assumptions.*;
|
|
|
|
class DumpThreads {
|
|
private static boolean trackAllThreads;
|
|
|
|
@BeforeAll
|
|
static void setup() throws Exception {
|
|
String s = System.getProperty("jdk.trackAllThreads");
|
|
trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
|
|
|
|
// need >=2 carriers for testing pinning
|
|
VThreadRunner.ensureParallelism(2);
|
|
}
|
|
|
|
/**
|
|
* Test thread dump in plain text format.
|
|
*/
|
|
@Test
|
|
void testPlainText() throws Exception {
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
|
|
// pid should be on the first line
|
|
String pid = Long.toString(ProcessHandle.current().pid());
|
|
assertEquals(pid, lines.get(0));
|
|
|
|
// timestamp should be on the second line
|
|
String secondLine = lines.get(1);
|
|
ZonedDateTime.parse(secondLine);
|
|
|
|
// runtime version should be on third line
|
|
String vs = Runtime.version().toString();
|
|
assertEquals(vs, lines.get(2));
|
|
|
|
// dump should include current thread
|
|
Thread currentThread = Thread.currentThread();
|
|
if (trackAllThreads || !currentThread.isVirtual()) {
|
|
ThreadFields fields = findThread(currentThread.threadId(), lines);
|
|
assertNotNull(fields, "current thread not found");
|
|
assertEquals(currentThread.getName(), fields.name());
|
|
assertEquals(currentThread.isVirtual(), fields.isVirtual());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test thread dump in JSON format.
|
|
*/
|
|
@Test
|
|
void testJsonFormat() throws Exception {
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
|
|
// dump should include current thread in the root container
|
|
Thread currentThread = Thread.currentThread();
|
|
if (trackAllThreads || !currentThread.isVirtual()) {
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(currentThread.threadId())
|
|
.orElse(null);
|
|
assertNotNull(ti, "current thread not found");
|
|
assertEquals(currentThread.isVirtual(), ti.isVirtual());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ExecutorService implementations that have their object identity in the container
|
|
* name so they can be found in the JSON format.
|
|
*/
|
|
static Stream<ExecutorService> executors() {
|
|
return Stream.of(
|
|
Executors.newFixedThreadPool(1),
|
|
Executors.newVirtualThreadPerTaskExecutor()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test that a thread container for an executor service is in the JSON format thread dump.
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("executors")
|
|
void testThreadContainer(ExecutorService executor) throws Exception {
|
|
try (executor) {
|
|
testThreadContainer(executor, Objects.toIdentityString(executor));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test that a thread container for the common pool is in the JSON format thread dump.
|
|
*/
|
|
@Test
|
|
void testCommonPool() throws Exception {
|
|
testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
|
|
}
|
|
|
|
/**
|
|
* Test that the JSON thread dump has a thread container for the given executor.
|
|
*/
|
|
private void testThreadContainer(ExecutorService executor, String name) throws Exception {
|
|
var threadRef = new AtomicReference<Thread>();
|
|
|
|
executor.submit(() -> {
|
|
threadRef.set(Thread.currentThread());
|
|
LockSupport.park();
|
|
});
|
|
|
|
// capture Thread
|
|
Thread thread;
|
|
while ((thread = threadRef.get()) == null) {
|
|
Thread.sleep(20);
|
|
}
|
|
|
|
try {
|
|
// dump threads to file and parse as JSON object
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
|
|
// find the thread container corresponding to the executor
|
|
var container = threadDump.findThreadContainer(name).orElse(null);
|
|
assertNotNull(container, name + " not found");
|
|
assertFalse(container.owner().isPresent());
|
|
var parent = container.parent().orElse(null);
|
|
assertEquals(threadDump.rootThreadContainer(), parent);
|
|
|
|
// find the thread in the thread container
|
|
ThreadDump.ThreadInfo ti = container.findThread(thread.threadId()).orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
|
|
} finally {
|
|
LockSupport.unpark(thread);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ThreadFactory implementations for tests.
|
|
*/
|
|
static Stream<ThreadFactory> threadFactories() {
|
|
Stream<ThreadFactory> s = Stream.of(Thread.ofPlatform().factory());
|
|
if (trackAllThreads) {
|
|
return Stream.concat(s, Stream.of(Thread.ofVirtual().factory()));
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread blocked on monitor enter.
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("threadFactories")
|
|
void testBlockedThread(ThreadFactory factory) throws Exception {
|
|
testBlockedThread(factory, false);
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread blocked on monitor enter when pinned.
|
|
*/
|
|
@Test
|
|
void testBlockedThreadWhenPinned() throws Exception {
|
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
|
testBlockedThread(Thread.ofVirtual().factory(), true);
|
|
}
|
|
|
|
void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception {
|
|
var lock = new Object();
|
|
var started = new CountDownLatch(1);
|
|
|
|
Thread thread = factory.newThread(() -> {
|
|
if (pinned) {
|
|
VThreadPinner.runPinned(() -> {
|
|
started.countDown();
|
|
synchronized (lock) { } // blocks
|
|
});
|
|
} else {
|
|
started.countDown();
|
|
synchronized (lock) { } // blocks
|
|
}
|
|
});
|
|
|
|
try {
|
|
synchronized (lock) {
|
|
// start thread and wait for it to block
|
|
thread.start();
|
|
started.await();
|
|
await(thread, Thread.State.BLOCKED);
|
|
|
|
long tid = thread.threadId();
|
|
String lockAsString = Objects.toIdentityString(lock);
|
|
|
|
// thread dump in plain text should include thread
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
ThreadFields fields = findThread(tid, lines);
|
|
assertNotNull(fields, "thread not found");
|
|
assertEquals("BLOCKED", fields.state());
|
|
assertTrue(contains(lines, "- waiting to lock <" + lockAsString));
|
|
|
|
// thread dump in JSON format should include thread in root container
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(tid)
|
|
.orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
assertEquals("BLOCKED", ti.state());
|
|
assertEquals(lockAsString, ti.blockedOn());
|
|
if (pinned) {
|
|
long carrierTid = ti.carrier().orElse(-1L);
|
|
assertNotEquals(-1L, carrierTid, "carrier not found");
|
|
assertForkJoinWorkerThread(carrierTid);
|
|
}
|
|
}
|
|
} finally {
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread waiting in Object.wait.
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("threadFactories")
|
|
void testWaitingThread(ThreadFactory factory) throws Exception {
|
|
testWaitingThread(factory, false);
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread waiting in Object.wait when pinned.
|
|
*/
|
|
@Test
|
|
void testWaitingThreadWhenPinned() throws Exception {
|
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
|
testWaitingThread(Thread.ofVirtual().factory(), true);
|
|
}
|
|
|
|
void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception {
|
|
var lock = new Object();
|
|
var started = new CountDownLatch(1);
|
|
|
|
Thread thread = factory.newThread(() -> {
|
|
try {
|
|
synchronized (lock) {
|
|
if (pinned) {
|
|
VThreadPinner.runPinned(() -> {
|
|
started.countDown();
|
|
lock.wait();
|
|
});
|
|
} else {
|
|
started.countDown();
|
|
lock.wait();
|
|
}
|
|
}
|
|
} catch (InterruptedException e) { }
|
|
});
|
|
|
|
try {
|
|
// start thread and wait for it to wait in Object.wait
|
|
thread.start();
|
|
started.await();
|
|
await(thread, Thread.State.WAITING);
|
|
|
|
long tid = thread.threadId();
|
|
String lockAsString = Objects.toIdentityString(lock);
|
|
|
|
// thread dump in plain text should include thread
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
ThreadFields fields = findThread(tid, lines);
|
|
assertNotNull(fields, "thread not found");
|
|
assertEquals("WAITING", fields.state());
|
|
|
|
// thread dump in JSON format should include thread in root container
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(thread.threadId())
|
|
.orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
assertEquals(ti.isVirtual(), thread.isVirtual());
|
|
assertEquals("WAITING", ti.state());
|
|
if (pinned) {
|
|
long carrierTid = ti.carrier().orElse(-1L);
|
|
assertNotEquals(-1L, carrierTid, "carrier not found");
|
|
assertForkJoinWorkerThread(carrierTid);
|
|
}
|
|
|
|
// Compiled native frames have no locals. If Object.wait0 has been compiled
|
|
// then we don't have the object that the thread is waiting on
|
|
Method wait0 = Object.class.getDeclaredMethod("wait0", long.class);
|
|
boolean expectWaitingOn = !WhiteBox.getWhiteBox().isMethodCompiled(wait0);
|
|
if (expectWaitingOn) {
|
|
// plain text dump should have "waiting on" line
|
|
assertTrue(contains(lines, "- waiting on <" + lockAsString));
|
|
|
|
// JSON thread dump should have waitingOn property
|
|
assertEquals(lockAsString, ti.waitingOn());
|
|
}
|
|
|
|
} finally {
|
|
synchronized (lock) {
|
|
lock.notifyAll();
|
|
}
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread parked on a j.u.c. lock.
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("threadFactories")
|
|
void testParkedThread(ThreadFactory factory) throws Exception {
|
|
testParkedThread(factory, false);
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread parked on a j.u.c. lock and pinned.
|
|
*/
|
|
@Test
|
|
void testParkedThreadWhenPinned() throws Exception {
|
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
|
testParkedThread(Thread.ofVirtual().factory(), true);
|
|
}
|
|
|
|
void testParkedThread(ThreadFactory factory, boolean pinned) throws Exception {
|
|
var lock = new ReentrantLock();
|
|
var started = new CountDownLatch(1);
|
|
|
|
Thread thread = factory.newThread(() -> {
|
|
if (pinned) {
|
|
VThreadPinner.runPinned(() -> {
|
|
started.countDown();
|
|
lock.lock();
|
|
lock.unlock();
|
|
});
|
|
} else {
|
|
started.countDown();
|
|
lock.lock();
|
|
lock.unlock();
|
|
}
|
|
});
|
|
|
|
lock.lock();
|
|
try {
|
|
// start thread and wait for it to park
|
|
thread.start();
|
|
started.await();
|
|
await(thread, Thread.State.WAITING);
|
|
|
|
long tid = thread.threadId();
|
|
|
|
// thread dump in plain text should include thread
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
ThreadFields fields = findThread(tid, lines);
|
|
assertNotNull(fields, "thread not found");
|
|
assertEquals("WAITING", fields.state());
|
|
assertTrue(contains(lines, "- parking to wait for <java.util.concurrent.locks.ReentrantLock"));
|
|
|
|
// thread dump in JSON format should include thread in root container
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(thread.threadId())
|
|
.orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
assertEquals(ti.isVirtual(), thread.isVirtual());
|
|
|
|
// thread should be waiting on the ReentrantLock
|
|
assertEquals("WAITING", ti.state());
|
|
String parkBlocker = ti.parkBlocker();
|
|
assertNotNull(parkBlocker);
|
|
assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
|
|
if (pinned) {
|
|
long carrierTid = ti.carrier().orElse(-1L);
|
|
assertNotEquals(-1L, carrierTid, "carrier not found");
|
|
assertForkJoinWorkerThread(carrierTid);
|
|
}
|
|
} finally {
|
|
lock.unlock();
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test thread dump with a thread owning a monitor.
|
|
*/
|
|
@ParameterizedTest
|
|
@MethodSource("threadFactories")
|
|
void testThreadOwnsMonitor(ThreadFactory factory) throws Exception {
|
|
testThreadOwnsMonitor(factory, false);
|
|
}
|
|
|
|
@Test
|
|
void testThreadOwnsMonitorWhenPinned() throws Exception {
|
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
|
testThreadOwnsMonitor(Thread.ofVirtual().factory(), true);
|
|
}
|
|
|
|
void testThreadOwnsMonitor(ThreadFactory factory, boolean pinned) throws Exception {
|
|
var lock = new Object();
|
|
var started = new CountDownLatch(1);
|
|
|
|
Thread thread = factory.newThread(() -> {
|
|
synchronized (lock) {
|
|
if (pinned) {
|
|
VThreadPinner.runPinned(() -> {
|
|
started.countDown();
|
|
LockSupport.park();
|
|
});
|
|
} else {
|
|
started.countDown();
|
|
LockSupport.park();
|
|
}
|
|
}
|
|
});
|
|
|
|
try {
|
|
// start thread and wait for it to park
|
|
thread.start();
|
|
started.await();
|
|
await(thread, Thread.State.WAITING);
|
|
|
|
long tid = thread.threadId();
|
|
String lockAsString = Objects.toIdentityString(lock);
|
|
|
|
// thread dump in plain text should include thread
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
ThreadFields fields = findThread(tid, lines);
|
|
assertNotNull(fields, "thread not found");
|
|
assertEquals("WAITING", fields.state());
|
|
assertTrue(contains(lines, "- locked <" + lockAsString));
|
|
|
|
// thread dump in JSON format should include thread in root container
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(tid)
|
|
.orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
assertEquals(ti.isVirtual(), thread.isVirtual());
|
|
|
|
// the lock should be in the ownedMonitors array
|
|
Set<String> ownedMonitors = ti.ownedMonitors().values()
|
|
.stream()
|
|
.flatMap(List::stream)
|
|
.collect(Collectors.toSet());
|
|
assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
|
|
} finally {
|
|
LockSupport.unpark(thread);
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test mounted virtual thread.
|
|
*/
|
|
@Test
|
|
void testMountedVirtualThread() throws Exception {
|
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
|
|
|
// start virtual thread that spins until done
|
|
var started = new AtomicBoolean();
|
|
var done = new AtomicBoolean();
|
|
var thread = Thread.ofVirtual().start(() -> {
|
|
started.set(true);
|
|
while (!done.get()) {
|
|
Thread.onSpinWait();
|
|
}
|
|
});
|
|
|
|
try {
|
|
// wait for thread to start
|
|
awaitTrue(started);
|
|
long tid = thread.threadId();
|
|
|
|
// thread dump in plain text should include thread
|
|
List<String> lines = dumpThreadsToPlainText();
|
|
ThreadFields fields = findThread(tid, lines);
|
|
assertNotNull(fields, "thread not found");
|
|
assertTrue(fields.isVirtual());
|
|
|
|
// thread dump in JSON format should include thread in root container
|
|
ThreadDump threadDump = dumpThreadsToJson();
|
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
|
.findThread(tid)
|
|
.orElse(null);
|
|
assertNotNull(ti, "thread not found");
|
|
assertTrue(ti.isVirtual());
|
|
long carrierTid = ti.carrier().orElse(-1L);
|
|
assertNotEquals(-1L, carrierTid, "carrier not found");
|
|
assertForkJoinWorkerThread(carrierTid);
|
|
} finally {
|
|
done.set(true);
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asserts that the given thread identifier is a ForkJoinWorkerThread.
|
|
*/
|
|
private void assertForkJoinWorkerThread(long tid) {
|
|
Thread thread = Thread.getAllStackTraces()
|
|
.keySet()
|
|
.stream()
|
|
.filter(t -> t.threadId() == tid)
|
|
.findAny()
|
|
.orElse(null);
|
|
assertNotNull(thread, "thread " + tid + " not found");
|
|
assertTrue(thread instanceof ForkJoinWorkerThread, "not a ForkJoinWorkerThread");
|
|
}
|
|
|
|
/**
|
|
* Test that dumpThreads throws if the output file already exists.
|
|
*/
|
|
@Test
|
|
void testFileAlreadyExists() throws Exception {
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
String file = Files.createFile(genOutputPath("txt")).toString();
|
|
assertThrows(FileAlreadyExistsException.class,
|
|
() -> mbean.dumpThreads(file, ThreadDumpFormat.TEXT_PLAIN));
|
|
assertThrows(FileAlreadyExistsException.class,
|
|
() -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
|
|
}
|
|
|
|
/**
|
|
* Test that dumpThreads throws IOException when the output file cannot be created.
|
|
*/
|
|
@Test
|
|
void testFileCreateFails() {
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
String badFile = Path.of(".").toAbsolutePath()
|
|
.resolve("does-not-exist")
|
|
.resolve("does-not-exist")
|
|
.resolve("threads.bad")
|
|
.toString();
|
|
assertThrows(IOException.class,
|
|
() -> mbean.dumpThreads(badFile, ThreadDumpFormat.TEXT_PLAIN));
|
|
assertThrows(IOException.class,
|
|
() -> mbean.dumpThreads(badFile, ThreadDumpFormat.JSON));
|
|
}
|
|
|
|
/**
|
|
* Test that dumpThreads throws IOException if writing to output file fails.
|
|
*/
|
|
@Test
|
|
void testFileWriteFails() {
|
|
var out = new OutputStream() {
|
|
@Override
|
|
public void write(int b) throws IOException {
|
|
throw new IOException("There is not enough space on the disk");
|
|
}
|
|
};
|
|
// need to invoke internal API directly to test this
|
|
assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreads(out));
|
|
assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreadsToJson(out));
|
|
}
|
|
|
|
/**
|
|
* Test that dumpThreads throws if the file path is relative.
|
|
*/
|
|
@Test
|
|
void testRelativePath() {
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
assertThrows(IllegalArgumentException.class,
|
|
() -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
|
|
assertThrows(IllegalArgumentException.class,
|
|
() -> mbean.dumpThreads("threads.json", ThreadDumpFormat.JSON));
|
|
}
|
|
|
|
/**
|
|
* Test that dumpThreads throws with null parameters.
|
|
*/
|
|
@Test
|
|
void testNull() {
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
assertThrows(NullPointerException.class,
|
|
() -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
|
|
assertThrows(NullPointerException.class,
|
|
() -> mbean.dumpThreads(genOutputPath("txt").toString(), null));
|
|
}
|
|
|
|
/**
|
|
* Represents the data for a thread found in a plain text thread dump.
|
|
*/
|
|
private record ThreadFields(long tid, String name, boolean isVirtual, String state) { }
|
|
|
|
/**
|
|
* Find a thread in the lines of a plain text thread dump.
|
|
*/
|
|
private ThreadFields findThread(long tid, List<String> lines) {
|
|
String line = lines.stream()
|
|
.filter(l -> l.startsWith("#" + tid + " "))
|
|
.findFirst()
|
|
.orElse(null);
|
|
if (line == null) {
|
|
return null;
|
|
}
|
|
|
|
// #3 "main" RUNNABLE 2025-04-18T15:22:12.012450Z
|
|
// #36 "" virtual WAITING 2025-04-18T15:22:12.012450Z
|
|
Pattern pattern = Pattern.compile("#(\\d+)\\s+\"([^\"]*)\"\\s+(virtual\\s+)?(\\w+)\\s+(.*)");
|
|
Matcher matcher = pattern.matcher(line);
|
|
assertTrue(matcher.matches());
|
|
String name = matcher.group(2);
|
|
boolean isVirtual = "virtual ".equals(matcher.group(3));
|
|
String state = matcher.group(4);
|
|
return new ThreadFields(tid, name, isVirtual, state);
|
|
}
|
|
|
|
/**
|
|
* Returns true if lines of a plain text thread dump contain the given text.
|
|
*/
|
|
private boolean contains(List<String> lines, String text) {
|
|
return lines.stream().map(String::trim)
|
|
.anyMatch(l -> l.contains(text));
|
|
}
|
|
|
|
/**
|
|
* Dump threads to a file in plain text format, return the lines in the file.
|
|
*/
|
|
private List<String> dumpThreadsToPlainText() throws Exception {
|
|
Path file = genOutputPath(".txt");
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
|
|
System.err.format("Dumped to %s%n", file.getFileName());
|
|
List<String> lines = Files.readAllLines(file);
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Dump threads to a file in JSON format, parse and return as JSON object.
|
|
*/
|
|
private static ThreadDump dumpThreadsToJson() throws Exception {
|
|
Path file = genOutputPath(".json");
|
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
|
mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
|
|
System.err.format("Dumped to %s%n", file.getFileName());
|
|
String jsonText = Files.readString(file);
|
|
return ThreadDump.parse(jsonText);
|
|
}
|
|
|
|
/**
|
|
* Generate a file path with the given suffix to use as an output file.
|
|
*/
|
|
private static Path genOutputPath(String suffix) throws Exception {
|
|
Path dir = Path.of(".").toAbsolutePath();
|
|
Path file = Files.createTempFile(dir, "dump", suffix);
|
|
Files.delete(file);
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* Waits for the given thread to get to a given state.
|
|
*/
|
|
private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
|
|
Thread.State state = thread.getState();
|
|
while (state != expectedState) {
|
|
assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
|
|
Thread.sleep(10);
|
|
state = thread.getState();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Waits for the boolean value to become true.
|
|
*/
|
|
private static void awaitTrue(AtomicBoolean ref) throws Exception {
|
|
while (!ref.get()) {
|
|
Thread.sleep(20);
|
|
}
|
|
}
|
|
}
|