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 }