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 }