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 }