1 /*
2 * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /**
25 * @test
26 * @bug 8284161 8287008 8309406
27 * @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
28 * @requires vm.continuations
29 * @modules jdk.management
30 * @library /test/lib
31 * @run junit/othervm DumpThreads
32 * @run junit/othervm -Djdk.trackAllThreads DumpThreads
33 * @run junit/othervm -Djdk.trackAllThreads=true DumpThreads
34 * @run junit/othervm -Djdk.trackAllThreads=false DumpThreads
35 */
36
37 import java.lang.management.ManagementFactory;
38 import java.nio.file.Files;
39 import java.nio.file.FileAlreadyExistsException;
40 import java.nio.file.Path;
41 import java.util.Objects;
42 import java.time.ZonedDateTime;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.ExecutorService;
45 import java.util.concurrent.Executors;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.ForkJoinPool;
48 import java.util.concurrent.locks.LockSupport;
49 import java.util.stream.Stream;
50 import com.sun.management.HotSpotDiagnosticMXBean;
51 import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
52 import jdk.test.lib.threaddump.ThreadDump;
53
54 import org.junit.jupiter.api.Test;
55 import org.junit.jupiter.api.BeforeAll;
56 import org.junit.jupiter.params.ParameterizedTest;
57 import org.junit.jupiter.params.provider.MethodSource;
58 import static org.junit.jupiter.api.Assertions.*;
59
60 class DumpThreads {
61 private static boolean trackAllThreads;
62
63 @BeforeAll
64 static void setup() throws Exception {
65 String s = System.getProperty("jdk.trackAllThreads");
66 trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
67 }
68
69 /**
70 * ExecutorService implementations that have their object identity in the container
71 * name so they can be found in the JSON format.
72 */
73 static Stream<ExecutorService> executors() {
74 return Stream.of(
75 Executors.newFixedThreadPool(1),
76 Executors.newVirtualThreadPerTaskExecutor()
77 );
78 }
79
80 /**
81 * Test thread dump in plain text format contains information about the current
82 * thread and a virtual thread created directly with the Thread API.
83 */
84 @Test
85 void testRootContainerPlainTextFormat() throws Exception {
86 Thread vthread = Thread.ofVirtual().start(LockSupport::park);
87 try {
88 testDumpThreadsPlainText(vthread, trackAllThreads);
89 } finally {
90 LockSupport.unpark(vthread);
91 }
92 }
93
94 /**
95 * Test thread dump in JSON format contains information about the current
96 * thread and a virtual thread created directly with the Thread API.
97 */
98 @Test
99 void testRootContainerJsonFormat() throws Exception {
100 Thread vthread = Thread.ofVirtual().start(LockSupport::park);
101 try {
102 testDumpThreadsJson(null, vthread, trackAllThreads);
103 } finally {
104 LockSupport.unpark(vthread);
105 }
106 }
107
108 /**
109 * Test thread dump in plain text format includes a thread executing a task in the
110 * given ExecutorService.
111 */
112 @ParameterizedTest
113 @MethodSource("executors")
114 void testExecutorServicePlainTextFormat(ExecutorService executor) throws Exception {
115 try (executor) {
116 Thread thread = forkParker(executor);
117 try {
118 testDumpThreadsPlainText(thread, true);
119 } finally {
120 LockSupport.unpark(thread);
121 }
122 }
123 }
124
125 /**
126 * Test thread dump in JSON format includes a thread executing a task in the
127 * given ExecutorService.
128 */
129 @ParameterizedTest
130 @MethodSource("executors")
131 void testExecutorServiceJsonFormat(ExecutorService executor) throws Exception {
132 try (executor) {
133 Thread thread = forkParker(executor);
134 try {
135 testDumpThreadsJson(Objects.toIdentityString(executor), thread, true);
136 } finally {
137 LockSupport.unpark(thread);
138 }
139 }
140 }
141
142 /**
143 * Test thread dump in JSON format includes a thread executing a task in the
144 * fork-join common pool.
145 */
146 @Test
147 void testForkJoinPool() throws Exception {
148 ForkJoinPool pool = ForkJoinPool.commonPool();
149 Thread thread = forkParker(pool);
150 try {
151 testDumpThreadsJson("ForkJoinPool.commonPool", thread, true);
152 } finally {
153 LockSupport.unpark(thread);
154 }
155 }
156
157 /**
158 * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in plain text
159 * format, then sanity check that the thread dump includes expected strings, the
160 * current thread, and maybe the given thread.
161 * @param thread the thread to test if included
162 * @param expectInDump true if the thread is expected to be included
163 */
164 private void testDumpThreadsPlainText(Thread thread, boolean expectInDump) throws Exception {
165 Path file = genOutputPath(".txt");
166 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
167 mbean.dumpThreads(file.toString(), ThreadDumpFormat.TEXT_PLAIN);
168 System.err.format("Dumped to %s%n", file);
169
170 // pid should be on the first line
171 String line1 = line(file, 0);
172 String pid = Long.toString(ProcessHandle.current().pid());
173 assertTrue(line1.contains(pid));
174
175 // timestamp should be on the second line
176 String line2 = line(file, 1);
177 ZonedDateTime.parse(line2);
178
179 // runtime version should be on third line
180 String line3 = line(file, 2);
181 String vs = Runtime.version().toString();
182 assertTrue(line3.contains(vs));
183
184 // test if thread is included in thread dump
185 assertEquals(expectInDump, isPresent(file, thread));
186
187 // current thread should be included if platform thread or tracking all threads
188 Thread currentThread = Thread.currentThread();
189 boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
190 assertEquals(currentThreadExpected, isPresent(file, currentThread));
191 }
192
193 /**
194 * Invoke HotSpotDiagnosticMXBean.dumpThreads to create a thread dump in JSON format.
195 * The thread dump is parsed as a JSON object and checked to ensure that it contains
196 * expected data, the current thread, and maybe the given thread.
197 * @param containerName the name of the container or null for the root container
198 * @param thread the thread to test if included
199 * @param expect true if the thread is expected to be included
200 */
201 private void testDumpThreadsJson(String containerName,
202 Thread thread,
203 boolean expectInDump) throws Exception {
204 Path file = genOutputPath(".json");
205 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
206 mbean.dumpThreads(file.toString(), ThreadDumpFormat.JSON);
207 System.err.format("Dumped to %s%n", file);
208
209 // parse the JSON text
210 String jsonText = Files.readString(file);
211 ThreadDump threadDump = ThreadDump.parse(jsonText);
212
213 // test threadDump/processId
214 assertTrue(threadDump.processId() == ProcessHandle.current().pid());
215
216 // test threadDump/time can be parsed
217 ZonedDateTime.parse(threadDump.time());
218
219 // test threadDump/runtimeVersion
220 assertEquals(Runtime.version().toString(), threadDump.runtimeVersion());
221
222 // test root container, has no parent and no owner
223 var rootContainer = threadDump.rootThreadContainer();
224 assertFalse(rootContainer.owner().isPresent());
225 assertFalse(rootContainer.parent().isPresent());
226
227 // test that the container contains the given thread
228 ThreadDump.ThreadContainer container;
229 if (containerName == null) {
230 // root container, the thread should be found if trackAllThreads is true
231 container = rootContainer;
232 } else {
233 // find the container
234 container = threadDump.findThreadContainer(containerName).orElse(null);
235 assertNotNull(container, containerName + " not found");
236 assertFalse(container.owner().isPresent());
237 assertTrue(container.parent().get() == rootContainer);
238
239 }
240 boolean found = container.findThread(thread.threadId()).isPresent();
241 assertEquals(expectInDump, found);
242
243 // current thread should be in root container if platform thread or tracking all threads
244 Thread currentThread = Thread.currentThread();
245 boolean currentThreadExpected = trackAllThreads || !currentThread.isVirtual();
246 found = rootContainer.findThread(currentThread.threadId()).isPresent();
247 assertEquals(currentThreadExpected, found);
248 }
249
250 /**
251 * Test that dumpThreads throws if the output file already exists.
252 */
253 @Test
254 void testFileAlreadyExsists() throws Exception {
255 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
256 String file = Files.createFile(genOutputPath("txt")).toString();
257 assertThrows(FileAlreadyExistsException.class,
258 () -> mbean.dumpThreads(file, ThreadDumpFormat.TEXT_PLAIN));
259 assertThrows(FileAlreadyExistsException.class,
260 () -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
261 }
262
263 /**
264 * Test that dumpThreads throws if the file path is relative.
265 */
266 @Test
267 void testRelativePath() throws Exception {
268 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
269 assertThrows(IllegalArgumentException.class,
270 () -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
271 assertThrows(IllegalArgumentException.class,
272 () -> mbean.dumpThreads("threads.json", ThreadDumpFormat.JSON));
273 }
274
275 /**
276 * Test that dumpThreads throws with null parameters.
277 */
278 @Test
279 void testNull() throws Exception {
280 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
281 assertThrows(NullPointerException.class,
282 () -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
283 assertThrows(NullPointerException.class,
284 () -> mbean.dumpThreads(genOutputPath("txt").toString(), null));
285 }
286
287 /**
288 * Submits a parking task to the given executor, returns the Thread object of
289 * the parked thread.
290 */
291 private static Thread forkParker(ExecutorService executor) {
292 class Box { static volatile Thread thread;}
293 var latch = new CountDownLatch(1);
294 executor.submit(() -> {
295 Box.thread = Thread.currentThread();
296 latch.countDown();
297 LockSupport.park();
298 });
299 try {
300 latch.await();
301 } catch (InterruptedException e) {
302 throw new RuntimeException(e);
303 }
304 return Box.thread;
305 }
306
307 /**
308 * Returns true if a Thread is present in a plain text thread dump.
309 */
310 private static boolean isPresent(Path file, Thread thread) throws Exception {
311 String expect = "#" + thread.threadId();
312 return count(file, expect) > 0;
313 }
314
315 /**
316 * Generate a file path with the given suffix to use as an output file.
317 */
318 private static Path genOutputPath(String suffix) throws Exception {
319 Path dir = Path.of(".").toAbsolutePath();
320 Path file = Files.createTempFile(dir, "dump", suffix);
321 Files.delete(file);
322 return file;
323 }
324
325 /**
326 * Return the count of the number of files in the given file that contain
327 * the given character sequence.
328 */
329 static long count(Path file, CharSequence cs) throws Exception {
330 try (Stream<String> stream = Files.lines(file)) {
331 return stream.filter(line -> line.contains(cs)).count();
332 }
333 }
334
335 /**
336 * Return line $n of the given file.
337 */
338 private String line(Path file, long n) throws Exception {
339 try (Stream<String> stream = Files.lines(file)) {
340 return stream.skip(n).findFirst().orElseThrow();
341 }
342 }
343 }
|
1 /*
2 * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /**
25 * @test
26 * @bug 8284161 8287008 8309406
27 * @summary Basic test for com.sun.management.HotSpotDiagnosticMXBean.dumpThreads
28 * @requires vm.continuations
29 * @modules jdk.management
30 * @library /test/lib
31 * @build jdk.test.whitebox.WhiteBox
32 * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
33 * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI DumpThreads
34 * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Djdk.trackAllThreads DumpThreads
35 * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Djdk.trackAllThreads=true DumpThreads
36 * @run junit/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Djdk.trackAllThreads=false DumpThreads
37 */
38
39 import java.lang.management.ManagementFactory;
40 import java.lang.reflect.Method;
41 import java.nio.file.Files;
42 import java.nio.file.FileAlreadyExistsException;
43 import java.nio.file.Path;
44 import java.time.ZonedDateTime;
45 import java.util.List;
46 import java.util.Objects;
47 import java.util.Set;
48 import java.util.concurrent.CountDownLatch;
49 import java.util.concurrent.ExecutorService;
50 import java.util.concurrent.Executors;
51 import java.util.concurrent.ExecutorService;
52 import java.util.concurrent.ForkJoinPool;
53 import java.util.concurrent.ForkJoinWorkerThread;
54 import java.util.concurrent.ThreadFactory;
55 import java.util.concurrent.atomic.AtomicBoolean;
56 import java.util.concurrent.atomic.AtomicReference;
57 import java.util.concurrent.locks.LockSupport;
58 import java.util.concurrent.locks.ReentrantLock;
59 import java.util.stream.Collectors;
60 import java.util.stream.Stream;
61 import com.sun.management.HotSpotDiagnosticMXBean;
62 import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
63 import jdk.test.lib.threaddump.ThreadDump;
64 import jdk.test.lib.thread.VThreadRunner;
65 import jdk.test.whitebox.WhiteBox;
66
67 import org.junit.jupiter.api.Test;
68 import org.junit.jupiter.api.BeforeAll;
69 import org.junit.jupiter.api.Disabled;
70 import org.junit.jupiter.params.ParameterizedTest;
71 import org.junit.jupiter.params.provider.MethodSource;
72 import static org.junit.jupiter.api.Assertions.*;
73 import static org.junit.jupiter.api.Assumptions.*;
74
75 class DumpThreads {
76 private static boolean trackAllThreads;
77
78 @BeforeAll
79 static void setup() throws Exception {
80 String s = System.getProperty("jdk.trackAllThreads");
81 trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
82
83 // need >=2 carriers for testing mounted virtual thread
84 VThreadRunner.ensureParallelism(2);
85 }
86
87 /**
88 * Test thread dump in plain text format.
89 */
90 @Test
91 void testPlainText() throws Exception {
92 List<String> lines = dumpThreadsToPlainText();
93
94 // pid should be on the first line
95 String pid = Long.toString(ProcessHandle.current().pid());
96 assertEquals(pid, lines.get(0));
97
98 // timestamp should be on the second line
99 String secondLine = lines.get(1);
100 ZonedDateTime.parse(secondLine);
101
102 // runtime version should be on third line
103 String vs = Runtime.version().toString();
104 assertEquals(vs, lines.get(2));
105
106 // dump should include current thread
107 Thread currentThread = Thread.currentThread();
108 if (trackAllThreads || !currentThread.isVirtual()) {
109 ThreadFields fields = findThread(currentThread.threadId(), lines);
110 assertNotNull(fields, "current thread not found");
111 assertEquals(currentThread.getName(), fields.name());
112 }
113 }
114
115 /**
116 * Test thread dump in JSON format.
117 */
118 @Test
119 void testJsonFormat() throws Exception {
120 ThreadDump threadDump = dumpThreadsToJson();
121
122 // dump should include current thread in the root container
123 Thread currentThread = Thread.currentThread();
124 if (trackAllThreads || !currentThread.isVirtual()) {
125 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
126 .findThread(currentThread.threadId())
127 .orElse(null);
128 assertNotNull(ti, "current thread not found");
129 assertEquals(currentThread.isVirtual(), ti.isVirtual());
130 }
131 }
132
133 /**
134 * ExecutorService implementations that have their object identity in the container
135 * name so they can be found in the JSON format.
136 */
137 static Stream<ExecutorService> executors() {
138 return Stream.of(
139 Executors.newFixedThreadPool(1),
140 Executors.newVirtualThreadPerTaskExecutor()
141 );
142 }
143
144 /**
145 * Test that a thread container for an executor service is in the JSON format thread dump.
146 */
147 @ParameterizedTest
148 @MethodSource("executors")
149 void testThreadContainer(ExecutorService executor) throws Exception {
150 try (executor) {
151 testThreadContainer(executor, Objects.toIdentityString(executor));
152 }
153 }
154
155 /**
156 * Test that a thread container for the common pool is in the JSON format thread dump.
157 */
158 @Test
159 void testCommonPool() throws Exception {
160 testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
161 }
162
163 /**
164 * Test that the JSON thread dump has a thread container for the given executor.
165 */
166 private void testThreadContainer(ExecutorService executor, String name) throws Exception {
167 var threadRef = new AtomicReference<Thread>();
168
169 executor.submit(() -> {
170 threadRef.set(Thread.currentThread());
171 LockSupport.park();
172 });
173
174 // capture Thread
175 Thread thread;
176 while ((thread = threadRef.get()) == null) {
177 Thread.sleep(20);
178 }
179
180 try {
181 // dump threads to file and parse as JSON object
182 ThreadDump threadDump = dumpThreadsToJson();
183
184 // find the thread container corresponding to the executor
185 var container = threadDump.findThreadContainer(name).orElse(null);
186 assertNotNull(container, name + " not found");
187 assertFalse(container.owner().isPresent());
188 var parent = container.parent().orElseThrow();
189 assertEquals(threadDump.rootThreadContainer(), parent);
190
191 // find the thread in the thread container
192 ThreadDump.ThreadInfo ti = container.findThread(thread.threadId()).orElse(null);
193 assertNotNull(ti, "thread not found");
194
195 } finally {
196 LockSupport.unpark(thread);
197 }
198 }
199
200 /**
201 * ThreadFactory implementations for tests.
202 */
203 static Stream<ThreadFactory> threadFactories() {
204 return Stream.of(Thread.ofPlatform().factory(), Thread.ofVirtual().factory());
205 }
206
207 /**
208 * Test thread dump with a thread blocked on monitor enter.
209 */
210 @ParameterizedTest
211 @MethodSource("threadFactories")
212 void testBlockedThread(ThreadFactory factory) throws Exception {
213 assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
214
215 var lock = new Object();
216 var started = new CountDownLatch(1);
217
218 Thread thread = factory.newThread(() -> {
219 started.countDown();
220 synchronized (lock) { } // blocks
221 });
222
223 long tid = thread.threadId();
224 String lockAsString = Objects.toIdentityString(lock);
225
226 try {
227 synchronized (lock) {
228 // start thread and wait for it to block
229 thread.start();
230 started.await();
231 await(thread, Thread.State.BLOCKED);
232
233 // thread dump in plain text should include thread
234 List<String> lines = dumpThreadsToPlainText();
235 ThreadFields fields = findThread(tid, lines);
236 assertNotNull(fields, "thread not found");
237 assertEquals("BLOCKED", fields.state());
238 assertTrue(contains(lines, "// blocked on " + lockAsString));
239
240 // thread dump in JSON format should include thread in root container
241 ThreadDump threadDump = dumpThreadsToJson();
242 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
243 .findThread(tid)
244 .orElse(null);
245 assertNotNull(ti, "thread not found");
246 assertEquals("BLOCKED", ti.state());
247 assertEquals(lockAsString, ti.blockedOn());
248 }
249 } finally {
250 thread.join();
251 }
252 }
253
254 /**
255 * Test thread dump with a thread waiting in Object.wait.
256 */
257 @ParameterizedTest
258 @MethodSource("threadFactories")
259 void testWaitingThread(ThreadFactory factory) throws Exception {
260 assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
261 var lock = new Object();
262 var started = new CountDownLatch(1);
263
264 Thread thread = factory.newThread(() -> {
265 synchronized (lock) {
266 started.countDown();
267 try {
268 lock.wait();
269 } catch (InterruptedException e) { }
270 }
271 });
272
273 long tid = thread.threadId();
274 String lockAsString = Objects.toIdentityString(lock);
275
276 // compiled native frames have no locals
277 Method wait0 = Object.class.getDeclaredMethod("wait0", long.class);
278 boolean expectWaitingOn = !WhiteBox.getWhiteBox().isMethodCompiled(wait0);
279
280 try {
281 // start thread and wait for it to wait in Object.wait
282 thread.start();
283 started.await();
284 await(thread, Thread.State.WAITING);
285
286 // thread dump in plain text should include thread
287 List<String> lines = dumpThreadsToPlainText();
288 ThreadFields fields = findThread(tid, lines);
289 assertNotNull(fields, "thread not found");
290 assertEquals("WAITING", fields.state());
291 if (expectWaitingOn) {
292 assertTrue(contains(lines, "// waiting on " + lockAsString));
293 }
294
295 // thread dump in JSON format should include thread in root container
296 ThreadDump threadDump = dumpThreadsToJson();
297 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
298 .findThread(thread.threadId())
299 .orElse(null);
300 assertNotNull(ti, "thread not found");
301 assertEquals(ti.isVirtual(), thread.isVirtual());
302
303 assertEquals("WAITING", ti.state());
304 if (expectWaitingOn) {
305 assertEquals(Objects.toIdentityString(lock), ti.waitingOn());
306 }
307 } finally {
308 synchronized (lock) {
309 lock.notifyAll();
310 }
311 thread.join();
312 }
313 }
314
315 /**
316 * Test thread dump with a thread parked on a j.u.c. lock.
317 */
318 @ParameterizedTest
319 @MethodSource("threadFactories")
320 void testParkedThread(ThreadFactory factory) throws Exception {
321 assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
322
323 var lock = new ReentrantLock();
324 var started = new CountDownLatch(1);
325
326 Thread thread = factory.newThread(() -> {
327 started.countDown();
328 lock.lock();
329 lock.unlock();
330 });
331
332 long tid = thread.threadId();
333
334 lock.lock();
335 try {
336 // start thread and wait for it to park
337 thread.start();
338 started.await();
339 await(thread, Thread.State.WAITING);
340
341 // thread dump in plain text should include thread
342 List<String> lines = dumpThreadsToPlainText();
343 ThreadFields fields = findThread(tid, lines);
344 assertNotNull(fields, "thread not found");
345 assertEquals("WAITING", fields.state());
346 assertTrue(contains(lines, "// parked on java.util.concurrent.locks.ReentrantLock"));
347
348 // thread dump in JSON format should include thread in root container
349 ThreadDump threadDump = dumpThreadsToJson();
350 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
351 .findThread(thread.threadId())
352 .orElse(null);
353 assertNotNull(ti, "thread not found");
354 assertEquals(ti.isVirtual(), thread.isVirtual());
355
356 // thread should be waiting on the ReentrantLock, owned by the main thread.
357 assertEquals("WAITING", ti.state());
358 String parkBlocker = ti.parkBlocker();
359 assertNotNull(parkBlocker);
360 assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
361 long ownerTid = ti.exclusiveOwnerThreadId().orElseThrow();
362 assertEquals(Thread.currentThread().threadId(), ownerTid);
363 } finally {
364 lock.unlock();
365 thread.join();
366 }
367 }
368
369 /**
370 * Test thread dump wth a thread owning a monitor.
371 */
372 @ParameterizedTest
373 @MethodSource("threadFactories")
374 void testThreadOwnsMonitor(ThreadFactory factory) throws Exception {
375 assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
376
377 var lock = new Object();
378 var started = new CountDownLatch(1);
379
380 Thread thread = factory.newThread(() -> {
381 synchronized (lock) {
382 started.countDown();
383 LockSupport.park();
384 }
385 });
386
387 long tid = thread.threadId();
388 String lockAsString = Objects.toIdentityString(lock);
389
390 try {
391 // start thread and wait for it to park
392 thread.start();
393 started.await();
394 await(thread, Thread.State.WAITING);
395
396 // thread dump in plain text should include thread
397 List<String> lines = dumpThreadsToPlainText();
398 ThreadFields fields = findThread(tid, lines);
399 assertNotNull(fields, "thread not found");
400 assertEquals("WAITING", fields.state());
401 assertTrue(contains(lines, "// locked " + lockAsString));
402
403 // thread dump in JSON format should include thread in root container
404 ThreadDump threadDump = dumpThreadsToJson();
405 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
406 .findThread(tid)
407 .orElse(null);
408 assertNotNull(ti, "thread not found");
409 assertEquals(ti.isVirtual(), thread.isVirtual());
410
411 // the lock should be in the ownedMonitors array
412 Set<String> ownedMonitors = ti.ownedMonitors().values()
413 .stream()
414 .flatMap(List::stream)
415 .collect(Collectors.toSet());
416 assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
417 } finally {
418 LockSupport.unpark(thread);
419 thread.join();
420 }
421 }
422
423 /**
424 * Test mounted virtual thread.
425 */
426 @Disabled
427 @Test
428 void testMountedVirtualThread() throws Exception {
429 assumeTrue(trackAllThreads, "This test requires all threads to be tracked");
430
431 // start virtual thread that spins until done
432 var started = new AtomicBoolean();
433 var done = new AtomicBoolean();
434 var thread = Thread.ofVirtual().start(() -> {
435 started.set(true);
436 while (!done.get()) {
437 Thread.onSpinWait();
438 }
439 });
440
441 try {
442 // wait for thread to start
443 awaitTrue(started);
444
445 // thread dump in JSON format should include thread in root container
446 ThreadDump threadDump = dumpThreadsToJson();
447 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
448 .findThread(thread.threadId())
449 .orElse(null);
450 assertNotNull(ti, "thread not found");
451 assertTrue(ti.isVirtual());
452
453 // the carrier should be the thread identifier of a ForkJoinWorkerThread
454 long carrierTid = ti.carrier().orElseThrow();
455 boolean found = Thread.getAllStackTraces()
456 .keySet()
457 .stream()
458 .filter(t -> t instanceof ForkJoinWorkerThread)
459 .map(Thread::threadId)
460 .anyMatch(tid -> tid == carrierTid);
461 assertTrue(found, carrierTid + " is not a ForkJoinWorkerThread");
462 } finally {
463 done.set(true);
464 thread.join();
465 }
466 }
467
468 /**
469 * Test that dumpThreads throws if the output file already exists.
470 */
471 @Test
472 void testFileAlreadyExsists() throws Exception {
473 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
474 String file = Files.createFile(genOutputPath("txt")).toString();
475 assertThrows(FileAlreadyExistsException.class,
476 () -> mbean.dumpThreads(file, ThreadDumpFormat.TEXT_PLAIN));
477 assertThrows(FileAlreadyExistsException.class,
478 () -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
479 }
480
481 /**
482 * Test that dumpThreads throws if the file path is relative.
483 */
484 @Test
485 void testRelativePath() throws Exception {
486 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
487 assertThrows(IllegalArgumentException.class,
488 () -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
489 assertThrows(IllegalArgumentException.class,
490 () -> mbean.dumpThreads("threads.json", ThreadDumpFormat.JSON));
491 }
492
493 /**
494 * Test that dumpThreads throws with null parameters.
495 */
496 @Test
497 void testNull() throws Exception {
498 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
499 assertThrows(NullPointerException.class,
500 () -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
501 assertThrows(NullPointerException.class,
502 () -> mbean.dumpThreads(genOutputPath("txt").toString(), null));
503 }
504
505 /**
506 * Represents the data for a thread found in a plain text thread dump.
507 */
508 private record ThreadFields(long tid, String name, String state) { }
509
510 /**
511 * Find a thread in the lines of a plain text thread dump.
512 */
513 private ThreadFields findThread(long tid, List<String> lines) {
514 String line = lines.stream()
515 .filter(l -> l.startsWith("#" + tid + " "))
516 .findFirst()
517 .orElse(null);
518 if (line == null) {
519 return null;
520 }
521
522 // #3 "main" RUNNABLE 2025-04-18T15:22:12.012450Z
523 String[] components = line.split("\\s+"); // assume no spaces in thread name
524 assertEquals(4, components.length);
525 String nameInQuotes = components[1];
526 String name = nameInQuotes.substring(1, nameInQuotes.length()-1);
527 String state = components[2];
528 return new ThreadFields(tid, name, state);
529 }
530
531 /**
532 * Returns true if lines of a plain text thread dump contain the given text.
533 */
534 private boolean contains(List<String> lines, String text) {
535 return lines.stream().map(String::trim)
536 .anyMatch(l -> l.contains(text));
537 }
538
539 /**
540 * Dump threads to a file in plain text format, return the lines in the file.
541 */
542 private List<String> dumpThreadsToPlainText() throws Exception {
543 Path file = genOutputPath(".txt");
544 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
545 mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
546 System.err.format("Dumped to %s%n", file);
547 return Files.readAllLines(file);
548 }
549
550 /**
551 * Dump threads to a file in JSON format, parse and return as JSON object.
552 */
553 private static ThreadDump dumpThreadsToJson() throws Exception {
554 Path file = genOutputPath(".json");
555 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
556 mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
557 System.err.format("Dumped to %s%n", file);
558 String jsonText = Files.readString(file);
559 return ThreadDump.parse(jsonText);
560 }
561
562 /**
563 * Generate a file path with the given suffix to use as an output file.
564 */
565 private static Path genOutputPath(String suffix) throws Exception {
566 Path dir = Path.of(".").toAbsolutePath();
567 Path file = Files.createTempFile(dir, "dump", suffix);
568 Files.delete(file);
569 return file;
570 }
571
572 /**
573 * Waits for the given thread to get to a given state.
574 */
575 private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
576 Thread.State state = thread.getState();
577 while (state != expectedState) {
578 assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
579 Thread.sleep(10);
580 state = thread.getState();
581 }
582 }
583
584 /**
585 * Waits for the boolean value to become true.
586 */
587 private static void awaitTrue(AtomicBoolean ref) throws Exception {
588 while (!ref.get()) {
589 Thread.sleep(20);
590 }
591 }
592 }
|