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 }