8380641: Thread dump parsing and test improvements

8378946: threadDump.schema.json syntax error, missing comma after owner

Reviewed-by: amenkov, sspitsyn
This commit is contained in:
Alan Bateman 2026-03-25 07:40:26 +00:00
parent 0423483bfd
commit 3bf5022bc6
5 changed files with 240 additions and 143 deletions

View File

@ -81,7 +81,7 @@
"owner": {
"type": "string",
"description": "The thread identifier of the owner when the parkBlocker is an AbstractOwnableSynchronizer."
}
},
"required": [
"object"
]

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2026, 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
@ -49,6 +49,7 @@ import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
@ -238,6 +239,7 @@ class DumpThreads {
void testBlockedThread(ThreadFactory factory, boolean pinned) throws Exception {
var lock = new Object();
String lockAsString = Objects.toIdentityString(lock);
var started = new CountDownLatch(1);
Thread thread = factory.newThread(() -> {
@ -258,9 +260,7 @@ class DumpThreads {
thread.start();
started.await();
await(thread, Thread.State.BLOCKED);
long tid = thread.threadId();
String lockAsString = Objects.toIdentityString(lock);
// thread dump in plain text should include thread
List<String> lines = dumpThreadsToPlainText();
@ -308,6 +308,7 @@ class DumpThreads {
void testWaitingThread(ThreadFactory factory, boolean pinned) throws Exception {
var lock = new Object();
String lockAsString = Objects.toIdentityString(lock);
var started = new CountDownLatch(1);
Thread thread = factory.newThread(() -> {
@ -331,9 +332,7 @@ class DumpThreads {
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();
@ -417,7 +416,6 @@ class DumpThreads {
thread.start();
started.await();
await(thread, Thread.State.WAITING);
long tid = thread.threadId();
// thread dump in plain text should include thread
@ -460,7 +458,7 @@ class DumpThreads {
}
/**
* Test thread dump with a thread owning a monitor.
* Test thread dump with a thread owning monitors.
*/
@ParameterizedTest
@MethodSource("threadFactories")
@ -475,19 +473,26 @@ class DumpThreads {
}
void testThreadOwnsMonitor(ThreadFactory factory, boolean pinned) throws Exception {
var lock = new Object();
var started = new CountDownLatch(1);
var lock1 = new Object();
var lock2 = new Object();
var lock3 = new Object();
String lock1AsString = Objects.toIdentityString(lock1);
String lock2AsString = Objects.toIdentityString(lock2);
String lock3AsString = Objects.toIdentityString(lock3);
var started = new CountDownLatch(1);
Thread thread = factory.newThread(() -> {
synchronized (lock) {
if (pinned) {
VThreadPinner.runPinned(() -> {
synchronized (lock1) {
synchronized (lock2) {
if (pinned) {
VThreadPinner.runPinned(() -> {
started.countDown();
lockAndRun(lock3, LockSupport::park);
});
} else {
started.countDown();
LockSupport.park();
});
} else {
started.countDown();
LockSupport.park();
lockAndRun(lock3, LockSupport::park);
}
}
}
});
@ -497,16 +502,16 @@ class DumpThreads {
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));
assertTrue(contains(lines, "- locked <" + lock1AsString));
assertTrue(contains(lines, "- locked <" + lock2AsString));
assertTrue(contains(lines, "- locked <" + lock3AsString));
// thread dump in JSON format should include thread in root container
ThreadDump threadDump = dumpThreadsToJson();
@ -516,18 +521,47 @@ class DumpThreads {
assertNotNull(ti, "thread not found");
assertEquals(ti.isVirtual(), thread.isVirtual());
// the lock should be in the ownedMonitors array
Set<String> ownedMonitors = ti.ownedMonitors().values()
// depth -> list of locks
Map<Integer, List<String>> ownedMonitors = ti.ownedMonitors();
// lock -> list of depths
Map<String, List<Integer>> monitorDepths = ownedMonitors.entrySet()
.stream()
.flatMap(List::stream)
.collect(Collectors.toSet());
assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
.flatMap(e -> e.getValue()
.stream()
.map(monitor -> Map.entry(monitor, e.getKey())))
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
// each lock should be owned
List<Integer> lock1Depths = monitorDepths.getOrDefault(lock1AsString, List.of());
List<Integer> lock2Depths = monitorDepths.getOrDefault(lock2AsString, List.of());
List<Integer> lock3Depths = monitorDepths.getOrDefault(lock3AsString, List.of());
assertEquals(1, lock1Depths.size());
assertEquals(1, lock2Depths.size());
assertEquals(1, lock3Depths.size());
// lock1 and lock2 owned at the same depth, lock3 is the innermost
int depth1 = lock1Depths.get(0);
int depth2 = lock2Depths.get(0);
int depth3 = lock3Depths.get(0);
assertEquals(depth1, depth2);
assertTrue(depth3 < depth1);
} finally {
LockSupport.unpark(thread);
thread.join();
}
}
private void lockAndRun(Object lock, Runnable action) {
synchronized (lock) {
action.run();
}
}
/**
* Test mounted virtual thread.
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, 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
@ -88,13 +88,13 @@ public class ML_DSA_Test {
for (var t : kat.get("testGroups").asArray()) {
var pname = t.get("parameterSet").asString();
System.out.println(">> " + pname + " sign");
var det = Boolean.parseBoolean(t.get("deterministic").asString());
var det = t.get("deterministic").asBoolean();
if (t.get("signatureInterface").asString().equals("internal")) {
ML_DSA_Impls.version = ML_DSA_Impls.Version.DRAFT;
} else {
ML_DSA_Impls.version = ML_DSA_Impls.Version.FINAL;
}
if (t.get("externalMu").asString().equals("true")) {
if (t.get("externalMu").asBoolean()) {
continue; // Not supported
}
for (var c : t.get("tests").asArray()) {
@ -139,7 +139,7 @@ public class ML_DSA_Test {
ML_DSA_Impls.version = ML_DSA_Impls.Version.FINAL;
}
if (t.get("externalMu").asString().equals("true")) {
if (t.get("externalMu").asBoolean()) {
continue; // Not supported
}
@ -157,7 +157,7 @@ public class ML_DSA_Test {
public byte[] getEncoded() { return toByteArray(c.get("pk").asString()); }
};
// Only ML-DSA sigVer has negative tests
var expected = Boolean.parseBoolean(c.get("testPassed").asString());
var expected = c.get("testPassed").asBoolean();
var actual = true;
try {
s.initVerify(pk);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2026, 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
@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface JSONValue {
@ -88,9 +89,6 @@ public interface JSONValue {
@Override
public String toString() {
if (value == null) {
return "null";
}
var builder = new StringBuilder();
builder.append("\"");
@ -172,6 +170,56 @@ public interface JSONValue {
public Iterator<JSONValue> iterator() {
return values.iterator();
}
@Override
public List<JSONValue> elements() {
return List.copyOf(values);
}
}
public final class JSONBoolean implements JSONValue {
private static JSONBoolean TRUE = new JSONBoolean(true);
private static JSONBoolean FALSE = new JSONBoolean(false);
private final boolean value;
private JSONBoolean(boolean value) {
this.value = value;
}
@Override
public boolean asBoolean() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
public static JSONBoolean of(boolean value) {
return value ? TRUE : FALSE;
}
}
public final class JSONNull implements JSONValue {
private static JSONNull NULL = new JSONNull();
private JSONNull() {}
@Override
public Optional<JSONValue> valueOrNull() {
return Optional.empty();
}
@Override
public String toString() {
return "null";
}
public static JSONNull of() {
return NULL;
}
}
class JSONParser {
@ -181,8 +229,8 @@ public interface JSONValue {
JSONParser() {
}
private IllegalStateException failure(String message) {
return new IllegalStateException(String.format("[%d]: %s : %s", pos, message, input));
private IllegalArgumentException failure(String message) {
return new IllegalArgumentException(String.format("[%d]: %s : %s", pos, message, input));
}
private char current() {
@ -220,13 +268,13 @@ public interface JSONValue {
}
}
private JSONString parseBoolean() {
private JSONBoolean parseBoolean() {
if (current() == 't') {
expect('r');
expect('u');
expect('e');
advance();
return new JSONString("true");
return JSONBoolean.of(true);
}
if (current() == 'f') {
@ -235,7 +283,7 @@ public interface JSONValue {
expect('s');
expect('e');
advance();
return new JSONString("false");
return JSONBoolean.of(false);
}
throw failure("a boolean can only be 'true' or 'false'");
}
@ -400,12 +448,12 @@ public interface JSONValue {
return new JSONArray(list);
}
public JSONString parseNull() {
public JSONNull parseNull() {
expect('u');
expect('l');
expect('l');
advance();
return new JSONString(null);
return JSONNull.of();
}
public JSONObject parseObject() {
@ -531,22 +579,38 @@ public interface JSONValue {
}
default int size() {
throw new IllegalStateException("Size operation unsupported");
throw new UnsupportedOperationException("Size operation unsupported");
}
default List<JSONValue> elements() {
throw new UnsupportedOperationException("Unsupported conversion to array");
}
default String asString() {
throw new IllegalStateException("Unsupported conversion to String");
throw new UnsupportedOperationException("Unsupported conversion to String");
}
default JSONArray asArray() {
throw new IllegalStateException("Unsupported conversion to array");
throw new UnsupportedOperationException("Unsupported conversion to array");
}
default JSONObject asObject() {
throw new IllegalStateException("Unsupported conversion to object");
throw new UnsupportedOperationException("Unsupported conversion to object");
}
default boolean asBoolean() {
throw new UnsupportedOperationException("Unsupported conversion to boolean");
}
default JSONValue get(String field) {
return asObject().get(field);
}
default Optional<JSONValue> getOrAbsent(String field) {
return Optional.ofNullable(get(field));
}
default Optional<JSONValue> valueOrNull() {
return Optional.of(this);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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
@ -32,13 +32,17 @@ import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
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>}.
* using {@code jcmd <pid> Thread.dump_to_file -format=json <file>}. The thread dump
* format is documented in {@code
* src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json}.
*
* <p> The following is an example thread dump that is parsed by this class. Many of the
* objects are collapsed to reduce the size.
@ -127,6 +131,20 @@ public final class ThreadDump {
this.threadDumpObj = threadDumpObj;
}
/**
* Assert that a JSONValue is a JSONString and parse the string as an int.
*/
private static int parseStringAsInt(JSONValue valueObj) {
return Integer.parseInt(valueObj.asString());
}
/**
* Assert that a JSONValue is a JSONString and parse the string as a long.
*/
private static long parseStringAsLong(JSONValue valueObj) {
return Long.parseLong(valueObj.asString());
}
/**
* Represents an element in the threadDump/threadContainers array.
*/
@ -149,14 +167,6 @@ public final class ThreadDump {
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;
}
/**
* Returns the thread container name.
*/
@ -168,10 +178,10 @@ public final class ThreadDump {
* Return the thread identifier of the owner or empty OptionalLong if not owned.
*/
public OptionalLong owner() {
String owner = getStringProperty("owner");
return (owner != null)
? OptionalLong.of(Long.parseLong(owner))
: OptionalLong.empty();
return containerObj.get("owner") // string or null
.valueOrNull()
.map(v -> OptionalLong.of(parseStringAsLong(v)))
.orElse(OptionalLong.empty());
}
/**
@ -192,12 +202,10 @@ public final class ThreadDump {
* Returns a stream of {@code ThreadInfo} objects for the threads in this container.
*/
public Stream<ThreadInfo> threads() {
JSONValue.JSONArray threadsObj = containerObj.get("threads").asArray();
Set<ThreadInfo> threadInfos = new HashSet<>();
for (JSONValue threadObj : threadsObj) {
threadInfos.add(new ThreadInfo(threadObj));
}
return threadInfos.stream();
return containerObj.get("threads")
.elements()
.stream()
.map(ThreadInfo::new);
}
/**
@ -237,29 +245,10 @@ public final class ThreadDump {
private final JSONValue threadObj;
ThreadInfo(JSONValue threadObj) {
this.tid = Long.parseLong(threadObj.get("tid").asString());
this.tid = parseStringAsLong(threadObj.get("tid"));
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;
}
/**
* Returns the thread identifier.
*/
@ -271,83 +260,92 @@ public final class ThreadDump {
* Returns the thread name.
*/
public String name() {
return getStringProperty("name");
return threadObj.get("name").asString();
}
/**
* Returns the thread state.
*/
public String state() {
return getStringProperty("state");
return threadObj.get("state").asString();
}
/**
* Returns true if virtual thread.
*/
public boolean isVirtual() {
String s = getStringProperty("virtual");
return (s != null) ? Boolean.parseBoolean(s) : false;
return threadObj.getOrAbsent("virtual")
.map(JSONValue::asBoolean)
.orElse(false);
}
/**
* Returns the thread's parkBlocker.
* Returns the thread's parkBlocker or null.
*/
public String parkBlocker() {
return getStringProperty("parkBlocker", "object");
return threadObj.getOrAbsent("parkBlocker")
.map(v -> v.get("object").asString())
.orElse(null);
}
/**
* Returns the owner of the parkBlocker if the parkBlocker is an AbstractOwnableSynchronizer.
*/
public OptionalLong parkBlockerOwner() {
String owner = getStringProperty("parkBlocker", "owner");
return (owner != null)
? OptionalLong.of(Long.parseLong(owner))
: OptionalLong.empty();
return threadObj.getOrAbsent("parkBlocker")
.map(v -> OptionalLong.of(parseStringAsLong(v.get("owner"))))
.orElse(OptionalLong.empty());
}
/**
* Returns the object that the thread is blocked entering its monitor.
* Returns the object that the thread is blocked entering its monitor or null.
*/
public String blockedOn() {
return getStringProperty("blockedOn");
return threadObj.getOrAbsent("blockedOn")
.map(JSONValue::asString)
.orElse(null);
}
/**
* Return the object that is the therad is waiting on with Object.wait.
* Return the object that is the thread is waiting on with Object.wait or null.
*/
public String waitingOn() {
return getStringProperty("waitingOn");
return threadObj.getOrAbsent("waitingOn")
.map(JSONValue::asString)
.orElse(null);
}
/**
* 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 threadObj.get("stack")
.elements()
.stream()
.map(JSONValue::asString);
}
/**
* 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;
Map<Integer, List<String>> result = new HashMap<>();
threadObj.getOrAbsent("monitorsOwned")
.map(JSONValue::elements)
.orElse(List.of())
.forEach(e -> {
int depth = parseStringAsInt(e.get("depth"));
List<String> locks = e.get("locks")
.elements()
.stream()
.map(v -> v.valueOrNull() // string or null
.map(JSONValue::asString)
.orElse(null))
.toList();
result.computeIfAbsent(depth, _ -> new ArrayList<>()).addAll(locks);
});
return result;
}
/**
@ -355,10 +353,9 @@ public final class ThreadDump {
* its carrier.
*/
public OptionalLong carrier() {
String s = getStringProperty("carrier");
return (s != null)
? OptionalLong.of(Long.parseLong(s))
: OptionalLong.empty();
return threadObj.getOrAbsent("carrier")
.map(s -> OptionalLong.of(parseStringAsLong(s)))
.orElse(OptionalLong.empty());
}
@Override
@ -388,33 +385,25 @@ public final class ThreadDump {
}
}
/**
* Returns the value of a property of this thread dump, as a string.
*/
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 Long.parseLong(getStringProperty("processId"));
return parseStringAsLong(threadDumpObj.get("processId"));
}
/**
* Returns the value of threadDump/time.
*/
public String time() {
return getStringProperty("time");
return threadDumpObj.get("time").asString();
}
/**
* Returns the value of threadDump/runtimeVersion.
*/
public String runtimeVersion() {
return getStringProperty("runtimeVersion");
return threadDumpObj.get("runtimeVersion").asString();
}
/**
@ -449,24 +438,31 @@ public final class ThreadDump {
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);
}
Map<String, JSONValue> containerObjs = threadDumpObj.get("threadContainers")
.elements()
.stream()
.collect(Collectors.toMap(
c -> c.get("container").asString(),
Function.identity(),
(a, b) -> { throw new RuntimeException("Duplicate container"); },
LinkedHashMap::new
));
// 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) {
JSONValue parentObj = containerObj.get("parent");
if (parentObj instanceof JSONValue.JSONNull) {
if (root != null) {
throw new RuntimeException("More than one root container");
}
root = new ThreadContainer(name, null, containerObj);
map.put(name, root);
} else {
var parent = map.get(parentName);
String parentName = parentObj.asString();
ThreadContainer parent = map.get(parentName);
if (parent == null) {
throw new RuntimeException("Thread container " + name + " found before " + parentName);
}
@ -475,7 +471,10 @@ public final class ThreadDump {
map.put(name, container);
}
}
if (root == null) {
throw new RuntimeException("No root container");
}
return new ThreadDump(root, map, threadDumpObj);
}
}
}