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 }