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 * @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.time.ZonedDateTime;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.Set;
45 import java.util.concurrent.CountDownLatch;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Executors;
48 import java.util.concurrent.ExecutorService;
49 import java.util.concurrent.ForkJoinPool;
50 import java.util.concurrent.atomic.AtomicReference;
51 import java.util.concurrent.locks.LockSupport;
52 import java.util.concurrent.locks.ReentrantLock;
53 import java.util.stream.Collectors;
54 import java.util.stream.Stream;
55 import com.sun.management.HotSpotDiagnosticMXBean;
56 import com.sun.management.HotSpotDiagnosticMXBean.ThreadDumpFormat;
57 import jdk.test.lib.threaddump.ThreadDump;
58
59 import org.junit.jupiter.api.Test;
60 import org.junit.jupiter.api.BeforeAll;
61 import org.junit.jupiter.params.ParameterizedTest;
62 import org.junit.jupiter.params.provider.MethodSource;
63 import static org.junit.jupiter.api.Assertions.*;
64 import static org.junit.jupiter.api.Assumptions.*;
65
66 class DumpThreads {
67 private static boolean trackAllThreads;
68
69 @BeforeAll
70 static void setup() throws Exception {
71 String s = System.getProperty("jdk.trackAllThreads");
72 trackAllThreads = (s == null) || s.isEmpty() || Boolean.parseBoolean(s);
73 }
74
75 /**
76 * Test thread dump in plain text format.
77 */
78 @Test
79 void testPlainText() throws Exception {
80 List<String> lines = dumpThreadsToPlainText();
81
82 // pid should be on the first line
83 String pid = Long.toString(ProcessHandle.current().pid());
84 assertEquals(pid, lines.get(0));
85
86 // timestamp should be on the second line
87 String secondLine = lines.get(1);
88 ZonedDateTime.parse(secondLine);
89
90 // runtime version should be on third line
91 String vs = Runtime.version().toString();
92 assertEquals(vs, lines.get(2));
93
94 // dump should include current thread
95 Thread currentThread = Thread.currentThread();
96 if (trackAllThreads || !currentThread.isVirtual()) {
97 ThreadFields fields = findThread(currentThread.threadId(), lines);
98 assertNotNull(fields, "current thread not found");
99 assertEquals(currentThread.getName(), fields.name());
100 }
101 }
102
103 /**
104 * Test thread dump in JSON format.
105 */
106 @Test
107 void testJsonFormat() throws Exception {
108 ThreadDump threadDump = dumpThreadsToJson();
109
110 // dump should include current thread in the root container
111 Thread currentThread = Thread.currentThread();
112 if (trackAllThreads || !currentThread.isVirtual()) {
113 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
114 .findThread(currentThread.threadId())
115 .orElse(null);
116 assertNotNull(ti, "current thread not found");
117 }
118 }
119
120 /**
121 * ExecutorService implementations that have their object identity in the container
122 * name so they can be found in the JSON format.
123 */
124 static Stream<ExecutorService> executors() {
125 return Stream.of(
126 Executors.newFixedThreadPool(1),
127 Executors.newVirtualThreadPerTaskExecutor()
128 );
129 }
130
131 /**
132 * Test that a thread container for an executor service is in the JSON format thread dump.
133 */
134 @ParameterizedTest
135 @MethodSource("executors")
136 void testThreadContainer(ExecutorService executor) throws Exception {
137 try (executor) {
138 testThreadContainer(executor, Objects.toIdentityString(executor));
139 }
140 }
141
142 /**
143 * Test that a thread container for the common pool is in the JSON format thread dump.
144 */
145 @Test
146 void testCommonPool() throws Exception {
147 testThreadContainer(ForkJoinPool.commonPool(), "ForkJoinPool.commonPool");
148 }
149
150 /**
151 * Test that the JSON thread dump has a thread container for the given executor.
152 */
153 void testThreadContainer(ExecutorService executor, String name) throws Exception {
154 var threadRef = new AtomicReference<Thread>();
155
156 executor.submit(() -> {
157 threadRef.set(Thread.currentThread());
158 LockSupport.park();
159 });
160
161 // capture Thread
162 Thread thread;
163 while ((thread = threadRef.get()) == null) {
164 Thread.sleep(20);
165 }
166
167 try {
168 // dump threads to file and parse as JSON object
169 ThreadDump threadDump = dumpThreadsToJson();
170
171 // find the thread container corresponding to the executor
172 var container = threadDump.findThreadContainer(name).orElse(null);
173 assertNotNull(container, name + " not found");
174 assertFalse(container.owner().isPresent());
175 var parent = container.parent().orElseThrow();
176 assertEquals(threadDump.rootThreadContainer(), parent);
177
178 // find the thread in the thread container
179 ThreadDump.ThreadInfo ti = container.findThread(thread.threadId()).orElse(null);
180 assertNotNull(ti, "thread not found");
181
182 } finally {
183 LockSupport.unpark(thread);
184 }
185 }
186
187 /**
188 * Test thread dump with a thread blocked on monitor enter.
189 */
190 @Test
191 void testBlockedThread() throws Exception {
192 assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
193 var lock = new Object();
194 var started = new CountDownLatch(1);
195
196 Thread vthread = Thread.ofVirtual().unstarted(() -> {
197 started.countDown();
198 synchronized (lock) { } // blocks
199 });
200
201 long tid = vthread.threadId();
202 String lockAsString = Objects.toIdentityString(lock);
203
204 try {
205 synchronized (lock) {
206 // start thread and wait for it to block
207 vthread.start();
208 started.await();
209 await(vthread, Thread.State.BLOCKED);
210
211 // thread dump in plain text should include thread
212 List<String> lines = dumpThreadsToPlainText();
213 ThreadFields fields = findThread(tid, lines);
214 assertNotNull(fields, "thread not found");
215 assertEquals("BLOCKED", fields.state());
216 assertTrue(contains(lines, "// blocked on " + lockAsString));
217
218 // thread dump in JSON format should include thread in root container
219 ThreadDump threadDump = dumpThreadsToJson();
220 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
221 .findThread(tid)
222 .orElse(null);
223 assertNotNull(ti, "thread not found");
224 assertEquals("BLOCKED", ti.state());
225 assertEquals(lockAsString, ti.blockedOn());
226 }
227 } finally {
228 vthread.join();
229 }
230 }
231
232 /**
233 * Test thread dump with a thread waiting in Object.wait.
234 */
235 @Test
236 void testWaitingThread() throws Exception {
237 assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
238 var lock = new Object();
239 var started = new CountDownLatch(1);
240
241 Thread vthread = Thread.ofVirtual().start(() -> {
242 synchronized (lock) {
243 started.countDown();
244 try {
245 lock.wait();
246 } catch (InterruptedException e) { }
247 }
248 });
249
250 long tid = vthread.threadId();
251 String lockAsString = Objects.toIdentityString(lock);
252
253 try {
254 // wait for thread to be waiting in Object.wait
255 started.await();
256 await(vthread, Thread.State.WAITING);
257
258 // thread dump in plain text should include thread
259 List<String> lines = dumpThreadsToPlainText();
260 ThreadFields fields = findThread(tid, lines);
261 assertNotNull(fields, "thread not found");
262 assertEquals("WAITING", fields.state());
263 assertTrue(contains(lines, "// waiting on " + lockAsString));
264
265 // thread dump in JSON format should include thread in root container
266 ThreadDump threadDump = dumpThreadsToJson();
267 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
268 .findThread(vthread.threadId())
269 .orElse(null);
270 assertNotNull(ti, "thread not found");
271 assertEquals("WAITING", ti.state());
272 assertEquals(Objects.toIdentityString(lock), ti.waitingOn());
273
274 } finally {
275 synchronized (lock) {
276 lock.notifyAll();
277 }
278 vthread.join();
279 }
280 }
281
282 /**
283 * Test thread dump with a thread parked on a j.u.c. lock.
284 */
285 @Test
286 void testParkedThread() throws Exception {
287 assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
288 var lock = new ReentrantLock();
289 var started = new CountDownLatch(1);
290
291 Thread vthread = Thread.ofVirtual().unstarted(() -> {
292 started.countDown();
293 lock.lock();
294 lock.unlock();
295 });
296
297 long tid = vthread.threadId();
298
299 lock.lock();
300 try {
301 // start thread and wait for it to park
302 vthread.start();
303 started.await();
304 await(vthread, Thread.State.WAITING);
305
306 // thread dump in plain text should include thread
307 List<String> lines = dumpThreadsToPlainText();
308 ThreadFields fields = findThread(tid, lines);
309 assertNotNull(fields, "thread not found");
310 assertEquals("WAITING", fields.state());
311 assertTrue(contains(lines, "// parked on java.util.concurrent.locks.ReentrantLock"));
312
313 // thread dump in JSON format should include thread in root container
314 ThreadDump threadDump = dumpThreadsToJson();
315 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
316 .findThread(vthread.threadId())
317 .orElse(null);
318 assertNotNull(ti, "thread not found");
319 assertEquals("WAITING", ti.state());
320 String parkBlocker = ti.parkBlocker();
321 assertNotNull(parkBlocker);
322 assertTrue(parkBlocker.contains("java.util.concurrent.locks.ReentrantLock"));
323 } finally {
324 lock.unlock();
325 }
326 }
327
328 /**
329 * Test thread dump wth a thread owning a monitor.
330 */
331 @Test
332 void testThreadOwnsMonitor() throws Exception {
333 assumeTrue(trackAllThreads, "This test requires all virtual threads to be tracked");
334 var lock = new Object();
335 var started = new CountDownLatch(1);
336
337 Thread vthread = Thread.ofVirtual().start(() -> {
338 synchronized (lock) {
339 started.countDown();
340 LockSupport.park();
341 }
342 });
343
344 long tid = vthread.threadId();
345 String lockAsString = Objects.toIdentityString(lock);
346
347 try {
348 // wait for thread to park
349 started.await();
350 await(vthread, Thread.State.WAITING);
351
352 // thread dump in plain text should include thread
353 List<String> lines = dumpThreadsToPlainText();
354 ThreadFields fields = findThread(tid, lines);
355 assertNotNull(fields, "thread not found");
356 assertEquals("WAITING", fields.state());
357 assertTrue(contains(lines, "// locked " + lockAsString));
358
359 // thread dump in JSON format should include thread in root container
360 ThreadDump threadDump = dumpThreadsToJson();
361 ThreadDump.ThreadInfo ti = threadDump.rootThreadContainer()
362 .findThread(tid)
363 .orElse(null);
364 assertNotNull(ti, "thread not found");
365 // the lock should be in the ownedMonitors array
366 Set<String> ownedMonitors = ti.ownedMonitors().values()
367 .stream()
368 .flatMap(List::stream)
369 .collect(Collectors.toSet());
370 assertTrue(ownedMonitors.contains(lockAsString), lockAsString + " not found");
371 } finally {
372 LockSupport.unpark(vthread);
373 }
374 }
375
376 /**
377 * Test that dumpThreads throws if the output file already exists.
378 */
379 @Test
380 void testFileAlreadyExsists() throws Exception {
381 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
382 String file = Files.createFile(genOutputPath("txt")).toString();
383 assertThrows(FileAlreadyExistsException.class,
384 () -> mbean.dumpThreads(file, ThreadDumpFormat.TEXT_PLAIN));
385 assertThrows(FileAlreadyExistsException.class,
386 () -> mbean.dumpThreads(file, ThreadDumpFormat.JSON));
387 }
388
389 /**
390 * Test that dumpThreads throws if the file path is relative.
391 */
392 @Test
393 void testRelativePath() throws Exception {
394 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
395 assertThrows(IllegalArgumentException.class,
396 () -> mbean.dumpThreads("threads.txt", ThreadDumpFormat.TEXT_PLAIN));
397 assertThrows(IllegalArgumentException.class,
398 () -> mbean.dumpThreads("threads.json", ThreadDumpFormat.JSON));
399 }
400
401 /**
402 * Test that dumpThreads throws with null parameters.
403 */
404 @Test
405 void testNull() throws Exception {
406 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
407 assertThrows(NullPointerException.class,
408 () -> mbean.dumpThreads(null, ThreadDumpFormat.TEXT_PLAIN));
409 assertThrows(NullPointerException.class,
410 () -> mbean.dumpThreads(genOutputPath("txt").toString(), null));
411 }
412
413 /**
414 * Represents the data for a thread found in a plain text thread dump.
415 */
416 private record ThreadFields(long tid, String name, String state) { }
417
418 /**
419 * Find a thread in the lines of a plain text thread dump.
420 */
421 private ThreadFields findThread(long tid, List<String> lines) {
422 String line = lines.stream()
423 .filter(l -> l.startsWith("#" + tid + " "))
424 .findFirst()
425 .orElse(null);
426 if (line == null) {
427 return null;
428 }
429
430 // #3 "main" RUNNABLE 2025-04-18T15:22:12.012450Z
431 String[] components = line.split("\\s+"); // assume no spaces in thread name
432 assertEquals(4, components.length);
433 String nameInQuotes = components[1];
434 String name = nameInQuotes.substring(1, nameInQuotes.length()-1);
435 String state = components[2];
436 return new ThreadFields(tid, name, state);
437 }
438
439 /**
440 * Returns true if lines of a plain text thread dump contain the given text.
441 */
442 private boolean contains(List<String> lines, String text) {
443 return lines.stream().map(String::trim)
444 .anyMatch(l -> l.contains(text));
445 }
446
447 /**
448 * Dump threads to a file in plain text format, return the lines in the file.
449 */
450 private List<String> dumpThreadsToPlainText() throws Exception {
451 Path file = genOutputPath(".txt");
452 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
453 mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.TEXT_PLAIN);
454 System.err.format("Dumped to %s%n", file);
455 return Files.readAllLines(file);
456 }
457
458 /**
459 * Dump threads to a file in JSON format, parse and return as JSON object.
460 */
461 private static ThreadDump dumpThreadsToJson() throws Exception {
462 Path file = genOutputPath(".json");
463 var mbean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
464 mbean.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
465 System.err.format("Dumped to %s%n", file);
466 String jsonText = Files.readString(file);
467 return ThreadDump.parse(jsonText);
468 }
469
470 /**
471 * Generate a file path with the given suffix to use as an output file.
472 */
473 private static Path genOutputPath(String suffix) throws Exception {
474 Path dir = Path.of(".").toAbsolutePath();
475 Path file = Files.createTempFile(dir, "dump", suffix);
476 Files.delete(file);
477 return file;
478 }
479
480 /**
481 * Waits for the given thread to get to a given state.
482 */
483 private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
484 Thread.State state = thread.getState();
485 while (state != expectedState) {
486 assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
487 Thread.sleep(10);
488 state = thread.getState();
489 }
490 }
491 }
|