8287008: Improve tests for thread dumps in JSON format

Reviewed-by: cjplummer
This commit is contained in:
Alan Bateman 2022-05-24 06:20:53 +00:00
parent a5caffd4a5
commit 15f15830f0
4 changed files with 421 additions and 32 deletions

View File

@ -188,7 +188,7 @@ public class ThreadDumper {
String now = Instant.now().toString();
String runtimeVersion = Runtime.version().toString();
out.format(" \"processId\": %d,%n", processId());
out.format(" \"processId\": \"%d\",%n", processId());
out.format(" \"time\": \"%s\",%n", escape(now));
out.format(" \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
@ -226,7 +226,7 @@ public class ThreadDumper {
if (owner == null) {
out.format(" \"owner\": null,%n");
} else {
out.format(" \"owner\": %d,%n", owner.threadId());
out.format(" \"owner\": \"%d\",%n", owner.threadId());
}
long threadCount = 0;
@ -241,7 +241,7 @@ public class ThreadDumper {
// thread count
threadCount = Long.max(threadCount, container.threadCount());
out.format(" \"threadCount\": %d%n", threadCount);
out.format(" \"threadCount\": \"%d\"%n", threadCount);
if (more) {
out.println(" },");
@ -255,7 +255,7 @@ public class ThreadDumper {
*/
private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
out.println(" {");
out.format(" \"tid\": %d,%n", thread.threadId());
out.format(" \"tid\": \"%d\",%n", thread.threadId());
out.format(" \"name\": \"%s\",%n", escape(thread.getName()));
out.format(" \"stack\": [%n");
int i = 0;

View File

@ -23,6 +23,7 @@
/*
* @test
* @bug 8284161 8287008
* @summary Basic test for jcmd Thread.dump_to_file
* @library /test/lib
* @run testng/othervm ThreadDumpToFileTest
@ -34,6 +35,8 @@ import java.nio.file.Path;
import java.util.stream.Stream;
import jdk.test.lib.dcmd.PidJcmdExecutor;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.threaddump.ThreadDump;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
@ -65,13 +68,17 @@ public class ThreadDumpToFileTest {
Path file = genThreadDumpPath(".json");
threadDump(file, "-format=json").shouldMatch("Created");
// test that the threadDump object is present
assertTrue(find(file, "threadDump"), "`threadDump` not found in " + file);
// parse the JSON text
String jsonText = Files.readString(file);
ThreadDump threadDump = ThreadDump.parse(jsonText);
// test that thread dump contains the id of the current thread
long tid = Thread.currentThread().threadId();
String expected = "\"tid\": " + tid;
assertTrue(find(file, expected), expected + " not found in " + file);
// test that the process id is this process
assertTrue(threadDump.processId() == ProcessHandle.current().pid());
// test that the current thread is in the root thread container
var rootContainer = threadDump.rootThreadContainer();
var tid = Thread.currentThread().threadId();
rootContainer.findThread(tid).orElseThrow();
}
/**

View File

@ -23,18 +23,21 @@
/**
* @test
* @bug 8284161 8287008
* @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
* @compile --enable-preview -source ${jdk.version} DumpThreads.java
* @run testng/othervm --enable-preview DumpThreads
* @run testng/othervm --enable-preview -Djdk.trackAllThreads DumpThreads
* @run testng/othervm --enable-preview -Djdk.trackAllThreads=true DumpThreads
* @run testng/othervm --enable-preview -Djdk.trackAllThreadds=false DumpThreads
* @enablePreview
* @library /test/lib
* @run testng/othervm DumpThreads
* @run testng/othervm -Djdk.trackAllThreads DumpThreads
* @run testng/othervm -Djdk.trackAllThreads=true DumpThreads
* @run testng/othervm -Djdk.trackAllThreadds=false DumpThreads
*/
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@ -42,6 +45,7 @@ import java.util.concurrent.locks.LockSupport;
import java.util.stream.Stream;
import com.sun.management.HotSpotDiagnosticMXBean;
import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
import jdk.test.lib.threaddump.ThreadDump;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
@ -103,21 +107,37 @@ public class DumpThreads {
mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON);
cat(file);
assertTrue(count(file, "threadDump") >= 1L);
assertTrue(count(file, "time") >= 1L);
assertTrue(count(file, "runtimeVersion") >= 1L);
assertTrue(count(file, "threadContainers") >= 1L);
assertTrue(count(file, "threads") >= 1L);
// parse the JSON text
String jsonText = Files.readString(file);
ThreadDump threadDump = ThreadDump.parse(jsonText);
// virtual thread should be found
assertTrue(isJsonPresent(file, vthread));
// test threadDump/processId
assertTrue(threadDump.processId() == ProcessHandle.current().pid());
// if the current thread is a platform thread then it should be included
// test threadDump/time can be parsed
ZonedDateTime.parse(threadDump.time());
// test threadDump/runtimeVersion
assertEquals(threadDump.runtimeVersion(), Runtime.version().toString());
// test root container
var rootContainer = threadDump.rootThreadContainer();
assertFalse(rootContainer.owner().isPresent());
assertFalse(rootContainer.parent().isPresent());
// if the current thread is a platform thread then it will be in root container
Thread currentThread = Thread.currentThread();
if (!currentThread.isVirtual() || TRACK_ALL_THREADS) {
assertTrue(isJsonPresent(file, currentThread));
rootContainer.findThread(currentThread.threadId()).orElseThrow();
}
// find the thread container for the executor. The name of this executor
// is its String representaiton in this case.
String name = executor.toString();
var container = threadDump.findThreadContainer(name).orElseThrow();
assertFalse(container.owner().isPresent());
assertTrue(container.parent().get() == rootContainer);
container.findThread(vthread.threadId()).orElseThrow();
} finally {
LockSupport.unpark(vthread);
}
@ -188,14 +208,6 @@ public class DumpThreads {
return count(file, expect) > 0;
}
/**
* Returns true if the file contains "tid": <tid>
*/
private static boolean isJsonPresent(Path file, Thread thread) throws Exception {
String expect = "\"tid\": " + thread.threadId();
return count(file, expect) > 0;
}
/**
* Generate a file path with the given suffix to use as an output file.
*/

View File

@ -0,0 +1,370 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.test.lib.threaddump;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.stream.Stream;
import jdk.test.lib.json.JSONValue;
/**
* Represents a thread dump that is obtained by parsing JSON text. A thread dump in JSON
* format is generated with the {@code com.sun.management.HotSpotDiagnosticMXBean} API or
* using {@code jcmd <pid> Thread.dump_to_file -format=json <file>}.
*
* <p> The following is an example thread dump that is parsed by this class. Many of the
* objects are collapsed to reduce the size.
*
* <pre>{@code
* {
* "threadDump": {
* "processId": "63406",
* "time": "2022-05-20T07:37:16.308017Z",
* "runtimeVersion": "19",
* "threadContainers": [
* {
* "container": "<root>",
* "parent": null,
* "owner": null,
* "threads": [
* {
* "tid": "1",
* "name": "main",
* "stack": [...]
* },
* {
* "tid": "8",
* "name": "Reference Handler",
* "stack": [
* "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$ReferenceHandler.run(Reference.java:207)"
* ]
* },
* {"name": "Finalizer"...},
* {"name": "Signal Dispatcher"...},
* {"name": "Common-Cleaner"...},
* {"name": "Monitor Ctrl-Break"...},
* {"name": "Notification Thread"...}
* ],
* "threadCount": "7"
* },
* {
* "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@56aac163",
* "parent": "<root>",
* "owner": null,
* "threads": [...],
* "threadCount": "1"
* },
* {
* "container": "java.util.concurrent.ThreadPoolExecutor@20322d26\/jdk.internal.vm.SharedThreadContainer@184f6be2",
* "parent": "<root>",
* "owner": null,
* "threads": [...],
* "threadCount": "1"
* }
* ]
* }
* }
* }</pre>
*
* <p> The following is an example using this class to print the tree of thread containers
* (grouping of threads) and the threads in each container:
*
* <pre>{@code
* void printThreadDump(Path file) throws IOException {
* String json = Files.readString(file);
* ThreadDump dump = ThreadDump.parse(json);
* printThreadContainer(dump.rootThreadContainer(), 0);
* }
*
* void printThreadContainer(ThreadDump.ThreadContainer container, int indent) {
* out.printf("%s%s%n", " ".repeat(indent), container);
* container.threads().forEach(t -> out.printf("%s%s%n", " ".repeat(indent), t.name()));
* container.children().forEach(c -> printThreadContainer(c, indent + 2));
* }
* }</pre>
*/
public final class ThreadDump {
private final long processId;
private final String time;
private final String runtimeVersion;
private ThreadContainer rootThreadContainer;
/**
* 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 Set<ThreadContainer> children = new HashSet<>();
ThreadContainer(String name) {
this.name = name;
}
/**
* Returns the thread container name.
*/
public String name() {
return name;
}
/**
* Return the thread identifier of the owner or empty OptionalLong if not owned.
*/
public OptionalLong owner() {
return (owner != 0) ? OptionalLong.of(owner) : OptionalLong.empty();
}
/**
* Returns the parent thread container or empty Optional if this is the root.
*/
public Optional<ThreadContainer> parent() {
return Optional.ofNullable(parent);
}
/**
* Returns a stream of the children thread containers.
*/
public Stream<ThreadContainer> children() {
return children.stream();
}
/**
* Returns a stream of {@code ThreadInfo} objects for the threads in this container.
*/
public Stream<ThreadInfo> threads() {
return threads.stream();
}
/**
* Finds the thread in this container with the given thread identifier.
*/
public Optional<ThreadInfo> findThread(long tid) {
return threads()
.filter(ti -> ti.tid() == tid)
.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();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ThreadContainer other) {
return name.equals(other.name);
} else {
return false;
}
}
@Override
public String toString() {
return name;
}
}
/**
* Represents an element in the threadDump/threadContainers/threads array.
*/
public static final class ThreadInfo {
private final long tid;
private final String name;
private final List<String> stack;
ThreadInfo(long tid, String name, List<String> stack) {
this.tid = tid;
this.name = name;
this.stack = stack;
}
/**
* Returns the thread identifier.
*/
public long tid() {
return tid;
}
/**
* Returns the thread name.
*/
public String name() {
return name;
}
/**
* Returns the thread stack.
*/
public Stream<String> stack() {
return stack.stream();
}
@Override
public int hashCode() {
return Long.hashCode(tid);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ThreadInfo other) {
return this.tid == other.tid;
} else {
return false;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("#");
sb.append(tid);
if (name.length() > 0) {
sb.append(",");
sb.append(name);
}
return sb.toString();
}
}
/**
* Parses the given JSON text as a thread dump.
*/
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();
}
/**
* Returns the value of threadDump/processId.
*/
public long processId() {
return processId;
}
/**
* Returns the value of threadDump/time.
*/
public String time() {
return time;
}
/**
* Returns the value of threadDump/runtimeVersion.
*/
public String runtimeVersion() {
return runtimeVersion;
}
/**
* Returns the root container in the threadDump/threadContainers array.
*/
public ThreadContainer rootThreadContainer() {
return rootThreadContainer;
}
/**
* 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);
}
/**
* Parses JSON text as a thread dump.
* @throws RuntimeException if an error occurs
*/
public static ThreadDump parse(String json) {
return new ThreadDump(json);
}
}