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 }