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.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -24,11 +24,13 @@
|
||||
*/
|
||||
package jdk.internal.vm;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
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.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
@ -36,15 +38,19 @@ import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
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.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Thread dump support.
|
||||
*
|
||||
* This class defines methods to dump threads to an output stream or file in plain
|
||||
* text or JSON format.
|
||||
* This class defines static methods to support the Thread.dump_to_file diagnostic command
|
||||
* 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 {
|
||||
private ThreadDumper() { }
|
||||
@ -53,13 +59,12 @@ public class ThreadDumper {
|
||||
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.
|
||||
*
|
||||
* @param file the file path to the file, null or "-" to return a byte array
|
||||
* @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) {
|
||||
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.
|
||||
*
|
||||
* @param file the file path to the file, null or "-" to return a byte array
|
||||
* @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) {
|
||||
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.
|
||||
* 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) {
|
||||
try (var out = new BoundedByteArrayOutputStream(maxSize);
|
||||
PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
|
||||
var out = new BoundedByteArrayOutputStream(maxSize);
|
||||
try (out; var writer = new TextWriter(out)) {
|
||||
if (json) {
|
||||
dumpThreadsToJson(ps);
|
||||
dumpThreadsToJson(writer);
|
||||
} 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.
|
||||
* 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) {
|
||||
Path path = Path.of(file).toAbsolutePath();
|
||||
@ -110,224 +125,412 @@ public class ThreadDumper {
|
||||
? new OpenOption[0]
|
||||
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
|
||||
String reply;
|
||||
try (OutputStream out = Files.newOutputStream(path, options);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) {
|
||||
if (json) {
|
||||
dumpThreadsToJson(ps);
|
||||
} else {
|
||||
dumpThreads(ps);
|
||||
try (OutputStream out = Files.newOutputStream(path, options)) {
|
||||
try (var writer = new TextWriter(out)) {
|
||||
if (json) {
|
||||
dumpThreadsToJson(writer);
|
||||
} else {
|
||||
dumpThreads(writer);
|
||||
}
|
||||
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 e) {
|
||||
} catch (FileAlreadyExistsException _) {
|
||||
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
|
||||
} catch (IOException ioe) {
|
||||
reply = String.format("Failed: %s%n", ioe);
|
||||
} catch (Exception ex) {
|
||||
reply = String.format("Failed: %s%n", ex);
|
||||
}
|
||||
return reply.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a thread dump in plain text format to the given output stream,
|
||||
* UTF-8 encoded.
|
||||
*
|
||||
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
||||
* Generate a thread dump in plain text format to the given output stream, UTF-8
|
||||
* encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void dumpThreads(OutputStream out) {
|
||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
|
||||
public static void dumpThreads(OutputStream out) throws IOException {
|
||||
var writer = new TextWriter(out);
|
||||
try {
|
||||
dumpThreads(ps);
|
||||
} finally {
|
||||
ps.flush(); // flushes underlying stream
|
||||
dumpThreads(writer);
|
||||
writer.flush();
|
||||
} 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) {
|
||||
ps.println(processId());
|
||||
ps.println(Instant.now());
|
||||
ps.println(Runtime.version());
|
||||
ps.println();
|
||||
dumpThreads(ThreadContainers.root(), ps);
|
||||
private static void dumpThreads(TextWriter writer) {
|
||||
writer.println(processId());
|
||||
writer.println(Instant.now());
|
||||
writer.println(Runtime.version());
|
||||
writer.println();
|
||||
dumpThreads(ThreadContainers.root(), writer);
|
||||
}
|
||||
|
||||
private static void dumpThreads(ThreadContainer container, PrintStream ps) {
|
||||
container.threads().forEach(t -> dumpThread(t, ps));
|
||||
container.children().forEach(c -> dumpThreads(c, ps));
|
||||
private static void dumpThreads(ThreadContainer container, TextWriter writer) {
|
||||
container.threads().forEach(t -> dumpThread(t, writer));
|
||||
container.children().forEach(c -> dumpThreads(c, writer));
|
||||
}
|
||||
|
||||
private static void dumpThread(Thread thread, PrintStream ps) {
|
||||
String suffix = thread.isVirtual() ? " virtual" : "";
|
||||
ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
|
||||
for (StackTraceElement ste : thread.getStackTrace()) {
|
||||
ps.print(" ");
|
||||
ps.println(ste);
|
||||
private static void dumpThread(Thread thread, TextWriter writer) {
|
||||
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
|
||||
Instant now = Instant.now();
|
||||
Thread.State state = snapshot.threadState();
|
||||
writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
|
||||
+ "\" " + (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.
|
||||
*
|
||||
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static void dumpThreadsToJson(OutputStream out) {
|
||||
BufferedOutputStream bos = new BufferedOutputStream(out);
|
||||
PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
|
||||
public static void dumpThreadsToJson(OutputStream out) throws IOException {
|
||||
var writer = new TextWriter(out);
|
||||
try {
|
||||
dumpThreadsToJson(ps);
|
||||
} finally {
|
||||
ps.flush(); // flushes underlying stream
|
||||
dumpThreadsToJson(writer);
|
||||
writer.flush();
|
||||
} 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) {
|
||||
out.println("{");
|
||||
out.println(" \"threadDump\": {");
|
||||
private static void dumpThreadsToJson(TextWriter textWriter) {
|
||||
var jsonWriter = new JsonWriter(textWriter);
|
||||
|
||||
String now = Instant.now().toString();
|
||||
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));
|
||||
jsonWriter.startObject(); // top-level object
|
||||
|
||||
out.println(" \"threadContainers\": [");
|
||||
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
|
||||
jsonWriter.startObject("threadDump");
|
||||
|
||||
out.println(" }"); // end threadDump
|
||||
out.println("}"); // end object
|
||||
jsonWriter.writeProperty("processId", processId());
|
||||
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,
|
||||
PrintStream out,
|
||||
boolean more) {
|
||||
out.println(" {");
|
||||
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()));
|
||||
}
|
||||
private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
|
||||
jsonWriter.startObject();
|
||||
jsonWriter.writeProperty("container", container);
|
||||
jsonWriter.writeProperty("parent", container.parent());
|
||||
|
||||
Thread owner = container.owner();
|
||||
if (owner == null) {
|
||||
out.format(" \"owner\": null,%n");
|
||||
} else {
|
||||
out.format(" \"owner\": \"%d\",%n", owner.threadId());
|
||||
}
|
||||
jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
|
||||
|
||||
long threadCount = 0;
|
||||
out.println(" \"threads\": [");
|
||||
jsonWriter.startArray("threads");
|
||||
Iterator<Thread> threads = container.threads().iterator();
|
||||
while (threads.hasNext()) {
|
||||
Thread thread = threads.next();
|
||||
dumpThreadToJson(thread, out, threads.hasNext());
|
||||
dumpThread(thread, jsonWriter);
|
||||
threadCount++;
|
||||
}
|
||||
out.println(" ],"); // end of threads
|
||||
jsonWriter.endArray(); // threads
|
||||
|
||||
// thread count
|
||||
if (!ThreadContainers.trackAllThreads()) {
|
||||
threadCount = Long.max(threadCount, container.threadCount());
|
||||
}
|
||||
out.format(" \"threadCount\": \"%d\"%n", threadCount);
|
||||
jsonWriter.writeProperty("threadCount", threadCount);
|
||||
|
||||
if (more) {
|
||||
out.println(" },");
|
||||
} else {
|
||||
out.println(" }"); // last container, no trailing comma
|
||||
}
|
||||
jsonWriter.endObject();
|
||||
|
||||
// the children of the thread container follow
|
||||
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) {
|
||||
out.println(" {");
|
||||
out.println(" \"tid\": \"" + thread.threadId() + "\",");
|
||||
out.println(" \"name\": \"" + escape(thread.getName()) + "\",");
|
||||
out.println(" \"stack\": [");
|
||||
private static void dumpThread(Thread thread, JsonWriter jsonWriter) {
|
||||
Instant now = Instant.now();
|
||||
ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
|
||||
Thread.State state = snapshot.threadState();
|
||||
StackTraceElement[] stackTrace = snapshot.stackTrace();
|
||||
|
||||
int i = 0;
|
||||
StackTraceElement[] stackTrace = thread.getStackTrace();
|
||||
while (i < stackTrace.length) {
|
||||
out.print(" \"");
|
||||
out.print(escape(stackTrace[i].toString()));
|
||||
out.print("\"");
|
||||
i++;
|
||||
if (i < stackTrace.length) {
|
||||
out.println(",");
|
||||
} else {
|
||||
out.println(); // last element, no trailing comma
|
||||
jsonWriter.startObject();
|
||||
jsonWriter.writeProperty("tid", thread.threadId());
|
||||
jsonWriter.writeProperty("time", now);
|
||||
if (thread.isVirtual()) {
|
||||
jsonWriter.writeProperty("virtual", Boolean.TRUE);
|
||||
}
|
||||
jsonWriter.writeProperty("name", snapshot.threadName());
|
||||
jsonWriter.writeProperty("state", state);
|
||||
|
||||
// park blocker
|
||||
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(" ]");
|
||||
if (more) {
|
||||
out.println(" },");
|
||||
} else {
|
||||
out.println(" }"); // last thread, no trailing comma
|
||||
private final Deque<Node> stack = new ArrayDeque<>();
|
||||
private final TextWriter writer;
|
||||
|
||||
JsonWriter(TextWriter writer) {
|
||||
this.writer = writer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all thread containers that are "reachable" from
|
||||
* the root container.
|
||||
*/
|
||||
private static List<ThreadContainer> allContainers() {
|
||||
List<ThreadContainer> containers = new ArrayList<>();
|
||||
collect(ThreadContainers.root(), containers);
|
||||
return containers;
|
||||
}
|
||||
private void indent() {
|
||||
int indent = stack.size() * 2;
|
||||
writer.print(" ".repeat(indent));
|
||||
}
|
||||
|
||||
private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
|
||||
containers.add(container);
|
||||
container.children().forEach(c -> collect(c, containers));
|
||||
}
|
||||
/**
|
||||
* Start of object or array.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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);
|
||||
/**
|
||||
* End of object or array.
|
||||
*/
|
||||
private void endObject(boolean isArray) {
|
||||
Node node = stack.pop();
|
||||
if (node.isArray() != isArray)
|
||||
throw new IllegalStateException();
|
||||
if (node.propertyCount() > 0) {
|
||||
writer.println();
|
||||
}
|
||||
indent();
|
||||
writer.print(isArray ? "]" : "}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a property.
|
||||
* @param name the property name, null for an unnamed property
|
||||
* @param obj the value or null
|
||||
*/
|
||||
void writeProperty(String name, Object obj) {
|
||||
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.
|
||||
*/
|
||||
|
@ -52,9 +52,13 @@ class ThreadSnapshot {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
ThreadSnapshot snapshot = create(thread);
|
||||
if (snapshot == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (snapshot.stackTrace == null) {
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* 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
|
||||
* include output for some or all virtual threads.
|
||||
*
|
||||
@ -151,6 +158,7 @@ public interface HotSpotDiagnosticMXBean extends PlatformManagedObject {
|
||||
TEXT_PLAIN,
|
||||
/**
|
||||
* JSON (JavaScript Object Notation) format.
|
||||
* @spec https://datatracker.ietf.org/doc/html/rfc8259 JavaScript Object Notation
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* 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");
|
||||
|
||||
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.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -25,6 +25,7 @@
|
||||
* @test
|
||||
* @bug 8284161 8287008
|
||||
* @summary Basic test for jcmd Thread.dump_to_file
|
||||
* @modules jdk.jcmd
|
||||
* @library /test/lib
|
||||
* @run junit/othervm ThreadDumpToFileTest
|
||||
*/
|
||||
@ -66,7 +67,8 @@ class ThreadDumpToFileTest {
|
||||
@Test
|
||||
void testJsonThreadDump() throws IOException {
|
||||
Path file = genThreadDumpPath(".json");
|
||||
jcmdThreadDumpToFile(file, "-format=json").shouldMatch("Created");
|
||||
jcmdThreadDumpToFile(file, "-format=json")
|
||||
.shouldMatch("Created");
|
||||
|
||||
// parse the JSON text
|
||||
String jsonText = Files.readString(file);
|
||||
@ -89,7 +91,8 @@ class ThreadDumpToFileTest {
|
||||
Path file = genThreadDumpPath(".txt");
|
||||
Files.writeString(file, "xxx");
|
||||
|
||||
jcmdThreadDumpToFile(file, "").shouldMatch("exists");
|
||||
jcmdThreadDumpToFile(file, "")
|
||||
.shouldMatch("exists");
|
||||
|
||||
// file should not be overridden
|
||||
assertEquals("xxx", Files.readString(file));
|
||||
@ -102,7 +105,23 @@ class ThreadDumpToFileTest {
|
||||
void testOverwriteFile() throws IOException {
|
||||
Path file = genThreadDumpPath(".txt");
|
||||
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.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -21,41 +21,64 @@
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/**
|
||||
/*
|
||||
* @test
|
||||
* @bug 8284161 8287008 8309406
|
||||
* @bug 8284161 8287008 8309406 8356870
|
||||
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
|
||||
* @requires vm.continuations
|
||||
* @modules jdk.management
|
||||
* @modules java.base/jdk.internal.vm jdk.management
|
||||
* @library /test/lib
|
||||
* @run junit/othervm DumpThreads
|
||||
* @run junit/othervm -Djdk.trackAllThreads DumpThreads
|
||||
* @run junit/othervm -Djdk.trackAllThreads=true DumpThreads
|
||||
* @run junit/othervm -Djdk.trackAllThreads=false DumpThreads
|
||||
* @build jdk.test.whitebox.WhiteBox
|
||||
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
|
||||
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
||||
* --enable-native-access=ALL-UNNAMED DumpThreads
|
||||
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
||||
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads DumpThreads
|
||||
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
||||
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=true DumpThreads
|
||||
* @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI
|
||||
* --enable-native-access=ALL-UNNAMED -Djdk.trackAllThreads=false DumpThreads
|
||||
*/
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.reflect.Method;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
|
||||
import jdk.test.lib.threaddump.ThreadDump;
|
||||
import jdk.test.lib.thread.VThreadPinner;
|
||||
import jdk.test.lib.thread.VThreadRunner;
|
||||
import jdk.test.whitebox.WhiteBox;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assumptions.*;
|
||||
|
||||
class DumpThreads {
|
||||
private static boolean trackAllThreads;
|
||||
@ -64,6 +87,56 @@ class DumpThreads {
|
||||
static void setup() throws Exception {
|
||||
String s = System.getProperty("jdk.trackAllThreads");
|
||||
trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
|
||||
|
||||
// need >=2 carriers for testing pinning
|
||||
VThreadRunner.ensureParallelism(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump in plain text format.
|
||||
*/
|
||||
@Test
|
||||
void testPlainText() throws Exception {
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
|
||||
// pid should be on the first line
|
||||
String pid = Long.toString(ProcessHandle.current().pid());
|
||||
assertEquals(pid, lines.get(0));
|
||||
|
||||
// timestamp should be on the second line
|
||||
String secondLine = lines.get(1);
|
||||
ZonedDateTime.parse(secondLine);
|
||||
|
||||
// runtime version should be on third line
|
||||
String vs = Runtime.version().toString();
|
||||
assertEquals(vs, lines.get(2));
|
||||
|
||||
// dump should include current thread
|
||||
Thread currentThread = Thread.currentThread();
|
||||
if (trackAllThreads || !currentThread.isVirtual()) {
|
||||
ThreadFields fields = findThread(currentThread.threadId(), lines);
|
||||
assertNotNull(fields, "current thread not found");
|
||||
assertEquals(currentThread.getName(), fields.name());
|
||||
assertEquals(currentThread.isVirtual(), fields.isVirtual());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump in JSON format.
|
||||
*/
|
||||
@Test
|
||||
void testJsonFormat() throws Exception {
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
|
||||
// dump should include current thread in the root container
|
||||
Thread currentThread = Thread.currentThread();
|
||||
if (trackAllThreads || !currentThread.isVirtual()) {
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(currentThread.threadId())
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "current thread not found");
|
||||
assertEquals(currentThread.isVirtual(), ti.isVirtual());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,180 +151,438 @@ class DumpThreads {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump in plain text format contains information about the current
|
||||
* 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.
|
||||
* Test that a thread container for an executor service is in the JSON format thread dump.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("executors")
|
||||
void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception {
|
||||
void testThreadContainer(ExecutorService executor) throws Exception {
|
||||
try (executor) {
|
||||
Thread thread = forkParker(executor);
|
||||
try {
|
||||
testDumpThreadsPlainText(thread, true);
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
testThreadContainer(executor, Objects.toIdentityString(executor));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump in JSON format includes a thread executing a task in the
|
||||
* 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 that a thread container for the common pool is in the JSON format thread dump.
|
||||
*/
|
||||
@Test
|
||||
void testForkJoinPool() throws Exception {
|
||||
ForkJoinPool pool = ForkJoinPool.commonPool();
|
||||
Thread thread = forkParker(pool);
|
||||
void testCommonPool() throws Exception {
|
||||
testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the JSON thread dump has a thread container for the given executor.
|
||||
*/
|
||||
private void testThreadContainer(ExecutorService executor, String name) throws Exception {
|
||||
var threadRef = new AtomicReference<Thread>();
|
||||
|
||||
executor.submit(() -> {
|
||||
threadRef.set(Thread.currentThread());
|
||||
LockSupport.park();
|
||||
});
|
||||
|
||||
// capture Thread
|
||||
Thread thread;
|
||||
while ((thread = threadRef.get()) == null) {
|
||||
Thread.sleep(20);
|
||||
}
|
||||
|
||||
try {
|
||||
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 {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in plain text
|
||||
* 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
|
||||
* ThreadFactory implementations for tests.
|
||||
*/
|
||||
private void testDumpThreadsPlainText(Thread thread, boolean expectInDump) throws Exception {
|
||||
Path file = genOutputPath(".txt");
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
mbean.dumpThreads(file.toString(), ThreadDumpFormat.TEXT_PLAIN);
|
||||
System.err.format("Dumped to %s%n", file);
|
||||
|
||||
// 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));
|
||||
static Stream<ThreadFactory> threadFactories() {
|
||||
Stream<ThreadFactory> s = Stream.of(Thread.ofPlatform().factory());
|
||||
if (trackAllThreads) {
|
||||
return Stream.concat(s, Stream.of(Thread.ofVirtual().factory()));
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in JSON format.
|
||||
* 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
|
||||
* Test thread dump with a thread blocked on monitor enter.
|
||||
*/
|
||||
private void testDumpThreadsJson(String containerName,
|
||||
Thread thread,
|
||||
boolean expectInDump) throws Exception {
|
||||
Path file = genOutputPath(".json");
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON);
|
||||
System.err.format("Dumped to %s%n", file);
|
||||
@ParameterizedTest
|
||||
@MethodSource("threadFactories")
|
||||
void testBlockedThread(ThreadFactory factory) throws Exception {
|
||||
testBlockedThread(factory, false);
|
||||
}
|
||||
|
||||
// parse the JSON text
|
||||
String jsonText = Files.readString(file);
|
||||
ThreadDump threadDump = ThreadDump.parse(jsonText);
|
||||
/**
|
||||
* Test thread dump with a thread blocked on monitor enter when pinned.
|
||||
*/
|
||||
@Test
|
||||
void testBlockedThreadWhenPinned() throws Exception {
|
||||
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||
testBlockedThread(Thread.ofVirtual().factory(), true);
|
||||
}
|
||||
|
||||
// test threadDump/processId
|
||||
assertTrue(threadDump.processId() == ProcessHandle.current().pid());
|
||||
void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception {
|
||||
var lock = new Object();
|
||||
var started = new CountDownLatch(1);
|
||||
|
||||
// test threadDump/time can be parsed
|
||||
ZonedDateTime.parse(threadDump.time());
|
||||
Thread thread = factory.newThread(() -> {
|
||||
if (pinned) {
|
||||
VThreadPinner.runPinned(() -> {
|
||||
started.countDown();
|
||||
synchronized (lock) { } // blocks
|
||||
});
|
||||
} else {
|
||||
started.countDown();
|
||||
synchronized (lock) { } // blocks
|
||||
}
|
||||
});
|
||||
|
||||
// test threadDump/runtimeVersion
|
||||
assertEquals(Runtime.version().toString(), threadDump.runtimeVersion());
|
||||
try {
|
||||
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
|
||||
var rootContainer = threadDump.rootThreadContainer();
|
||||
assertFalse(rootContainer.owner().isPresent());
|
||||
assertFalse(rootContainer.parent().isPresent());
|
||||
long tid = thread.threadId();
|
||||
String lockAsString = Objects.toIdentityString(lock);
|
||||
|
||||
// test that the container contains the given thread
|
||||
ThreadDump.ThreadContainer container;
|
||||
if (containerName == null) {
|
||||
// root container, the thread should be found if trackAllThreads is true
|
||||
container = rootContainer;
|
||||
} else {
|
||||
// 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 plain text should include thread
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
ThreadFields fields = findThread(tid, lines);
|
||||
assertNotNull(fields, "thread not found");
|
||||
assertEquals("BLOCKED", fields.state());
|
||||
assertTrue(contains(lines, "- waiting to lock <" + lockAsString));
|
||||
|
||||
// thread dump in JSON format should include thread in root container
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(tid)
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "thread not found");
|
||||
assertEquals("BLOCKED", ti.state());
|
||||
assertEquals(lockAsString, ti.blockedOn());
|
||||
if (pinned) {
|
||||
long carrierTid = ti.carrier().orElse(-1L);
|
||||
assertNotEquals(-1L, carrierTid, "carrier not found");
|
||||
assertForkJoinWorkerThread(carrierTid);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
thread.join();
|
||||
}
|
||||
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();
|
||||
boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
|
||||
found = rootContainer.findThread(currentThread.threadId()).isPresent();
|
||||
assertEquals(currentThreadExpected, found);
|
||||
/**
|
||||
* Test thread dump with a thread waiting in Object.wait.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("threadFactories")
|
||||
void testWaitingThread(ThreadFactory factory) throws Exception {
|
||||
testWaitingThread(factory, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump with a thread waiting in Object.wait when pinned.
|
||||
*/
|
||||
@Test
|
||||
void testWaitingThreadWhenPinned() throws Exception {
|
||||
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||
testWaitingThread(Thread.ofVirtual().factory(), true);
|
||||
}
|
||||
|
||||
void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception {
|
||||
var lock = new Object();
|
||||
var started = new CountDownLatch(1);
|
||||
|
||||
Thread thread = factory.newThread(() -> {
|
||||
try {
|
||||
synchronized (lock) {
|
||||
if (pinned) {
|
||||
VThreadPinner.runPinned(() -> {
|
||||
started.countDown();
|
||||
lock.wait();
|
||||
});
|
||||
} else {
|
||||
started.countDown();
|
||||
lock.wait();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) { }
|
||||
});
|
||||
|
||||
try {
|
||||
// start thread and wait for it to wait in Object.wait
|
||||
thread.start();
|
||||
started.await();
|
||||
await(thread, Thread.State.WAITING);
|
||||
|
||||
long tid = thread.threadId();
|
||||
String lockAsString = Objects.toIdentityString(lock);
|
||||
|
||||
// thread dump in plain text should include thread
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
ThreadFields fields = findThread(tid, lines);
|
||||
assertNotNull(fields, "thread not found");
|
||||
assertEquals("WAITING", fields.state());
|
||||
|
||||
// thread dump in JSON format should include thread in root container
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(thread.threadId())
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "thread not found");
|
||||
assertEquals(ti.isVirtual(), thread.isVirtual());
|
||||
assertEquals("WAITING", ti.state());
|
||||
if (pinned) {
|
||||
long carrierTid = ti.carrier().orElse(-1L);
|
||||
assertNotEquals(-1L, carrierTid, "carrier not found");
|
||||
assertForkJoinWorkerThread(carrierTid);
|
||||
}
|
||||
|
||||
// Compiled native frames have no locals. If Object.wait0 has been compiled
|
||||
// then we don't have the object that the thread is waiting on
|
||||
Method wait0 = Object.class.getDeclaredMethod("wait0", long.class);
|
||||
boolean expectWaitingOn = !WhiteBox.getWhiteBox().isMethodCompiled(wait0);
|
||||
if (expectWaitingOn) {
|
||||
// plain text dump should have "waiting on" line
|
||||
assertTrue(contains(lines, "- waiting on <" + lockAsString));
|
||||
|
||||
// JSON thread dump should have waitingOn property
|
||||
assertEquals(lockAsString, ti.waitingOn());
|
||||
}
|
||||
|
||||
} finally {
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump with a thread parked on a j.u.c. lock.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("threadFactories")
|
||||
void testParkedThread(ThreadFactory factory) throws Exception {
|
||||
testParkedThread(factory, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump with a thread parked on a j.u.c. lock and pinned.
|
||||
*/
|
||||
@Test
|
||||
void testParkedThreadWhenPinned() throws Exception {
|
||||
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||
testParkedThread(Thread.ofVirtual().factory(), true);
|
||||
}
|
||||
|
||||
void testParkedThread(ThreadFactory factory, boolean pinned) throws Exception {
|
||||
var lock = new ReentrantLock();
|
||||
var started = new CountDownLatch(1);
|
||||
|
||||
Thread thread = factory.newThread(() -> {
|
||||
if (pinned) {
|
||||
VThreadPinner.runPinned(() -> {
|
||||
started.countDown();
|
||||
lock.lock();
|
||||
lock.unlock();
|
||||
});
|
||||
} else {
|
||||
started.countDown();
|
||||
lock.lock();
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
// start thread and wait for it to park
|
||||
thread.start();
|
||||
started.await();
|
||||
await(thread, Thread.State.WAITING);
|
||||
|
||||
long tid = thread.threadId();
|
||||
|
||||
// thread dump in plain text should include thread
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
ThreadFields fields = findThread(tid, lines);
|
||||
assertNotNull(fields, "thread not found");
|
||||
assertEquals("WAITING", fields.state());
|
||||
assertTrue(contains(lines, "- parking to wait for <java.util.concurrent.locks.ReentrantLock"));
|
||||
|
||||
// thread dump in JSON format should include thread in root container
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(thread.threadId())
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "thread not found");
|
||||
assertEquals(ti.isVirtual(), thread.isVirtual());
|
||||
|
||||
// thread should be waiting on the ReentrantLock
|
||||
assertEquals("WAITING", ti.state());
|
||||
String parkBlocker = ti.parkBlocker();
|
||||
assertNotNull(parkBlocker);
|
||||
assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
|
||||
if (pinned) {
|
||||
long carrierTid = ti.carrier().orElse(-1L);
|
||||
assertNotEquals(-1L, carrierTid, "carrier not found");
|
||||
assertForkJoinWorkerThread(carrierTid);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test thread dump with a thread owning a monitor.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("threadFactories")
|
||||
void testThreadOwnsMonitor(ThreadFactory factory) throws Exception {
|
||||
testThreadOwnsMonitor(factory, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testThreadOwnsMonitorWhenPinned() throws Exception {
|
||||
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||
testThreadOwnsMonitor(Thread.ofVirtual().factory(), true);
|
||||
}
|
||||
|
||||
void testThreadOwnsMonitor(ThreadFactory factory, boolean pinned) throws Exception {
|
||||
var lock = new Object();
|
||||
var started = new CountDownLatch(1);
|
||||
|
||||
Thread thread = factory.newThread(() -> {
|
||||
synchronized (lock) {
|
||||
if (pinned) {
|
||||
VThreadPinner.runPinned(() -> {
|
||||
started.countDown();
|
||||
LockSupport.park();
|
||||
});
|
||||
} else {
|
||||
started.countDown();
|
||||
LockSupport.park();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// start thread and wait for it to park
|
||||
thread.start();
|
||||
started.await();
|
||||
await(thread, Thread.State.WAITING);
|
||||
|
||||
long tid = thread.threadId();
|
||||
String lockAsString = Objects.toIdentityString(lock);
|
||||
|
||||
// thread dump in plain text should include thread
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
ThreadFields fields = findThread(tid, lines);
|
||||
assertNotNull(fields, "thread not found");
|
||||
assertEquals("WAITING", fields.state());
|
||||
assertTrue(contains(lines, "- locked <" + lockAsString));
|
||||
|
||||
// thread dump in JSON format should include thread in root container
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(tid)
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "thread not found");
|
||||
assertEquals(ti.isVirtual(), thread.isVirtual());
|
||||
|
||||
// the lock should be in the ownedMonitors array
|
||||
Set<String> ownedMonitors = ti.ownedMonitors().values()
|
||||
.stream()
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toSet());
|
||||
assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test mounted virtual thread.
|
||||
*/
|
||||
@Test
|
||||
void testMountedVirtualThread() throws Exception {
|
||||
assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
|
||||
|
||||
// start virtual thread that spins until done
|
||||
var started = new AtomicBoolean();
|
||||
var done = new AtomicBoolean();
|
||||
var thread = Thread.ofVirtual().start(() -> {
|
||||
started.set(true);
|
||||
while (!done.get()) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// wait for thread to start
|
||||
awaitTrue(started);
|
||||
long tid = thread.threadId();
|
||||
|
||||
// thread dump in plain text should include thread
|
||||
List<String> lines = dumpThreadsToPlainText();
|
||||
ThreadFields fields = findThread(tid, lines);
|
||||
assertNotNull(fields, "thread not found");
|
||||
assertTrue(fields.isVirtual());
|
||||
|
||||
// thread dump in JSON format should include thread in root container
|
||||
ThreadDump threadDump = dumpThreadsToJson();
|
||||
ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
|
||||
.findThread(tid)
|
||||
.orElse(null);
|
||||
assertNotNull(ti, "thread not found");
|
||||
assertTrue(ti.isVirtual());
|
||||
long carrierTid = ti.carrier().orElse(-1L);
|
||||
assertNotEquals(-1L, carrierTid, "carrier not found");
|
||||
assertForkJoinWorkerThread(carrierTid);
|
||||
} finally {
|
||||
done.set(true);
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given thread identifier is a ForkJoinWorkerThread.
|
||||
*/
|
||||
private void assertForkJoinWorkerThread(long tid) {
|
||||
Thread thread = Thread.getAllStackTraces()
|
||||
.keySet()
|
||||
.stream()
|
||||
.filter(t -> t.threadId() == tid)
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
assertNotNull(thread, "thread " + tid + " not found");
|
||||
assertTrue(thread instanceof ForkJoinWorkerThread, "not a ForkJoinWorkerThread");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that dumpThreads throws if the output file already exists.
|
||||
*/
|
||||
@Test
|
||||
void testFileAlreadyExsists() throws Exception {
|
||||
void testFileAlreadyExists() throws Exception {
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
String file = Files.createFile(genOutputPath("txt")).toString();
|
||||
assertThrows(FileAlreadyExistsException.class,
|
||||
@ -260,11 +591,44 @@ class DumpThreads {
|
||||
() -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that dumpThreads throws IOException when the output file cannot be created.
|
||||
*/
|
||||
@Test
|
||||
void testFileCreateFails() {
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
String badFile = Path.of(".").toAbsolutePath()
|
||||
.resolve("does-not-exist")
|
||||
.resolve("does-not-exist")
|
||||
.resolve("threads.bad")
|
||||
.toString();
|
||||
assertThrows(IOException.class,
|
||||
() -> mbean.dumpThreads(badFile, ThreadDumpFormat.TEXT_PLAIN));
|
||||
assertThrows(IOException.class,
|
||||
() -> mbean.dumpThreads(badFile, ThreadDumpFormat.JSON));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that dumpThreads throws IOException if writing to output file fails.
|
||||
*/
|
||||
@Test
|
||||
void testFileWriteFails() {
|
||||
var out = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
throw new IOException("There is not enough space on the disk");
|
||||
}
|
||||
};
|
||||
// need to invoke internal API directly to test this
|
||||
assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreads(out));
|
||||
assertThrows(IOException.class, () -> jdk.internal.vm.ThreadDumper.dumpThreadsToJson(out));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that dumpThreads throws if the file path is relative.
|
||||
*/
|
||||
@Test
|
||||
void testRelativePath() throws Exception {
|
||||
void testRelativePath() {
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
|
||||
@ -276,7 +640,7 @@ class DumpThreads {
|
||||
* Test that dumpThreads throws with null parameters.
|
||||
*/
|
||||
@Test
|
||||
void testNull() throws Exception {
|
||||
void testNull() {
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> 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
|
||||
* the parked thread.
|
||||
* Represents the data for a thread found in a plain text thread dump.
|
||||
*/
|
||||
private static Thread forkParker(ExecutorService executor) {
|
||||
class Box { static volatile Thread thread;}
|
||||
var latch = new CountDownLatch(1);
|
||||
executor.submit(() -> {
|
||||
Box.thread = Thread.currentThread();
|
||||
latch.countDown();
|
||||
LockSupport.park();
|
||||
});
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
private record ThreadFields(long tid, String name, boolean isVirtual, String state) { }
|
||||
|
||||
/**
|
||||
* Find a thread in the lines of a plain text thread dump.
|
||||
*/
|
||||
private ThreadFields findThread(long tid, List<String> lines) {
|
||||
String line = lines.stream()
|
||||
.filter(l -> l.startsWith("#" + tid + " "))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (line == null) {
|
||||
return null;
|
||||
}
|
||||
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 {
|
||||
String expect = "#" + thread.threadId();
|
||||
return count(file, expect) > 0;
|
||||
private boolean contains(List<String> lines, String text) {
|
||||
return lines.stream().map(String::trim)
|
||||
.anyMatch(l -> l.contains(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump threads to a file in plain text format, return the lines in the file.
|
||||
*/
|
||||
private List<String> dumpThreadsToPlainText() throws Exception {
|
||||
Path file = genOutputPath(".txt");
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
|
||||
System.err.format("Dumped to %s%n", file.getFileName());
|
||||
List<String> lines = Files.readAllLines(file);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump threads to a file in JSON format, parse and return as JSON object.
|
||||
*/
|
||||
private static ThreadDump dumpThreadsToJson() throws Exception {
|
||||
Path file = genOutputPath(".json");
|
||||
var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
|
||||
mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
|
||||
System.err.format("Dumped to %s%n", file.getFileName());
|
||||
String jsonText = Files.readString(file);
|
||||
return ThreadDump.parse(jsonText);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -323,21 +719,23 @@ class DumpThreads {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of the number of files in the given file that contain
|
||||
* the given character sequence.
|
||||
* Waits for the given thread to get to a given state.
|
||||
*/
|
||||
static long count(Path file, CharSequence cs) throws Exception {
|
||||
try (Stream<String> stream = Files.lines(file)) {
|
||||
return stream.filter(line -> line.contains(cs)).count();
|
||||
private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
|
||||
Thread.State state = thread.getState();
|
||||
while (state != expectedState) {
|
||||
assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
|
||||
Thread.sleep(10);
|
||||
state = thread.getState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return line $n of the given file.
|
||||
* Waits for the boolean value to become true.
|
||||
*/
|
||||
private String line(Path file, long n) throws Exception {
|
||||
try (Stream<String> stream = Files.lines(file)) {
|
||||
return stream.skip(n).findFirst().orElseThrow();
|
||||
private static void awaitTrue(AtomicBoolean ref) throws Exception {
|
||||
while (!ref.get()) {
|
||||
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.
|
||||
*
|
||||
* 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.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -62,6 +63,7 @@ import jdk.test.lib.json.JSONValue;
|
||||
* {
|
||||
* "tid": "8",
|
||||
* "name": "Reference Handler",
|
||||
* "state": "RUNNABLE",
|
||||
* "stack": [
|
||||
* "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)",
|
||||
* "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:245)",
|
||||
@ -113,23 +115,46 @@ import jdk.test.lib.json.JSONValue;
|
||||
* }</pre>
|
||||
*/
|
||||
public final class ThreadDump {
|
||||
private final long processId;
|
||||
private final String time;
|
||||
private final String runtimeVersion;
|
||||
private ThreadContainer rootThreadContainer;
|
||||
private final ThreadContainer rootThreadContainer;
|
||||
private final Map<String, ThreadContainer> nameToThreadContainer;
|
||||
private final JSONValue threadDumpObj;
|
||||
|
||||
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.
|
||||
*/
|
||||
public static class ThreadContainer {
|
||||
private final String name;
|
||||
private long owner;
|
||||
private ThreadContainer parent;
|
||||
private Set<ThreadInfo> threads;
|
||||
private final ThreadContainer parent;
|
||||
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.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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
@ -216,13 +234,30 @@ public final class ThreadDump {
|
||||
*/
|
||||
public static final class ThreadInfo {
|
||||
private final long tid;
|
||||
private final String name;
|
||||
private final List<String> stack;
|
||||
private final JSONValue threadObj;
|
||||
|
||||
ThreadInfo(long tid, String name, List<String> stack) {
|
||||
this.tid = tid;
|
||||
this.name = name;
|
||||
this.stack = stack;
|
||||
ThreadInfo(JSONValue threadObj) {
|
||||
this.tid = Long.parseLong(threadObj.get("tid").asString());
|
||||
this.threadObj = threadObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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 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
|
||||
public int hashCode() {
|
||||
return Long.hashCode(tid);
|
||||
@ -264,84 +369,42 @@ public final class ThreadDump {
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("#");
|
||||
sb.append(tid);
|
||||
String name = name();
|
||||
if (name.length() > 0) {
|
||||
sb.append(",");
|
||||
sb.append(name);
|
||||
sb.append(",")
|
||||
.append(name);
|
||||
}
|
||||
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) {
|
||||
JSONValue threadDumpObj = JSONValue.parse(json).get("threadDump");
|
||||
|
||||
// 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();
|
||||
private String getStringProperty(String propertyName) {
|
||||
JSONValue value = threadDumpObj.get(propertyName);
|
||||
return (value != null) ? value.asString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of threadDump/processId.
|
||||
*/
|
||||
public long processId() {
|
||||
return processId;
|
||||
return Long.parseLong(getStringProperty("processId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of threadDump/time.
|
||||
*/
|
||||
public String time() {
|
||||
return time;
|
||||
return getStringProperty("time");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of threadDump/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.
|
||||
*/
|
||||
public Optional<ThreadContainer> findThreadContainer(String name) {
|
||||
ThreadContainer container = rootThreadContainer.findThreadContainer(name);
|
||||
return Optional.ofNullable(container);
|
||||
ThreadContainer container = nameToThreadContainer.get(name);
|
||||
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
|
||||
*/
|
||||
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