8356870: HotSpotDiagnosticMXBean.dumpThreads and jcmd Thread.dump_to_file updates
Reviewed-by: sspitsyn, kevinw
This commit is contained in:
parent
ebd85288ce
commit
f17b2bc06a
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -24,11 +24,13 @@
|
|||||||
*/
|
*/
|
||||||
package jdk.internal.vm;
|
package jdk.internal.vm;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedWriter;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.io.Writer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -36,15 +38,19 @@ import java.nio.file.OpenOption;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thread dump support.
|
* Thread dump support.
|
||||||
*
|
*
|
||||||
* This class defines methods to dump threads to an output stream or file in plain
|
* This class defines static methods to support the Thread.dump_to_file diagnostic command
|
||||||
* text or JSON format.
|
* and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
|
||||||
|
* thread dump to a file or byte array in plain text or JSON format.
|
||||||
*/
|
*/
|
||||||
public class ThreadDumper {
|
public class ThreadDumper {
|
||||||
private ThreadDumper() { }
|
private ThreadDumper() { }
|
||||||
@ -53,13 +59,12 @@ public class ThreadDumper {
|
|||||||
private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
|
private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded.
|
* Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.
|
||||||
*
|
|
||||||
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
|
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
|
||||||
*
|
*
|
||||||
* @param file the file path to the file, null or "-" to return a byte array
|
* @param file the file path to the file, null or "-" to return a byte array
|
||||||
* @param okayToOverwrite true to overwrite an existing file
|
* @param okayToOverwrite true to overwrite an existing file
|
||||||
* @return the UTF-8 encoded thread dump or message to return to the user
|
* @return the UTF-8 encoded thread dump or message to return to the tool user
|
||||||
*/
|
*/
|
||||||
public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
|
public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
|
||||||
if (file == null || file.equals("-")) {
|
if (file == null || file.equals("-")) {
|
||||||
@ -70,13 +75,12 @@ public class ThreadDumper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded.
|
* Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded.
|
||||||
*
|
|
||||||
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
|
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
|
||||||
*
|
*
|
||||||
* @param file the file path to the file, null or "-" to return a byte array
|
* @param file the file path to the file, null or "-" to return a byte array
|
||||||
* @param okayToOverwrite true to overwrite an existing file
|
* @param okayToOverwrite true to overwrite an existing file
|
||||||
* @return the UTF-8 encoded thread dump or message to return to the user
|
* @return the UTF-8 encoded thread dump or message to return to the tool user
|
||||||
*/
|
*/
|
||||||
public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
|
public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
|
||||||
if (file == null || file.equals("-")) {
|
if (file == null || file.equals("-")) {
|
||||||
@ -88,21 +92,32 @@ public class ThreadDumper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
|
* Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
|
||||||
|
* This method is the implementation of the Thread.dump_to_file diagnostic command
|
||||||
|
* when a file path is not specified. It returns the thread dump and/or message to
|
||||||
|
* send to the tool user.
|
||||||
*/
|
*/
|
||||||
private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
|
private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
|
||||||
try (var out = new BoundedByteArrayOutputStream(maxSize);
|
var out = new BoundedByteArrayOutputStream(maxSize);
|
||||||
PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
|
try (out; var writer = new TextWriter(out)) {
|
||||||
if (json) {
|
if (json) {
|
||||||
dumpThreadsToJson(ps);
|
dumpThreadsToJson(writer);
|
||||||
} else {
|
} else {
|
||||||
dumpThreads(ps);
|
dumpThreads(writer);
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
} catch (Exception ex) {
|
||||||
|
if (ex instanceof UncheckedIOException ioe) {
|
||||||
|
ex = ioe.getCause();
|
||||||
|
}
|
||||||
|
String reply = String.format("Failed: %s%n", ex);
|
||||||
|
return reply.getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
|
* Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
|
||||||
|
* This method is the implementation of the Thread.dump_to_file diagnostic command.
|
||||||
|
* It returns the thread dump and/or message to send to the tool user.
|
||||||
*/
|
*/
|
||||||
private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
|
private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
|
||||||
Path path = Path.of(file).toAbsolutePath();
|
Path path = Path.of(file).toAbsolutePath();
|
||||||
@ -110,224 +125,412 @@ public class ThreadDumper {
|
|||||||
? new OpenOption[0]
|
? new OpenOption[0]
|
||||||
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
|
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
|
||||||
String reply;
|
String reply;
|
||||||
try (OutputStream out = Files.newOutputStream(path, options);
|
try (OutputStream out = Files.newOutputStream(path, options)) {
|
||||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
try (var writer = new TextWriter(out)) {
|
||||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) {
|
if (json) {
|
||||||
if (json) {
|
dumpThreadsToJson(writer);
|
||||||
dumpThreadsToJson(ps);
|
} else {
|
||||||
} else {
|
dumpThreads(writer);
|
||||||
dumpThreads(ps);
|
}
|
||||||
|
reply = String.format("Created %s%n", path);
|
||||||
|
} catch (UncheckedIOException e) {
|
||||||
|
reply = String.format("Failed: %s%n", e.getCause());
|
||||||
}
|
}
|
||||||
reply = String.format("Created %s%n", path);
|
} catch (FileAlreadyExistsException _) {
|
||||||
} catch (FileAlreadyExistsException e) {
|
|
||||||
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
|
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
|
||||||
} catch (IOException ioe) {
|
} catch (Exception ex) {
|
||||||
reply = String.format("Failed: %s%n", ioe);
|
reply = String.format("Failed: %s%n", ex);
|
||||||
}
|
}
|
||||||
return reply.getBytes(StandardCharsets.UTF_8);
|
return reply.getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in plain text format to the given output stream,
|
* Generate a thread dump in plain text format to the given output stream, UTF-8
|
||||||
* UTF-8 encoded.
|
* encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
||||||
*
|
* @throws IOException if an I/O error occurs
|
||||||
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
|
||||||
*/
|
*/
|
||||||
public static void dumpThreads(OutputStream out) {
|
public static void dumpThreads(OutputStream out) throws IOException {
|
||||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
var writer = new TextWriter(out);
|
||||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
|
|
||||||
try {
|
try {
|
||||||
dumpThreads(ps);
|
dumpThreads(writer);
|
||||||
} finally {
|
writer.flush();
|
||||||
ps.flush(); // flushes underlying stream
|
} catch (UncheckedIOException e) {
|
||||||
|
IOException ioe = e.getCause();
|
||||||
|
throw ioe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in plain text format to the given print stream.
|
* Generate a thread dump in plain text format to the given text stream.
|
||||||
|
* @throws UncheckedIOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
private static void dumpThreads(PrintStream ps) {
|
private static void dumpThreads(TextWriter writer) {
|
||||||
ps.println(processId());
|
writer.println(processId());
|
||||||
ps.println(Instant.now());
|
writer.println(Instant.now());
|
||||||
ps.println(Runtime.version());
|
writer.println(Runtime.version());
|
||||||
ps.println();
|
writer.println();
|
||||||
dumpThreads(ThreadContainers.root(), ps);
|
dumpThreads(ThreadContainers.root(), writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void dumpThreads(ThreadContainer container, PrintStream ps) {
|
private static void dumpThreads(ThreadContainer container, TextWriter writer) {
|
||||||
container.threads().forEach(t -> dumpThread(t, ps));
|
container.threads().forEach(t -> dumpThread(t, writer));
|
||||||
container.children().forEach(c -> dumpThreads(c, ps));
|
container.children().forEach(c -> dumpThreads(c, writer));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void dumpThread(Thread thread, PrintStream ps) {
|
private static void dumpThread(Thread thread, TextWriter writer) {
|
||||||
String suffix = thread.isVirtual() ? " virtual" : "";
|
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
|
||||||
ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
|
Instant now = Instant.now();
|
||||||
for (StackTraceElement ste : thread.getStackTrace()) {
|
Thread.State state = snapshot.threadState();
|
||||||
ps.print(" ");
|
writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
|
||||||
ps.println(ste);
|
+ "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now);
|
||||||
|
|
||||||
|
StackTraceElement[] stackTrace = snapshot.stackTrace();
|
||||||
|
int depth = 0;
|
||||||
|
while (depth < stackTrace.length) {
|
||||||
|
writer.print(" at ");
|
||||||
|
writer.println(stackTrace[depth]);
|
||||||
|
snapshot.ownedMonitorsAt(depth).forEach(o -> {
|
||||||
|
if (o != null) {
|
||||||
|
writer.println(" - locked " + decorateObject(o));
|
||||||
|
} else {
|
||||||
|
writer.println(" - lock is eliminated");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if parkBlocker set, or blocked/waiting on monitor, then print after top frame
|
||||||
|
if (depth == 0) {
|
||||||
|
// park blocker
|
||||||
|
Object parkBlocker = snapshot.parkBlocker();
|
||||||
|
if (parkBlocker != null) {
|
||||||
|
writer.println(" - parking to wait for " + decorateObject(parkBlocker));
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocked on monitor enter or Object.wait
|
||||||
|
if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
|
||||||
|
writer.println(" - waiting to lock " + decorateObject(obj));
|
||||||
|
} else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
|
||||||
|
&& snapshot.waitingOn() instanceof Object obj) {
|
||||||
|
writer.println(" - waiting on " + decorateObject(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depth++;
|
||||||
}
|
}
|
||||||
ps.println();
|
writer.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identity string for the given object in a form suitable for the plain
|
||||||
|
* text format thread dump.
|
||||||
|
*/
|
||||||
|
private static String decorateObject(Object obj) {
|
||||||
|
return "<" + Objects.toIdentityString(obj) + ">";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
|
* Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
|
||||||
*
|
|
||||||
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public static void dumpThreadsToJson(OutputStream out) {
|
public static void dumpThreadsToJson(OutputStream out) throws IOException {
|
||||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
var writer = new TextWriter(out);
|
||||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
|
|
||||||
try {
|
try {
|
||||||
dumpThreadsToJson(ps);
|
dumpThreadsToJson(writer);
|
||||||
} finally {
|
writer.flush();
|
||||||
ps.flush(); // flushes underlying stream
|
} catch (UncheckedIOException e) {
|
||||||
|
IOException ioe = e.getCause();
|
||||||
|
throw ioe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a thread dump to the given print stream in JSON format.
|
* Generate a thread dump to the given text stream in JSON format.
|
||||||
|
* @throws UncheckedIOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
private static void dumpThreadsToJson(PrintStream out) {
|
private static void dumpThreadsToJson(TextWriter textWriter) {
|
||||||
out.println("{");
|
var jsonWriter = new JsonWriter(textWriter);
|
||||||
out.println(" \"threadDump\": {");
|
|
||||||
|
|
||||||
String now = Instant.now().toString();
|
jsonWriter.startObject(); // top-level object
|
||||||
String runtimeVersion = Runtime.version().toString();
|
|
||||||
out.format(" \"processId\": \"%d\",%n", processId());
|
|
||||||
out.format(" \"time\": \"%s\",%n", escape(now));
|
|
||||||
out.format(" \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
|
|
||||||
|
|
||||||
out.println(" \"threadContainers\": [");
|
jsonWriter.startObject("threadDump");
|
||||||
List<ThreadContainer> containers = allContainers();
|
|
||||||
Iterator<ThreadContainer> iterator = containers.iterator();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
ThreadContainer container = iterator.next();
|
|
||||||
boolean more = iterator.hasNext();
|
|
||||||
dumpThreadsToJson(container, out, more);
|
|
||||||
}
|
|
||||||
out.println(" ]"); // end of threadContainers
|
|
||||||
|
|
||||||
out.println(" }"); // end threadDump
|
jsonWriter.writeProperty("processId", processId());
|
||||||
out.println("}"); // end object
|
jsonWriter.writeProperty("time", Instant.now());
|
||||||
|
jsonWriter.writeProperty("runtimeVersion", Runtime.version());
|
||||||
|
|
||||||
|
jsonWriter.startArray("threadContainers");
|
||||||
|
dumpThreads(ThreadContainers.root(), jsonWriter);
|
||||||
|
jsonWriter.endArray();
|
||||||
|
|
||||||
|
jsonWriter.endObject(); // threadDump
|
||||||
|
|
||||||
|
jsonWriter.endObject(); // end of top-level object
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dump the given thread container to the print stream in JSON format.
|
* Write a thread container to the given JSON writer.
|
||||||
|
* @throws UncheckedIOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
private static void dumpThreadsToJson(ThreadContainer container,
|
private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
|
||||||
PrintStream out,
|
jsonWriter.startObject();
|
||||||
boolean more) {
|
jsonWriter.writeProperty("container", container);
|
||||||
out.println(" {");
|
jsonWriter.writeProperty("parent", container.parent());
|
||||||
out.format(" \"container\": \"%s\",%n", escape(container.toString()));
|
|
||||||
|
|
||||||
ThreadContainer parent = container.parent();
|
|
||||||
if (parent == null) {
|
|
||||||
out.format(" \"parent\": null,%n");
|
|
||||||
} else {
|
|
||||||
out.format(" \"parent\": \"%s\",%n", escape(parent.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread owner = container.owner();
|
Thread owner = container.owner();
|
||||||
if (owner == null) {
|
jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
|
||||||
out.format(" \"owner\": null,%n");
|
|
||||||
} else {
|
|
||||||
out.format(" \"owner\": \"%d\",%n", owner.threadId());
|
|
||||||
}
|
|
||||||
|
|
||||||
long threadCount = 0;
|
long threadCount = 0;
|
||||||
out.println(" \"threads\": [");
|
jsonWriter.startArray("threads");
|
||||||
Iterator<Thread> threads = container.threads().iterator();
|
Iterator<Thread> threads = container.threads().iterator();
|
||||||
while (threads.hasNext()) {
|
while (threads.hasNext()) {
|
||||||
Thread thread = threads.next();
|
Thread thread = threads.next();
|
||||||
dumpThreadToJson(thread, out, threads.hasNext());
|
dumpThread(thread, jsonWriter);
|
||||||
threadCount++;
|
threadCount++;
|
||||||
}
|
}
|
||||||
out.println(" ],"); // end of threads
|
jsonWriter.endArray(); // threads
|
||||||
|
|
||||||
// thread count
|
// thread count
|
||||||
if (!ThreadContainers.trackAllThreads()) {
|
if (!ThreadContainers.trackAllThreads()) {
|
||||||
threadCount = Long.max(threadCount, container.threadCount());
|
threadCount = Long.max(threadCount, container.threadCount());
|
||||||
}
|
}
|
||||||
out.format(" \"threadCount\": \"%d\"%n", threadCount);
|
jsonWriter.writeProperty("threadCount", threadCount);
|
||||||
|
|
||||||
if (more) {
|
jsonWriter.endObject();
|
||||||
out.println(" },");
|
|
||||||
} else {
|
// the children of the thread container follow
|
||||||
out.println(" }"); // last container, no trailing comma
|
container.children().forEach(c -> dumpThreads(c, jsonWriter));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dump the given thread and its stack trace to the print stream in JSON format.
|
* Write a thread to the given JSON writer.
|
||||||
|
* @throws UncheckedIOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
|
private static void dumpThread(Thread thread, JsonWriter jsonWriter) {
|
||||||
out.println(" {");
|
Instant now = Instant.now();
|
||||||
out.println(" \"tid\": \"" + thread.threadId() + "\",");
|
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
|
||||||
out.println(" \"name\": \"" + escape(thread.getName()) + "\",");
|
Thread.State state = snapshot.threadState();
|
||||||
out.println(" \"stack\": [");
|
StackTraceElement[] stackTrace = snapshot.stackTrace();
|
||||||
|
|
||||||
int i = 0;
|
jsonWriter.startObject();
|
||||||
StackTraceElement[] stackTrace = thread.getStackTrace();
|
jsonWriter.writeProperty("tid", thread.threadId());
|
||||||
while (i < stackTrace.length) {
|
jsonWriter.writeProperty("time", now);
|
||||||
out.print(" \"");
|
if (thread.isVirtual()) {
|
||||||
out.print(escape(stackTrace[i].toString()));
|
jsonWriter.writeProperty("virtual", Boolean.TRUE);
|
||||||
out.print("\"");
|
}
|
||||||
i++;
|
jsonWriter.writeProperty("name", snapshot.threadName());
|
||||||
if (i < stackTrace.length) {
|
jsonWriter.writeProperty("state", state);
|
||||||
out.println(",");
|
|
||||||
} else {
|
// park blocker
|
||||||
out.println(); // last element, no trailing comma
|
Object parkBlocker = snapshot.parkBlocker();
|
||||||
|
if (parkBlocker != null) {
|
||||||
|
// parkBlocker is an object to allow for exclusiveOwnerThread in the future
|
||||||
|
jsonWriter.startObject("parkBlocker");
|
||||||
|
jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
|
||||||
|
jsonWriter.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocked on monitor enter or Object.wait
|
||||||
|
if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
|
||||||
|
jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
|
||||||
|
} else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
|
||||||
|
&& snapshot.waitingOn() instanceof Object obj) {
|
||||||
|
jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack trace
|
||||||
|
jsonWriter.startArray("stack");
|
||||||
|
Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
|
||||||
|
jsonWriter.endArray();
|
||||||
|
|
||||||
|
// monitors owned, skip if none
|
||||||
|
if (snapshot.ownsMonitors()) {
|
||||||
|
jsonWriter.startArray("monitorsOwned");
|
||||||
|
int depth = 0;
|
||||||
|
while (depth < stackTrace.length) {
|
||||||
|
List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
|
||||||
|
if (!objs.isEmpty()) {
|
||||||
|
jsonWriter.startObject();
|
||||||
|
jsonWriter.writeProperty("depth", depth);
|
||||||
|
jsonWriter.startArray("locks");
|
||||||
|
snapshot.ownedMonitorsAt(depth)
|
||||||
|
.map(o -> (o != null) ? Objects.toIdentityString(o) : null)
|
||||||
|
.forEach(jsonWriter::writeProperty);
|
||||||
|
jsonWriter.endArray();
|
||||||
|
jsonWriter.endObject();
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
jsonWriter.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// thread identifier of carrier, when mounted
|
||||||
|
if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
|
||||||
|
jsonWriter.writeProperty("carrier", carrier.threadId());
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonWriter.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
|
||||||
|
* This class is not intended to be a fully featured JSON writer.
|
||||||
|
*/
|
||||||
|
private static class JsonWriter {
|
||||||
|
private static class Node {
|
||||||
|
final boolean isArray;
|
||||||
|
int propertyCount;
|
||||||
|
Node(boolean isArray) {
|
||||||
|
this.isArray = isArray;
|
||||||
|
}
|
||||||
|
boolean isArray() {
|
||||||
|
return isArray;
|
||||||
|
}
|
||||||
|
int propertyCount() {
|
||||||
|
return propertyCount;
|
||||||
|
}
|
||||||
|
int getAndIncrementPropertyCount() {
|
||||||
|
int old = propertyCount;
|
||||||
|
propertyCount++;
|
||||||
|
return old;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.println(" ]");
|
private final Deque<Node> stack = new ArrayDeque<>();
|
||||||
if (more) {
|
private final TextWriter writer;
|
||||||
out.println(" },");
|
|
||||||
} else {
|
JsonWriter(TextWriter writer) {
|
||||||
out.println(" }"); // last thread, no trailing comma
|
this.writer = writer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private void indent() {
|
||||||
* Returns a list of all thread containers that are "reachable" from
|
int indent = stack.size() * 2;
|
||||||
* the root container.
|
writer.print(" ".repeat(indent));
|
||||||
*/
|
}
|
||||||
private static List<ThreadContainer> allContainers() {
|
|
||||||
List<ThreadContainer> containers = new ArrayList<>();
|
|
||||||
collect(ThreadContainers.root(), containers);
|
|
||||||
return containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
|
/**
|
||||||
containers.add(container);
|
* Start of object or array.
|
||||||
container.children().forEach(c -> collect(c, containers));
|
*/
|
||||||
}
|
private void startObject(String name, boolean isArray) {
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
Node node = stack.peek();
|
||||||
|
if (node.getAndIncrementPropertyCount() > 0) {
|
||||||
|
writer.println(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indent();
|
||||||
|
if (name != null) {
|
||||||
|
writer.print("\"" + name + "\": ");
|
||||||
|
}
|
||||||
|
writer.println(isArray ? "[" : "{");
|
||||||
|
stack.push(new Node(isArray));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape any characters that need to be escape in the JSON output.
|
* End of object or array.
|
||||||
*/
|
*/
|
||||||
private static String escape(String value) {
|
private void endObject(boolean isArray) {
|
||||||
StringBuilder sb = new StringBuilder();
|
Node node = stack.pop();
|
||||||
for (int i = 0; i < value.length(); i++) {
|
if (node.isArray() != isArray)
|
||||||
char c = value.charAt(i);
|
throw new IllegalStateException();
|
||||||
switch (c) {
|
if (node.propertyCount() > 0) {
|
||||||
case '"' -> sb.append("\\\"");
|
writer.println();
|
||||||
case '\\' -> sb.append("\\\\");
|
}
|
||||||
case '/' -> sb.append("\\/");
|
indent();
|
||||||
case '\b' -> sb.append("\\b");
|
writer.print(isArray ? "]" : "}");
|
||||||
case '\f' -> sb.append("\\f");
|
}
|
||||||
case '\n' -> sb.append("\\n");
|
|
||||||
case '\r' -> sb.append("\\r");
|
/**
|
||||||
case '\t' -> sb.append("\\t");
|
* Write a property.
|
||||||
default -> {
|
* @param name the property name, null for an unnamed property
|
||||||
if (c <= 0x1f) {
|
* @param obj the value or null
|
||||||
sb.append(String.format("\\u%04x", c));
|
*/
|
||||||
} else {
|
void writeProperty(String name, Object obj) {
|
||||||
sb.append(c);
|
Node node = stack.peek();
|
||||||
|
if (node.getAndIncrementPropertyCount() > 0) {
|
||||||
|
writer.println(",");
|
||||||
|
}
|
||||||
|
indent();
|
||||||
|
if (name != null) {
|
||||||
|
writer.print("\"" + name + "\": ");
|
||||||
|
}
|
||||||
|
switch (obj) {
|
||||||
|
// Long may be larger than safe range of JSON integer value
|
||||||
|
case Long _ -> writer.print("\"" + obj + "\"");
|
||||||
|
case Number _ -> writer.print(obj);
|
||||||
|
case Boolean _ -> writer.print(obj);
|
||||||
|
case null -> writer.print("null");
|
||||||
|
default -> writer.print("\"" + escape(obj.toString()) + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an unnamed property.
|
||||||
|
*/
|
||||||
|
void writeProperty(Object obj) {
|
||||||
|
writeProperty(null, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start named object.
|
||||||
|
*/
|
||||||
|
void startObject(String name) {
|
||||||
|
startObject(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start unnamed object.
|
||||||
|
*/
|
||||||
|
void startObject() {
|
||||||
|
startObject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End of object.
|
||||||
|
*/
|
||||||
|
void endObject() {
|
||||||
|
endObject(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start named array.
|
||||||
|
*/
|
||||||
|
void startArray(String name) {
|
||||||
|
startObject(name, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End of array.
|
||||||
|
*/
|
||||||
|
void endArray() {
|
||||||
|
endObject(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape any characters that need to be escape in the JSON output.
|
||||||
|
*/
|
||||||
|
private static String escape(String value) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '"' -> sb.append("\\\"");
|
||||||
|
case '\\' -> sb.append("\\\\");
|
||||||
|
case '/' -> sb.append("\\/");
|
||||||
|
case '\b' -> sb.append("\\b");
|
||||||
|
case '\f' -> sb.append("\\f");
|
||||||
|
case '\n' -> sb.append("\\n");
|
||||||
|
case '\r' -> sb.append("\\r");
|
||||||
|
case '\t' -> sb.append("\\t");
|
||||||
|
default -> {
|
||||||
|
if (c <= 0x1f) {
|
||||||
|
sb.append(String.format("\\u%04x", c));
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -357,6 +560,56 @@ public class ThreadDumper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Writer implementation for printing text. The print/println methods
|
||||||
|
* throw UncheckedIOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
private static class TextWriter extends Writer {
|
||||||
|
private final Writer delegate;
|
||||||
|
|
||||||
|
TextWriter(OutputStream out) {
|
||||||
|
delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(char[] cbuf, int off, int len) throws IOException {
|
||||||
|
delegate.write(cbuf, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print(Object obj) {
|
||||||
|
String s = String.valueOf(obj);
|
||||||
|
try {
|
||||||
|
write(s, 0, s.length());
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new UncheckedIOException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void println() {
|
||||||
|
print(System.lineSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
void println(String s) {
|
||||||
|
print(s);
|
||||||
|
println();
|
||||||
|
}
|
||||||
|
|
||||||
|
void println(Object obj) {
|
||||||
|
print(obj);
|
||||||
|
println();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
delegate.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
delegate.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the process ID or -1 if not supported.
|
* Returns the process ID or -1 if not supported.
|
||||||
*/
|
*/
|
||||||
|
@ -52,9 +52,13 @@ class ThreadSnapshot {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Take a snapshot of a Thread to get all information about the thread.
|
* Take a snapshot of a Thread to get all information about the thread.
|
||||||
|
* @throws UnsupportedOperationException if not supported by VM
|
||||||
*/
|
*/
|
||||||
static ThreadSnapshot of(Thread thread) {
|
static ThreadSnapshot of(Thread thread) {
|
||||||
ThreadSnapshot snapshot = create(thread);
|
ThreadSnapshot snapshot = create(thread);
|
||||||
|
if (snapshot == null) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
if (snapshot.stackTrace == null) {
|
if (snapshot.stackTrace == null) {
|
||||||
snapshot.stackTrace = EMPTY_STACK;
|
snapshot.stackTrace = EMPTY_STACK;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -116,6 +116,13 @@ public interface HotSpotDiagnosticMXBean extends PlatformManagedObject {
|
|||||||
* {@code outputFile} parameter must be an absolute path to a file that
|
* {@code outputFile} parameter must be an absolute path to a file that
|
||||||
* does not exist.
|
* does not exist.
|
||||||
*
|
*
|
||||||
|
* <p> When the format is specified as {@link ThreadDumpFormat#JSON JSON}, the
|
||||||
|
* thread dump is generated in JavaScript Object Notation.
|
||||||
|
* <a href="doc-files/threadDump.schema.json">threadDump.schema.json</a>
|
||||||
|
* describes the thread dump format in draft
|
||||||
|
* <a href="https://tools.ietf.org/html/draft-json-schema-language-02">
|
||||||
|
* JSON Schema Language version 2</a>.
|
||||||
|
*
|
||||||
* <p> The thread dump will include output for all platform threads. It may
|
* <p> The thread dump will include output for all platform threads. It may
|
||||||
* include output for some or all virtual threads.
|
* include output for some or all virtual threads.
|
||||||
*
|
*
|
||||||
@ -151,6 +158,7 @@ public interface HotSpotDiagnosticMXBean extends PlatformManagedObject {
|
|||||||
TEXT_PLAIN,
|
TEXT_PLAIN,
|
||||||
/**
|
/**
|
||||||
* JSON (JavaScript Object Notation) format.
|
* JSON (JavaScript Object Notation) format.
|
||||||
|
* @spec https://datatracker.ietf.org/doc/html/rfc8259 JavaScript Object Notation
|
||||||
*/
|
*/
|
||||||
JSON,
|
JSON,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"threadDump": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"processId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The native process id of the Java virtual machine."
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The time in ISO 8601 format when the thread dump was generated."
|
||||||
|
},
|
||||||
|
"runtimeVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The runtime version, see java.lang.Runtime.Version"
|
||||||
|
},
|
||||||
|
"threadContainers": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The array of thread containers (thread groupings).",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"container": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The container name. The container name is unique."
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "The parent container name or null for the root container."
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "The thread identifier of the owner thread if owned."
|
||||||
|
},
|
||||||
|
"threads": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The array of threads in the thread container.",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The thread identifier."
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The time in ISO 8601 format that the thread was sampled."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The thread name."
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The thread state (Thread::getState)."
|
||||||
|
},
|
||||||
|
"virtual" : {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "true for a virtual thread."
|
||||||
|
},
|
||||||
|
"parkBlocker": {
|
||||||
|
"type": [
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"object": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The blocker object responsible for the thread parking."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"object"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"blockedOn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The object that the thread is blocked on waiting to enter/re-enter a synchronization block/method."
|
||||||
|
},
|
||||||
|
"waitingOn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The object that the thread is waiting to be notified (Object.wait)."
|
||||||
|
},
|
||||||
|
"stack": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The thread stack. The first element is the top of the stack.",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "A stack trace element (java.lang.StackTraceElement)."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"monitorsOwned": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The objects for which monitors are owned by the thread.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"depth": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The stack depth at which the monitors are owned."
|
||||||
|
},
|
||||||
|
"locks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
null
|
||||||
|
],
|
||||||
|
"description": "The object for which the monitor is owned by the thread, null if eliminated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"depth",
|
||||||
|
"locks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"carrier": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The thread identifier of the carrier thread if mounted."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"tid",
|
||||||
|
"time",
|
||||||
|
"name",
|
||||||
|
"state",
|
||||||
|
"stack"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"threadCount": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The number of threads in the thread container."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"container",
|
||||||
|
"parent",
|
||||||
|
"owner",
|
||||||
|
"threads"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"processId",
|
||||||
|
"time",
|
||||||
|
"runtimeVersion",
|
||||||
|
"threadContainers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"threadDump"
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -153,7 +153,7 @@ public class HotSpotDiagnostic implements HotSpotDiagnosticMXBean {
|
|||||||
throw new IllegalArgumentException("'outputFile' not absolute path");
|
throw new IllegalArgumentException("'outputFile' not absolute path");
|
||||||
|
|
||||||
try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW)) {
|
try (OutputStream out = Files.newOutputStream(file, StandardOpenOption.CREATE_NEW)) {
|
||||||
dumpThreads(out, format);
|
dumpThreads(out, format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -25,6 +25,7 @@
|
|||||||
* @test
|
* @test
|
||||||
* @bug 8284161 8287008
|
* @bug 8284161 8287008
|
||||||
* @summary Basic test for jcmd Thread.dump_to_file
|
* @summary Basic test for jcmd Thread.dump_to_file
|
||||||
|
* @modules jdk.jcmd
|
||||||
* @library /test/lib
|
* @library /test/lib
|
||||||
* @run junit/othervm ThreadDumpToFileTest
|
* @run junit/othervm ThreadDumpToFileTest
|
||||||
*/
|
*/
|
||||||
@ -66,7 +67,8 @@ class ThreadDumpToFileTest {
|
|||||||
@Test
|
@Test
|
||||||
void testJsonThreadDump() throws IOException {
|
void testJsonThreadDump() throws IOException {
|
||||||
Path file = genThreadDumpPath(".json");
|
Path file = genThreadDumpPath(".json");
|
||||||
jcmdThreadDumpToFile(file, "-format=json").shouldMatch("Created");
|
jcmdThreadDumpToFile(file, "-format=json")
|
||||||
|
.shouldMatch("Created");
|
||||||
|
|
||||||
// parse the JSON text
|
// parse the JSON text
|
||||||
String jsonText = Files.readString(file);
|
String jsonText = Files.readString(file);
|
||||||
@ -89,7 +91,8 @@ class ThreadDumpToFileTest {
|
|||||||
Path file = genThreadDumpPath(".txt");
|
Path file = genThreadDumpPath(".txt");
|
||||||
Files.writeString(file, "xxx");
|
Files.writeString(file, "xxx");
|
||||||
|
|
||||||
jcmdThreadDumpToFile(file, "").shouldMatch("exists");
|
jcmdThreadDumpToFile(file, "")
|
||||||
|
.shouldMatch("exists");
|
||||||
|
|
||||||
// file should not be overridden
|
// file should not be overridden
|
||||||
assertEquals("xxx", Files.readString(file));
|
assertEquals("xxx", Files.readString(file));
|
||||||
@ -102,7 +105,23 @@ class ThreadDumpToFileTest {
|
|||||||
void testOverwriteFile() throws IOException {
|
void testOverwriteFile() throws IOException {
|
||||||
Path file = genThreadDumpPath(".txt");
|
Path file = genThreadDumpPath(".txt");
|
||||||
Files.writeString(file, "xxx");
|
Files.writeString(file, "xxx");
|
||||||
jcmdThreadDumpToFile(file, "-overwrite");
|
jcmdThreadDumpToFile(file, "-overwrite")
|
||||||
|
.shouldMatch("Created");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test output file cannot be created.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testFileCreateFails() throws IOException {
|
||||||
|
Path badFile = Path.of(".").toAbsolutePath()
|
||||||
|
.resolve("does-not-exist")
|
||||||
|
.resolve("does-not-exist")
|
||||||
|
.resolve("threads.bad");
|
||||||
|
jcmdThreadDumpToFile(badFile, "-format=plain")
|
||||||
|
.shouldMatch("Failed");
|
||||||
|
jcmdThreadDumpToFile(badFile, "-format=json")
|
||||||
|
.shouldMatch("Failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -21,41 +21,64 @@
|
|||||||
* questions.
|
* questions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* @test
|
* @test
|
||||||
* @bug 8284161 8287008 8309406
|
* @bug 8284161 8287008 8309406 8356870
|
||||||
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
|
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
|
||||||
* @requires vm.continuations
|
* @requires vm.continuations
|
||||||
* @modules jdk.management
|
* @modules java.base/jdk.internal.vm jdk.management
|
||||||
* @library /test/lib
|
* @library /test/lib
|
||||||
* @run junit/othervm DumpThreads
|
* @build jdk.test.whitebox.WhiteBox
|
||||||
* @run junit/othervm -Djdk.trackAllThreads DumpThreads
|
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
|
||||||
* @run junit/othervm -Djdk.trackAllThreads=true DumpThreads
|
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
||||||
* @run junit/othervm -Djdk.trackAllThreads=false DumpThreads
|
* --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.management.ManagementFactory;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.FileAlreadyExistsException;
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
|
||||||
import java.time.ZonedDateTime;
|
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.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.ForkJoinPool;
|
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.LockSupport;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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;
|
||||||
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
|
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
|
||||||
import jdk.test.lib.threaddump.ThreadDump;
|
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.Test;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.*;
|
||||||
|
|
||||||
class DumpThreads {
|
class DumpThreads {
|
||||||
private static boolean trackAllThreads;
|
private static boolean trackAllThreads;
|
||||||
@ -64,6 +87,56 @@ class DumpThreads {
|
|||||||
static void setup() throws Exception {
|
static void setup() throws Exception {
|
||||||
String s = System.getProperty("jdk.trackAllThreads");
|
String s = System.getProperty("jdk.trackAllThreads");
|
||||||
trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,180 +151,438 @@ class DumpThreads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test thread dump in plain text format contains information about the current
|
* Test that a thread container for an executor service is in the JSON format thread dump.
|
||||||
* thread and a virtual thread created directly with the Thread API.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void testRootContainerPlainTextFormat() throws Exception {
|
|
||||||
Thread vthread = Thread.ofVirtual().start(LockSupport::park);
|
|
||||||
try {
|
|
||||||
testDumpThreadsPlainText(vthread, trackAllThreads);
|
|
||||||
} finally {
|
|
||||||
LockSupport.unpark(vthread);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test thread dump in JSON format contains information about the current
|
|
||||||
* thread and a virtual thread created directly with the Thread API.
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void testRootContainerJsonFormat() throws Exception {
|
|
||||||
Thread vthread = Thread.ofVirtual().start(LockSupport::park);
|
|
||||||
try {
|
|
||||||
testDumpThreadsJson(null, vthread, trackAllThreads);
|
|
||||||
} finally {
|
|
||||||
LockSupport.unpark(vthread);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test thread dump in plain text format includes a thread executing a task in the
|
|
||||||
* given ExecutorService.
|
|
||||||
*/
|
*/
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("executors")
|
@MethodSource("executors")
|
||||||
void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception {
|
void testThreadContainer(ExecutorService executor) throws Exception {
|
||||||
try (executor) {
|
try (executor) {
|
||||||
Thread thread = forkParker(executor);
|
testThreadContainer(executor, Objects.toIdentityString(executor));
|
||||||
try {
|
|
||||||
testDumpThreadsPlainText(thread, true);
|
|
||||||
} finally {
|
|
||||||
LockSupport.unpark(thread);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test thread dump in JSON format includes a thread executing a task in the
|
* Test that a thread container for the common pool is in the JSON format thread dump.
|
||||||
* given ExecutorService.
|
|
||||||
*/
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("executors")
|
|
||||||
void testExecutorServiceJsonFormat(ExecutorService executor) throws Exception {
|
|
||||||
try (executor) {
|
|
||||||
Thread thread = forkParker(executor);
|
|
||||||
try {
|
|
||||||
testDumpThreadsJson(Objects.toIdentityString(executor), thread, true);
|
|
||||||
} finally {
|
|
||||||
LockSupport.unpark(thread);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test thread dump in JSON format includes a thread executing a task in the
|
|
||||||
* fork-join common pool.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testForkJoinPool() throws Exception {
|
void testCommonPool() throws Exception {
|
||||||
ForkJoinPool pool = ForkJoinPool.commonPool();
|
testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
|
||||||
Thread thread = forkParker(pool);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
try {
|
||||||
testDumpThreadsJson("ForkJoinPool.commonPool", thread, true);
|
// 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 {
|
} finally {
|
||||||
LockSupport.unpark(thread);
|
LockSupport.unpark(thread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in plain text
|
* ThreadFactory implementations for tests.
|
||||||
* format, then sanity check that the thread dump includes expected strings, the
|
|
||||||
* current thread, and maybe the given thread.
|
|
||||||
* @param thread the thread to test if included
|
|
||||||
* @param expectInDump true if the thread is expected to be included
|
|
||||||
*/
|
*/
|
||||||
private void testDumpThreadsPlainText(Thread thread, boolean expectInDump) throws Exception {
|
static Stream<ThreadFactory> threadFactories() {
|
||||||
Path file = genOutputPath(".txt");
|
Stream<ThreadFactory> s = Stream.of(Thread.ofPlatform().factory());
|
||||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
if (trackAllThreads) {
|
||||||
mbean.dumpThreads(file.toString(), ThreadDumpFormat.TEXT_PLAIN);
|
return Stream.concat(s, Stream.of(Thread.ofVirtual().factory()));
|
||||||
System.err.format("Dumped to %s%n", file);
|
} else {
|
||||||
|
return s;
|
||||||
// pid should be on the first line
|
}
|
||||||
String line1 = line(file, 0);
|
|
||||||
String pid = Long.toString(ProcessHandle.current().pid());
|
|
||||||
assertTrue(line1.contains(pid));
|
|
||||||
|
|
||||||
// timestamp should be on the second line
|
|
||||||
String line2 = line(file, 1);
|
|
||||||
ZonedDateTime.parse(line2);
|
|
||||||
|
|
||||||
// runtime version should be on third line
|
|
||||||
String line3 = line(file, 2);
|
|
||||||
String vs = Runtime.version().toString();
|
|
||||||
assertTrue(line3.contains(vs));
|
|
||||||
|
|
||||||
// test if thread is included in thread dump
|
|
||||||
assertEquals(expectInDump, isPresent(file, thread));
|
|
||||||
|
|
||||||
// current thread should be included if platform thread or tracking all threads
|
|
||||||
Thread currentThread = Thread.currentThread();
|
|
||||||
boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
|
|
||||||
assertEquals(currentThreadExpected, isPresent(file, currentThread));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in JSON format.
|
* Test thread dump with a thread blocked on monitor enter.
|
||||||
* The thread dump is parsed as a JSON object and checked to ensure that it contains
|
|
||||||
* expected data, the current thread, and maybe the given thread.
|
|
||||||
* @param containerName the name of the container or null for the root container
|
|
||||||
* @param thread the thread to test if included
|
|
||||||
* @param expect true if the thread is expected to be included
|
|
||||||
*/
|
*/
|
||||||
private void testDumpThreadsJson(String containerName,
|
@ParameterizedTest
|
||||||
Thread thread,
|
@MethodSource("threadFactories")
|
||||||
boolean expectInDump) throws Exception {
|
void testBlockedThread(ThreadFactory factory) throws Exception {
|
||||||
Path file = genOutputPath(".json");
|
testBlockedThread(factory, false);
|
||||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
}
|
||||||
mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON);
|
|
||||||
System.err.format("Dumped to %s%n", file);
|
|
||||||
|
|
||||||
// parse the JSON text
|
/**
|
||||||
String jsonText = Files.readString(file);
|
* Test thread dump with a thread blocked on monitor enter when pinned.
|
||||||
ThreadDump threadDump = ThreadDump.parse(jsonText);
|
*/
|
||||||
|
@Test
|
||||||
|
void testBlockedThreadWhenPinned() throws Exception {
|
||||||
|
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||||
|
testBlockedThread(Thread.ofVirtual().factory(), true);
|
||||||
|
}
|
||||||
|
|
||||||
// test threadDump/processId
|
void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception {
|
||||||
assertTrue(threadDump.processId() == ProcessHandle.current().pid());
|
var lock = new Object();
|
||||||
|
var started = new CountDownLatch(1);
|
||||||
|
|
||||||
// test threadDump/time can be parsed
|
Thread thread = factory.newThread(() -> {
|
||||||
ZonedDateTime.parse(threadDump.time());
|
if (pinned) {
|
||||||
|
VThreadPinner.runPinned(() -> {
|
||||||
|
started.countDown();
|
||||||
|
synchronized (lock) { } // blocks
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
started.countDown();
|
||||||
|
synchronized (lock) { } // blocks
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// test threadDump/runtimeVersion
|
try {
|
||||||
assertEquals(Runtime.version().toString(), threadDump.runtimeVersion());
|
synchronized (lock) {
|
||||||
|
// start thread and wait for it to block
|
||||||
|
thread.start();
|
||||||
|
started.await();
|
||||||
|
await(thread, Thread.State.BLOCKED);
|
||||||
|
|
||||||
// test root container, has no parent and no owner
|
long tid = thread.threadId();
|
||||||
var rootContainer = threadDump.rootThreadContainer();
|
String lockAsString = Objects.toIdentityString(lock);
|
||||||
assertFalse(rootContainer.owner().isPresent());
|
|
||||||
assertFalse(rootContainer.parent().isPresent());
|
|
||||||
|
|
||||||
// test that the container contains the given thread
|
// thread dump in plain text should include thread
|
||||||
ThreadDump.ThreadContainer container;
|
List<String> lines = dumpThreadsToPlainText();
|
||||||
if (containerName == null) {
|
ThreadFields fields = findThread(tid, lines);
|
||||||
// root container, the thread should be found if trackAllThreads is true
|
assertNotNull(fields, "thread not found");
|
||||||
container = rootContainer;
|
assertEquals("BLOCKED", fields.state());
|
||||||
} else {
|
assertTrue(contains(lines, "- waiting to lock <" + lockAsString));
|
||||||
// find the container
|
|
||||||
container = threadDump.findThreadContainer(containerName).orElse(null);
|
|
||||||
assertNotNull(container, containerName + " not found");
|
|
||||||
assertFalse(container.owner().isPresent());
|
|
||||||
assertTrue(container.parent().get() == rootContainer);
|
|
||||||
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
boolean found = container.findThread(thread.threadId()).isPresent();
|
}
|
||||||
assertEquals(expectInDump, found);
|
|
||||||
|
|
||||||
// current thread should be in root container if platform thread or tracking all threads
|
/**
|
||||||
Thread currentThread = Thread.currentThread();
|
* Test thread dump with a thread waiting in Object.wait.
|
||||||
boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
|
*/
|
||||||
found = rootContainer.findThread(currentThread.threadId()).isPresent();
|
@ParameterizedTest
|
||||||
assertEquals(currentThreadExpected, found);
|
@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 that dumpThreads throws if the output file already exists.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testFileAlreadyExsists() throws Exception {
|
void testFileAlreadyExists() throws Exception {
|
||||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||||
String file = Files.createFile(genOutputPath("txt")).toString();
|
String file = Files.createFile(genOutputPath("txt")).toString();
|
||||||
assertThrows(FileAlreadyExistsException.class,
|
assertThrows(FileAlreadyExistsException.class,
|
||||||
@ -260,11 +591,44 @@ class DumpThreads {
|
|||||||
() -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
|
() -> 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 that dumpThreads throws if the file path is relative.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testRelativePath() throws Exception {
|
void testRelativePath() {
|
||||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||||
assertThrows(IllegalArgumentException.class,
|
assertThrows(IllegalArgumentException.class,
|
||||||
() -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
|
() -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
|
||||||
@ -276,7 +640,7 @@ class DumpThreads {
|
|||||||
* Test that dumpThreads throws with null parameters.
|
* Test that dumpThreads throws with null parameters.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testNull() throws Exception {
|
void testNull() {
|
||||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||||
assertThrows(NullPointerException.class,
|
assertThrows(NullPointerException.class,
|
||||||
() -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
|
() -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
|
||||||
@ -285,31 +649,63 @@ class DumpThreads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits a parking task to the given executor, returns the Thread object of
|
* Represents the data for a thread found in a plain text thread dump.
|
||||||
* the parked thread.
|
|
||||||
*/
|
*/
|
||||||
private static Thread forkParker(ExecutorService executor) {
|
private record ThreadFields(long tid, String name, boolean isVirtual, String state) { }
|
||||||
class Box { static volatile Thread thread;}
|
|
||||||
var latch = new CountDownLatch(1);
|
/**
|
||||||
executor.submit(() -> {
|
* Find a thread in the lines of a plain text thread dump.
|
||||||
Box.thread = Thread.currentThread();
|
*/
|
||||||
latch.countDown();
|
private ThreadFields findThread(long tid, List<String> lines) {
|
||||||
LockSupport.park();
|
String line = lines.stream()
|
||||||
});
|
.filter(l -> l.startsWith("#" + tid + " "))
|
||||||
try {
|
.findFirst()
|
||||||
latch.await();
|
.orElse(null);
|
||||||
} catch (InterruptedException e) {
|
if (line == null) {
|
||||||
throw new RuntimeException(e);
|
return null;
|
||||||
}
|
}
|
||||||
return Box.thread;
|
|
||||||
|
// #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 a Thread is present in a plain text thread dump.
|
* Returns true if lines of a plain text thread dump contain the given text.
|
||||||
*/
|
*/
|
||||||
private static boolean isPresent(Path file, Thread thread) throws Exception {
|
private boolean contains(List<String> lines, String text) {
|
||||||
String expect = "#" + thread.threadId();
|
return lines.stream().map(String::trim)
|
||||||
return count(file, expect) > 0;
|
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -323,21 +719,23 @@ class DumpThreads {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the count of the number of files in the given file that contain
|
* Waits for the given thread to get to a given state.
|
||||||
* the given character sequence.
|
|
||||||
*/
|
*/
|
||||||
static long count(Path file, CharSequence cs) throws Exception {
|
private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
|
||||||
try (Stream<String> stream = Files.lines(file)) {
|
Thread.State state = thread.getState();
|
||||||
return stream.filter(line -> line.contains(cs)).count();
|
while (state != expectedState) {
|
||||||
|
assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
|
||||||
|
Thread.sleep(10);
|
||||||
|
state = thread.getState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return line $n of the given file.
|
* Waits for the boolean value to become true.
|
||||||
*/
|
*/
|
||||||
private String line(Path file, long n) throws Exception {
|
private static void awaitTrue(AtomicBoolean ref) throws Exception {
|
||||||
try (Stream<String> stream = Files.lines(file)) {
|
while (!ref.get()) {
|
||||||
return stream.skip(n).findFirst().orElseThrow();
|
Thread.sleep(20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 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 8356870
|
||||||
|
* @summary Test HotSpotDiagnosticMXBean.dumpThreads with a thread owning a monitor for
|
||||||
|
* an object that is scalar replaced
|
||||||
|
* @requires !vm.debug & (vm.compMode != "Xcomp")
|
||||||
|
* @requires (vm.opt.TieredStopAtLevel == null | vm.opt.TieredStopAtLevel == 4)
|
||||||
|
* @modules jdk.management
|
||||||
|
* @library /test/lib
|
||||||
|
* @run main/othervm DumpThreadsWithEliminatedLock plain platform
|
||||||
|
* @run main/othervm DumpThreadsWithEliminatedLock plain virtual
|
||||||
|
* @run main/othervm DumpThreadsWithEliminatedLock json platform
|
||||||
|
* @run main/othervm DumpThreadsWithEliminatedLock json virtual
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||||
|
import jdk.test.lib.threaddump.ThreadDump;
|
||||||
|
import jdk.test.lib.thread.VThreadRunner;
|
||||||
|
|
||||||
|
public class DumpThreadsWithEliminatedLock {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
boolean plain = switch (args[0]) {
|
||||||
|
case "plain" -> true;
|
||||||
|
case "json" -> false;
|
||||||
|
default -> throw new RuntimeException("Unknown dump format");
|
||||||
|
};
|
||||||
|
|
||||||
|
ThreadFactory factory = switch (args[1]) {
|
||||||
|
case "platform" -> Thread.ofPlatform().factory();
|
||||||
|
case "virtual" -> Thread.ofVirtual().factory();
|
||||||
|
default -> throw new RuntimeException("Unknown thread kind");
|
||||||
|
};
|
||||||
|
|
||||||
|
// need at least two carriers for JTREG_TEST_THREAD_FACTORY=Virtual
|
||||||
|
if (Thread.currentThread().isVirtual()) {
|
||||||
|
VThreadRunner.ensureParallelism(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A thread that spins creating and adding to a StringBuffer. StringBuffer is
|
||||||
|
// synchronized, assume object will be scalar replaced and the lock eliminated.
|
||||||
|
var done = new AtomicBoolean();
|
||||||
|
var ref = new AtomicReference<String>();
|
||||||
|
Thread thread = factory.newThread(() -> {
|
||||||
|
while (!done.get()) {
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
sb.append(System.currentTimeMillis());
|
||||||
|
String s = sb.toString();
|
||||||
|
ref.set(s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
thread.start();
|
||||||
|
if (plain) {
|
||||||
|
testPlainFormat();
|
||||||
|
} else {
|
||||||
|
testJsonFormat(thread.threadId());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
done.set(true);
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke HotSpotDiagnosticMXBean.dumpThreads to generate a thread dump in plain text
|
||||||
|
* format until "lock is eliminated" is found in the output.
|
||||||
|
*/
|
||||||
|
private static void testPlainFormat() {
|
||||||
|
try {
|
||||||
|
Path file = genOutputPath(".txt");
|
||||||
|
boolean found = false;
|
||||||
|
int attempts = 0;
|
||||||
|
while (!found) {
|
||||||
|
attempts++;
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
|
||||||
|
.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
|
||||||
|
try (Stream<String> stream = Files.lines(file)) {
|
||||||
|
found = stream.map(String::trim)
|
||||||
|
.anyMatch(l -> l.contains("- lock is eliminated"));
|
||||||
|
}
|
||||||
|
System.out.format("%s Attempt %d, found: %b%n", Instant.now(), attempts, found);
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new UncheckedIOException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke HotSpotDiagnosticMXBean.dumpThreads to generate a thread dump in JSON format
|
||||||
|
* until the monitorsOwned.locks array for the given thread has a null lock.
|
||||||
|
*/
|
||||||
|
private static void testJsonFormat(long tid) {
|
||||||
|
try {
|
||||||
|
Path file = genOutputPath(".json");
|
||||||
|
boolean found = false;
|
||||||
|
int attempts = 0;
|
||||||
|
while (!found) {
|
||||||
|
attempts++;
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
|
||||||
|
.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
|
||||||
|
|
||||||
|
// parse thread dump as JSON and find thread
|
||||||
|
String jsonText = Files.readString(file);
|
||||||
|
ThreadDump threadDump = ThreadDump.parse(jsonText);
|
||||||
|
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||||
|
.findThread(tid)
|
||||||
|
.orElse(null);
|
||||||
|
if (ti == null) {
|
||||||
|
throw new RuntimeException("Thread " + tid + " not found in thread dump");
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for null element in ownedMonitors/locks array
|
||||||
|
found = ti.ownedMonitors()
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.anyMatch(o -> o == null);
|
||||||
|
System.out.format("%s Attempt %d, found: %b%n", Instant.now(), attempts, found);
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new UncheckedIOException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a file path with the given suffix to use as an output file.
|
||||||
|
*/
|
||||||
|
private static Path genOutputPath(String suffix) throws IOException {
|
||||||
|
Path dir = Path.of(".").toAbsolutePath();
|
||||||
|
Path file = Files.createTempFile(dir, "dump", suffix);
|
||||||
|
Files.delete(file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -26,6 +26,7 @@ package jdk.test.lib.threaddump;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -62,6 +63,7 @@ import jdk.test.lib.json.JSONValue;
|
|||||||
* {
|
* {
|
||||||
* "tid": "8",
|
* "tid": "8",
|
||||||
* "name": "Reference Handler",
|
* "name": "Reference Handler",
|
||||||
|
* "state": "RUNNABLE",
|
||||||
* "stack": [
|
* "stack": [
|
||||||
* "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
|
* "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
|
||||||
* "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:245)",
|
* "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:245)",
|
||||||
@ -113,23 +115,46 @@ import jdk.test.lib.json.JSONValue;
|
|||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
public final class ThreadDump {
|
public final class ThreadDump {
|
||||||
private final long processId;
|
private final ThreadContainer rootThreadContainer;
|
||||||
private final String time;
|
private final Map<String, ThreadContainer> nameToThreadContainer;
|
||||||
private final String runtimeVersion;
|
private final JSONValue threadDumpObj;
|
||||||
private ThreadContainer rootThreadContainer;
|
|
||||||
|
private ThreadDump(ThreadContainer rootThreadContainer,
|
||||||
|
Map<String, ThreadContainer> nameToThreadContainer,
|
||||||
|
JSONValue threadDumpObj) {
|
||||||
|
this.rootThreadContainer = rootThreadContainer;
|
||||||
|
this.nameToThreadContainer = nameToThreadContainer;
|
||||||
|
this.threadDumpObj = threadDumpObj;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an element in the threadDump/threadContainers array.
|
* Represents an element in the threadDump/threadContainers array.
|
||||||
*/
|
*/
|
||||||
public static class ThreadContainer {
|
public static class ThreadContainer {
|
||||||
private final String name;
|
private final String name;
|
||||||
private long owner;
|
private final ThreadContainer parent;
|
||||||
private ThreadContainer parent;
|
|
||||||
private Set<ThreadInfo> threads;
|
|
||||||
private final Set<ThreadContainer> children = new HashSet<>();
|
private final Set<ThreadContainer> children = new HashSet<>();
|
||||||
|
private final JSONValue containerObj;
|
||||||
|
|
||||||
ThreadContainer(String name) {
|
ThreadContainer(String name, ThreadContainer parent, JSONValue containerObj) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.parent = parent;
|
||||||
|
this.containerObj = containerObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a child thread container.
|
||||||
|
*/
|
||||||
|
void addChild(ThreadContainer container) {
|
||||||
|
children.add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of a property of this thread container, as a string.
|
||||||
|
*/
|
||||||
|
private String getStringProperty(String propertyName) {
|
||||||
|
JSONValue value = containerObj.get(propertyName);
|
||||||
|
return (value != null) ? value.asString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +168,10 @@ public final class ThreadDump {
|
|||||||
* Return the thread identifier of the owner or empty OptionalLong if not owned.
|
* Return the thread identifier of the owner or empty OptionalLong if not owned.
|
||||||
*/
|
*/
|
||||||
public OptionalLong owner() {
|
public OptionalLong owner() {
|
||||||
return (owner != 0) ? OptionalLong.of(owner) : OptionalLong.empty();
|
String owner = getStringProperty("owner");
|
||||||
|
return (owner != null)
|
||||||
|
? OptionalLong.of(Long.parseLong(owner))
|
||||||
|
: OptionalLong.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,7 +192,12 @@ public final class ThreadDump {
|
|||||||
* Returns a stream of {@code ThreadInfo} objects for the threads in this container.
|
* Returns a stream of {@code ThreadInfo} objects for the threads in this container.
|
||||||
*/
|
*/
|
||||||
public Stream<ThreadInfo> threads() {
|
public Stream<ThreadInfo> threads() {
|
||||||
return threads.stream();
|
JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray();
|
||||||
|
Set<ThreadInfo> threadInfos = new HashSet<>();
|
||||||
|
for (JSONValue threadObj : threadsObj) {
|
||||||
|
threadInfos.add(new ThreadInfo(threadObj));
|
||||||
|
}
|
||||||
|
return threadInfos.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,21 +209,6 @@ public final class ThreadDump {
|
|||||||
.findAny();
|
.findAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to recursively find a container with the given name.
|
|
||||||
*/
|
|
||||||
ThreadContainer findThreadContainer(String name) {
|
|
||||||
if (name().equals(name))
|
|
||||||
return this;
|
|
||||||
if (name().startsWith(name + "/"))
|
|
||||||
return this;
|
|
||||||
return children()
|
|
||||||
.map(c -> c.findThreadContainer(name))
|
|
||||||
.filter(c -> c != null)
|
|
||||||
.findAny()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return name.hashCode();
|
return name.hashCode();
|
||||||
@ -216,13 +234,30 @@ public final class ThreadDump {
|
|||||||
*/
|
*/
|
||||||
public static final class ThreadInfo {
|
public static final class ThreadInfo {
|
||||||
private final long tid;
|
private final long tid;
|
||||||
private final String name;
|
private final JSONValue threadObj;
|
||||||
private final List<String> stack;
|
|
||||||
|
|
||||||
ThreadInfo(long tid, String name, List<String> stack) {
|
ThreadInfo(JSONValue threadObj) {
|
||||||
this.tid = tid;
|
this.tid = Long.parseLong(threadObj.get("tid").asString());
|
||||||
this.name = name;
|
this.threadObj = threadObj;
|
||||||
this.stack = stack;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of a property of this thread object, as a string.
|
||||||
|
*/
|
||||||
|
private String getStringProperty(String propertyName) {
|
||||||
|
JSONValue value = threadObj.get(propertyName);
|
||||||
|
return (value != null) ? value.asString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of a property of an object in this thread object, as a string.
|
||||||
|
*/
|
||||||
|
private String getStringProperty(String objectName, String propertyName) {
|
||||||
|
if (threadObj.get(objectName) instanceof JSONValue.JSONObject obj
|
||||||
|
&& obj.get(propertyName) instanceof JSONValue value) {
|
||||||
|
return value.asString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,16 +271,86 @@ public final class ThreadDump {
|
|||||||
* Returns the thread name.
|
* Returns the thread name.
|
||||||
*/
|
*/
|
||||||
public String name() {
|
public String name() {
|
||||||
return name;
|
return getStringProperty("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the thread state.
|
||||||
|
*/
|
||||||
|
public String state() {
|
||||||
|
return getStringProperty("state");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if virtual thread.
|
||||||
|
*/
|
||||||
|
public boolean isVirtual() {
|
||||||
|
String s = getStringProperty("virtual");
|
||||||
|
return (s != null) ? Boolean.parseBoolean(s) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the thread's parkBlocker.
|
||||||
|
*/
|
||||||
|
public String parkBlocker() {
|
||||||
|
return getStringProperty("parkBlocker", "object");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the object that the thread is blocked entering its monitor.
|
||||||
|
*/
|
||||||
|
public String blockedOn() {
|
||||||
|
return getStringProperty("blockedOn");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the object that is the therad is waiting on with Object.wait.
|
||||||
|
*/
|
||||||
|
public String waitingOn() {
|
||||||
|
return getStringProperty("waitingOn");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the thread stack.
|
* Returns the thread stack.
|
||||||
*/
|
*/
|
||||||
public Stream<String> stack() {
|
public Stream<String> stack() {
|
||||||
|
JSONValue.JSONArray stackObj = threadObj.get("stack").asArray();
|
||||||
|
List<String> stack = new ArrayList<>();
|
||||||
|
for (JSONValue steObject : stackObj) {
|
||||||
|
stack.add(steObject.asString());
|
||||||
|
}
|
||||||
return stack.stream();
|
return stack.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a map of monitors owned.
|
||||||
|
*/
|
||||||
|
public Map<Integer, List<String>> ownedMonitors() {
|
||||||
|
Map<Integer, List<String>> ownedMonitors = new HashMap<>();
|
||||||
|
JSONValue monitorsOwnedObj = threadObj.get("monitorsOwned");
|
||||||
|
if (monitorsOwnedObj != null) {
|
||||||
|
for (JSONValue obj : monitorsOwnedObj.asArray()) {
|
||||||
|
int depth = Integer.parseInt(obj.get("depth").asString());
|
||||||
|
for (JSONValue lock : obj.get("locks").asArray()) {
|
||||||
|
ownedMonitors.computeIfAbsent(depth, _ -> new ArrayList<>())
|
||||||
|
.add(lock.asString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ownedMonitors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the thread is a mounted virtual thread, return the thread identifier of
|
||||||
|
* its carrier.
|
||||||
|
*/
|
||||||
|
public OptionalLong carrier() {
|
||||||
|
String s = getStringProperty("carrier");
|
||||||
|
return (s != null)
|
||||||
|
? OptionalLong.of(Long.parseLong(s))
|
||||||
|
: OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Long.hashCode(tid);
|
return Long.hashCode(tid);
|
||||||
@ -264,84 +369,42 @@ public final class ThreadDump {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder("#");
|
StringBuilder sb = new StringBuilder("#");
|
||||||
sb.append(tid);
|
sb.append(tid);
|
||||||
|
String name = name();
|
||||||
if (name.length() > 0) {
|
if (name.length() > 0) {
|
||||||
sb.append(",");
|
sb.append(",")
|
||||||
sb.append(name);
|
.append(name);
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the given JSON text as a thread dump.
|
* Returns the value of a property of this thread dump, as a string.
|
||||||
*/
|
*/
|
||||||
private ThreadDump(String json) {
|
private String getStringProperty(String propertyName) {
|
||||||
JSONValue threadDumpObj = JSONValue.parse(json).get("threadDump");
|
JSONValue value = threadDumpObj.get(propertyName);
|
||||||
|
return (value != null) ? value.asString() : null;
|
||||||
// maps container name to ThreadContainer
|
|
||||||
Map<String, ThreadContainer> map = new HashMap<>();
|
|
||||||
|
|
||||||
// threadContainers array
|
|
||||||
JSONValue threadContainersObj = threadDumpObj.get("threadContainers");
|
|
||||||
for (JSONValue containerObj : threadContainersObj.asArray()) {
|
|
||||||
String name = containerObj.get("container").asString();
|
|
||||||
String parentName = containerObj.get("parent").asString();
|
|
||||||
String owner = containerObj.get("owner").asString();
|
|
||||||
JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray();
|
|
||||||
|
|
||||||
// threads array
|
|
||||||
Set<ThreadInfo> threadInfos = new HashSet<>();
|
|
||||||
for (JSONValue threadObj : threadsObj) {
|
|
||||||
long tid = Long.parseLong(threadObj.get("tid").asString());
|
|
||||||
String threadName = threadObj.get("name").asString();
|
|
||||||
JSONValue.JSONArray stackObj = threadObj.get("stack").asArray();
|
|
||||||
List<String> stack = new ArrayList<>();
|
|
||||||
for (JSONValue steObject : stackObj) {
|
|
||||||
stack.add(steObject.asString());
|
|
||||||
}
|
|
||||||
threadInfos.add(new ThreadInfo(tid, threadName, stack));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add to map if not already encountered
|
|
||||||
var container = map.computeIfAbsent(name, k -> new ThreadContainer(name));
|
|
||||||
if (owner != null)
|
|
||||||
container.owner = Long.parseLong(owner);
|
|
||||||
container.threads = threadInfos;
|
|
||||||
|
|
||||||
if (parentName == null) {
|
|
||||||
rootThreadContainer = container;
|
|
||||||
} else {
|
|
||||||
// add parent to map if not already encountered and add to its set of children
|
|
||||||
var parent = map.computeIfAbsent(parentName, k -> new ThreadContainer(parentName));
|
|
||||||
container.parent = parent;
|
|
||||||
parent.children.add(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processId = Long.parseLong(threadDumpObj.get("processId").asString());
|
|
||||||
this.time = threadDumpObj.get("time").asString();
|
|
||||||
this.runtimeVersion = threadDumpObj.get("runtimeVersion").asString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of threadDump/processId.
|
* Returns the value of threadDump/processId.
|
||||||
*/
|
*/
|
||||||
public long processId() {
|
public long processId() {
|
||||||
return processId;
|
return Long.parseLong(getStringProperty("processId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of threadDump/time.
|
* Returns the value of threadDump/time.
|
||||||
*/
|
*/
|
||||||
public String time() {
|
public String time() {
|
||||||
return time;
|
return getStringProperty("time");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of threadDump/runtimeVersion.
|
* Returns the value of threadDump/runtimeVersion.
|
||||||
*/
|
*/
|
||||||
public String runtimeVersion() {
|
public String runtimeVersion() {
|
||||||
return runtimeVersion;
|
return getStringProperty("runtimeVersion");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -355,8 +418,17 @@ public final class ThreadDump {
|
|||||||
* Finds a container in the threadDump/threadContainers array with the given name.
|
* Finds a container in the threadDump/threadContainers array with the given name.
|
||||||
*/
|
*/
|
||||||
public Optional<ThreadContainer> findThreadContainer(String name) {
|
public Optional<ThreadContainer> findThreadContainer(String name) {
|
||||||
ThreadContainer container = rootThreadContainer.findThreadContainer(name);
|
ThreadContainer container = nameToThreadContainer.get(name);
|
||||||
return Optional.ofNullable(container);
|
if (container == null) {
|
||||||
|
// may be name/identity format
|
||||||
|
container = nameToThreadContainer.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> e.getKey().startsWith(name + "/"))
|
||||||
|
.map(e -> e.getValue())
|
||||||
|
.findAny()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
return Optional.of(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -364,6 +436,36 @@ public final class ThreadDump {
|
|||||||
* @throws RuntimeException if an error occurs
|
* @throws RuntimeException if an error occurs
|
||||||
*/
|
*/
|
||||||
public static ThreadDump parse(String json) {
|
public static ThreadDump parse(String json) {
|
||||||
return new ThreadDump(json);
|
JSONValue threadDumpObj = JSONValue.parse(json).get("threadDump");
|
||||||
|
|
||||||
|
// threadContainers array, preserve insertion order (parents are added before children)
|
||||||
|
Map<String, JSONValue> containerObjs = new LinkedHashMap<>();
|
||||||
|
JSONValue threadContainersObj = threadDumpObj.get("threadContainers");
|
||||||
|
for (JSONValue containerObj : threadContainersObj.asArray()) {
|
||||||
|
String name = containerObj.get("container").asString();
|
||||||
|
containerObjs.put(name, containerObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find root and create tree of thread containers
|
||||||
|
ThreadContainer root = null;
|
||||||
|
Map<String, ThreadContainer> map = new HashMap<>();
|
||||||
|
for (String name : containerObjs.keySet()) {
|
||||||
|
JSONValue containerObj = containerObjs.get(name);
|
||||||
|
String parentName = containerObj.get("parent").asString();
|
||||||
|
if (parentName == null) {
|
||||||
|
root = new ThreadContainer(name, null, containerObj);
|
||||||
|
map.put(name, root);
|
||||||
|
} else {
|
||||||
|
var parent = map.get(parentName);
|
||||||
|
if (parent == null) {
|
||||||
|
throw new RuntimeException("Thread container " + name + " found before " + parentName);
|
||||||
|
}
|
||||||
|
var container = new ThreadContainer(name, parent, containerObj);
|
||||||
|
parent.addChild(container);
|
||||||
|
map.put(name, container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThreadDump(root, map, threadDumpObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user