< prev index next >

src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java

Print this page

  1 /*
  2  * Copyright (c) 2020, 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.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package jdk.internal.vm;
 26 
 27 import java.io.BufferedOutputStream;
 28 import java.io.ByteArrayOutputStream;
 29 import java.io.IOException;
 30 import java.io.OutputStream;
 31 import java.io.PrintStream;


 32 import java.nio.charset.StandardCharsets;
 33 import java.nio.file.FileAlreadyExistsException;
 34 import java.nio.file.Files;
 35 import java.nio.file.OpenOption;
 36 import java.nio.file.Path;
 37 import java.nio.file.StandardOpenOption;
 38 import java.time.Instant;
 39 import java.util.ArrayList;


 40 import java.util.Iterator;
 41 import java.util.List;


 42 
 43 /**
 44  * Thread dump support.
 45  *
 46  * This class defines methods to dump threads to an output stream or file in plain
 47  * text or JSON format.

 48  */
 49 public class ThreadDumper {
 50     private ThreadDumper() { }
 51 
 52     // the maximum byte array to return when generating the thread dump to a byte array
 53     private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
 54 
 55     /**
 56      * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded.
 57      *
 58      * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
 59      *
 60      * @param file the file path to the file, null or "-" to return a byte array
 61      * @param okayToOverwrite true to overwrite an existing file
 62      * @return the UTF-8 encoded thread dump or message to return to the user
 63      */
 64     public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
 65         if (file == null || file.equals("-")) {
 66             return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
 67         } else {
 68             return dumpThreadsToFile(file, okayToOverwrite, false);
 69         }
 70     }
 71 
 72     /**
 73      * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded.
 74      *
 75      * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
 76      *
 77      * @param file the file path to the file, null or "-" to return a byte array
 78      * @param okayToOverwrite true to overwrite an existing file
 79      * @return the UTF-8 encoded thread dump or message to return to the user
 80      */
 81     public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
 82         if (file == null || file.equals("-")) {
 83             return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
 84         } else {
 85             return dumpThreadsToFile(file, okayToOverwrite, true);
 86         }
 87     }
 88 
 89     /**
 90      * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.



 91      */
 92     private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
 93         try (var out = new BoundedByteArrayOutputStream(maxSize);
 94              PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
 95             if (json) {
 96                 dumpThreadsToJson(ps);
 97             } else {
 98                 dumpThreads(ps);
 99             }
100             return out.toByteArray();





101         }

102     }
103 
104     /**
105      * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.


106      */
107     private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
108         Path path = Path.of(file).toAbsolutePath();
109         OpenOption[] options = (okayToOverwrite)
110                 ? new OpenOption[0]
111                 : new OpenOption[] { StandardOpenOption.CREATE_NEW };
112         String reply;
113         try (OutputStream out = Files.newOutputStream(path, options);
114              BufferedOutputStream bos = new BufferedOutputStream(out);
115              PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) {
116             if (json) {
117                 dumpThreadsToJson(ps);
118             } else {
119                 dumpThreads(ps);



120             }
121             reply = String.format("Created %s%n", path);
122         } catch (FileAlreadyExistsException e) {
123             reply = String.format("%s exists, use -overwrite to overwrite%n", path);
124         } catch (IOException ioe) {
125             reply = String.format("Failed: %s%n", ioe);
126         }
127         return reply.getBytes(StandardCharsets.UTF_8);
128     }
129 
130     /**
131      * Generate a thread dump in plain text format to the given output stream,
132      * UTF-8 encoded.
133      *
134      * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
135      */
136     public static void dumpThreads(OutputStream out) {
137         BufferedOutputStream bos = new BufferedOutputStream(out);
138         PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
139         try {
140             dumpThreads(ps);
141         } finally {
142             ps.flush();  // flushes underlying stream


143         }
144     }
145 
146     /**
147      * Generate a thread dump in plain text format to the given print stream.

148      */
149     private static void dumpThreads(PrintStream ps) {
150         ps.println(processId());
151         ps.println(Instant.now());
152         ps.println(Runtime.version());
153         ps.println();
154         dumpThreads(ThreadContainers.root(), ps);
155     }
156 
157     private static void dumpThreads(ThreadContainer container, PrintStream ps) {
158         container.threads().forEach(t -> dumpThread(t, ps));
159         container.children().forEach(c -> dumpThreads(c, ps));
160     }
161 
162     private static void dumpThread(Thread thread, PrintStream ps) {
163         String suffix = thread.isVirtual() ? " virtual" : "";
164         ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
165         for (StackTraceElement ste : thread.getStackTrace()) {
166             ps.print("      ");
167             ps.println(ste);























168         }
169         ps.println();












170     }
171 
172     /**
173      * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
174      *
175      * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.

176      */
177     public static void dumpThreadsToJson(OutputStream out) {
178         BufferedOutputStream bos = new BufferedOutputStream(out);
179         PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
180         try {
181             dumpThreadsToJson(ps);
182         } finally {
183             ps.flush();  // flushes underlying stream


184         }
185     }
186 
187     /**
188      * Generate a thread dump to the given print stream in JSON format.

189      */
190     private static void dumpThreadsToJson(PrintStream out) {
191         out.println("{");
192         out.println("  \"threadDump\": {");
193 
194         String now = Instant.now().toString();
195         String runtimeVersion = Runtime.version().toString();
196         out.format("    \"processId\": \"%d\",%n", processId());
197         out.format("    \"time\": \"%s\",%n", escape(now));
198         out.format("    \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
199 
200         out.println("    \"threadContainers\": [");
201         List<ThreadContainer> containers = allContainers();
202         Iterator<ThreadContainer> iterator = containers.iterator();
203         while (iterator.hasNext()) {
204             ThreadContainer container = iterator.next();
205             boolean more = iterator.hasNext();
206             dumpThreadsToJson(container, out, more);
207         }
208         out.println("    ]");   // end of threadContainers
209 
210         out.println("  }");   // end threadDump
211         out.println("}");  // end object
212     }
213 
214     /**
215      * Dump the given thread container to the print stream in JSON format.

216      */
217     private static void dumpThreadsToJson(ThreadContainer container,
218                                           PrintStream out,
219                                           boolean more) {
220         out.println("      {");
221         out.format("        \"container\": \"%s\",%n", escape(container.toString()));
222 
223         ThreadContainer parent = container.parent();
224         if (parent == null) {
225             out.format("        \"parent\": null,%n");
226         } else {
227             out.format("        \"parent\": \"%s\",%n", escape(parent.toString()));
228         }
229 
230         Thread owner = container.owner();
231         if (owner == null) {
232             out.format("        \"owner\": null,%n");
233         } else {
234             out.format("        \"owner\": \"%d\",%n", owner.threadId());
235         }
236 
237         long threadCount = 0;
238         out.println("        \"threads\": [");
239         Iterator<Thread> threads = container.threads().iterator();
240         while (threads.hasNext()) {
241             Thread thread = threads.next();
242             dumpThreadToJson(thread, out, threads.hasNext());
243             threadCount++;
244         }
245         out.println("        ],");   // end of threads
246 
247         // thread count
248         if (!ThreadContainers.trackAllThreads()) {
249             threadCount = Long.max(threadCount, container.threadCount());
250         }
251         out.format("        \"threadCount\": \"%d\"%n", threadCount);
252 
253         if (more) {
254             out.println("      },");
255         } else {
256             out.println("      }");  // last container, no trailing comma
257         }
258     }
259 
260     /**
261      * Dump the given thread and its stack trace to the print stream in JSON format.

262      */
263     private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
264         out.println("         {");
265         out.println("           \"tid\": \"" + thread.threadId() + "\",");
266         out.println("           \"name\": \"" + escape(thread.getName()) + "\",");
267         out.println("           \"stack\": [");
268 
269         int i = 0;
270         StackTraceElement[] stackTrace = thread.getStackTrace();
271         while (i < stackTrace.length) {
272             out.print("              \"");
273             out.print(escape(stackTrace[i].toString()));
274             out.print("\"");
275             i++;
276             if (i < stackTrace.length) {
277                 out.println(",");
278             } else {
279                 out.println();  // last element, no trailing comma






280             }

281         }
282         out.println("           ]");
283         if (more) {
284             out.println("         },");
285         } else {
286             out.println("         }");  // last thread, no trailing comma







287         }
288     }
289 
290     /**
291      * Returns a list of all thread containers that are "reachable" from
292      * the root container.
293      */
294     private static List<ThreadContainer> allContainers() {
295         List<ThreadContainer> containers = new ArrayList<>();
296         collect(ThreadContainers.root(), containers);
297         return containers;
298     }





















299 
300     private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
301         containers.add(container);
302         container.children().forEach(c -> collect(c, containers));
303     }
304 
305     /**
306      * Escape any characters that need to be escape in the JSON output.

307      */
308     private static String escape(String value) {
309         StringBuilder sb = new StringBuilder();
310         for (int i = 0; i < value.length(); i++) {
311             char c = value.charAt(i);
312             switch (c) {
313                 case '"'  -> sb.append("\\\"");
314                 case '\\' -> sb.append("\\\\");
315                 case '/'  -> sb.append("\\/");
316                 case '\b' -> sb.append("\\b");
317                 case '\f' -> sb.append("\\f");
318                 case '\n' -> sb.append("\\n");
319                 case '\r' -> sb.append("\\r");
320                 case '\t' -> sb.append("\\t");
321                 default -> {
322                     if (c <= 0x1f) {
323                         sb.append(String.format("\\u%04x", c));
324                     } else {
325                         sb.append(c);





































































































































326                     }
327                 }
328             }

329         }
330         return sb.toString();
331     }
332 
333     /**
334      * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
335      * written the subsequent bytes are discarded.
336      */
337     private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
338         final int max;
339         BoundedByteArrayOutputStream(int max) {
340             this.max = max;
341         }
342         @Override
343         public void write(int b) {
344             if (max < count) {
345                 super.write(b);
346             }
347         }
348         @Override
349         public void write(byte[] b, int off, int len) {
350             int remaining = max - count;
351             if (remaining > 0) {
352                 super.write(b, off, Integer.min(len, remaining));
353             }
354         }
355         @Override
356         public void close() {
357         }
358     }
359 


















































360     /**
361      * Returns the process ID or -1 if not supported.
362      */
363     private static long processId() {
364         try {
365             return ProcessHandle.current().pid();
366         } catch (UnsupportedOperationException e) {
367             return -1L;
368         }
369     }
370 }

  1 /*
  2  * Copyright (c) 2020, 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.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package jdk.internal.vm;
 26 
 27 import java.io.BufferedWriter;
 28 import java.io.ByteArrayOutputStream;
 29 import java.io.IOException;
 30 import java.io.OutputStream;
 31 import java.io.OutputStreamWriter;
 32 import java.io.UncheckedIOException;
 33 import java.io.Writer;
 34 import java.nio.charset.StandardCharsets;
 35 import java.nio.file.FileAlreadyExistsException;
 36 import java.nio.file.Files;
 37 import java.nio.file.OpenOption;
 38 import java.nio.file.Path;
 39 import java.nio.file.StandardOpenOption;
 40 import java.time.Instant;
 41 import java.util.ArrayDeque;
 42 import java.util.Arrays;
 43 import java.util.Deque;
 44 import java.util.Iterator;
 45 import java.util.List;
 46 import java.util.Objects;
 47 import java.util.concurrent.locks.AbstractOwnableSynchronizer;
 48 
 49 /**
 50  * Thread dump support.
 51  *
 52  * This class defines static methods to support the Thread.dump_to_file diagnostic command
 53  * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
 54  * thread dump to a file or byte array in plain text or JSON format.
 55  */
 56 public class ThreadDumper {
 57     private ThreadDumper() { }
 58 
 59     // the maximum byte array to return when generating the thread dump to a byte array
 60     private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
 61 
 62     /**
 63      * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.

 64      * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
 65      *
 66      * @param file the file path to the file, null or "-" to return a byte array
 67      * @param okayToOverwrite true to overwrite an existing file
 68      * @return the UTF-8 encoded thread dump or message to return to the tool user
 69      */
 70     public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
 71         if (file == null || file.equals("-")) {
 72             return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
 73         } else {
 74             return dumpThreadsToFile(file, okayToOverwrite, false);
 75         }
 76     }
 77 
 78     /**
 79      * Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded.

 80      * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
 81      *
 82      * @param file the file path to the file, null or "-" to return a byte array
 83      * @param okayToOverwrite true to overwrite an existing file
 84      * @return the UTF-8 encoded thread dump or message to return to the tool user
 85      */
 86     public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
 87         if (file == null || file.equals("-")) {
 88             return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
 89         } else {
 90             return dumpThreadsToFile(file, okayToOverwrite, true);
 91         }
 92     }
 93 
 94     /**
 95      * Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
 96      * This method is the implementation of the Thread.dump_to_file diagnostic command
 97      * when a file path is not specified. It returns the thread and/or message to send
 98      * to the tool user.
 99      */
100     private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
101         var out = new BoundedByteArrayOutputStream(maxSize);
102         try (out; var writer = new TextWriter(out)) {
103             if (json) {
104                 dumpThreadsToJson(writer);
105             } else {
106                 dumpThreads(writer);
107             }
108         } catch (Exception ex) {
109             if (ex instanceof UncheckedIOException ioe) {
110                 ex = ioe.getCause();
111             }
112             String reply = String.format("Failed: %s%n", ex);
113             return reply.getBytes(StandardCharsets.UTF_8);
114         }
115         return out.toByteArray();
116     }
117 
118     /**
119      * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
120      * This method is the implementation of the Thread.dump_to_file diagnostic command.
121      * It returns the thread and/or message to send to the tool user.
122      */
123     private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
124         Path path = Path.of(file).toAbsolutePath();
125         OpenOption[] options = (okayToOverwrite)
126                 ? new OpenOption[0]
127                 : new OpenOption[] { StandardOpenOption.CREATE_NEW };
128         String reply;
129         try (OutputStream out = Files.newOutputStream(path, options)) {
130             try (var writer = new TextWriter(out)) {
131                 if (json) {
132                     dumpThreadsToJson(writer);
133                 } else {
134                     dumpThreads(writer);
135                 }
136                 reply = String.format("Created %s%n", path);
137             } catch (UncheckedIOException e) {
138                 reply = String.format("Failed: %s%n", e.getCause());
139             }
140         } catch (FileAlreadyExistsException _) {

141             reply = String.format("%s exists, use -overwrite to overwrite%n", path);
142         } catch (Exception ex) {
143             reply = String.format("Failed: %s%n", ex);
144         }
145         return reply.getBytes(StandardCharsets.UTF_8);
146     }
147 
148     /**
149      * Generate a thread dump in plain text format to the given output stream, UTF-8
150      * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
151      * @throws IOException if an I/O error occurs

152      */
153     public static void dumpThreads(OutputStream out) throws IOException {
154         var writer = new TextWriter(out);

155         try {
156             dumpThreads(writer);
157             writer.flush();
158         } catch (UncheckedIOException e) {
159             IOException ioe = e.getCause();
160             throw ioe;
161         }
162     }
163 
164     /**
165      * Generate a thread dump in plain text format to the given text stream.
166      * @throws UncheckedIOException if an I/O error occurs
167      */
168     private static void dumpThreads(TextWriter writer) {
169         writer.println(processId());
170         writer.println(Instant.now());
171         writer.println(Runtime.version());
172         writer.println();
173         dumpThreads(ThreadContainers.root(), writer);
174     }
175 
176     private static void dumpThreads(ThreadContainer container, TextWriter writer) {
177         container.threads().forEach(t -> dumpThread(t, writer));
178         container.children().forEach(c -> dumpThreads(c, writer));
179     }
180 
181     private static void dumpThread(Thread thread, TextWriter writer) {
182         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
183         Instant now = Instant.now();
184         Thread.State state = snapshot.threadState();
185         writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
186                 +  "\" " + state + " " + now);
187 
188         // park blocker
189         Object parkBlocker = snapshot.parkBlocker();
190         if (parkBlocker != null) {
191             writer.print("      // parked on " + Objects.toIdentityString(parkBlocker));
192             if (parkBlocker instanceof AbstractOwnableSynchronizer
193                     && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
194                 writer.print(", owned by #" + owner.threadId());
195             }
196             writer.println();
197         }
198 
199         // blocked on monitor enter or Object.wait
200         if (state == Thread.State.BLOCKED) {
201             Object obj = snapshot.blockedOn();
202             if (obj != null) {
203                 writer.println("      // blocked on " + Objects.toIdentityString(obj));
204             }
205         } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
206             Object obj = snapshot.waitingOn();
207             if (obj != null) {
208                 writer.println("      // waiting on " + Objects.toIdentityString(obj));
209             }
210         }
211 
212         StackTraceElement[] stackTrace = snapshot.stackTrace();
213         int depth = 0;
214         while (depth < stackTrace.length) {
215             snapshot.ownedMonitorsAt(depth).forEach(obj -> {
216                 writer.print("      // locked ");
217                 writer.println(Objects.toIdentityString(obj));
218             });
219             writer.print("      ");
220             writer.println(stackTrace[depth]);
221             depth++;
222         }
223         writer.println();
224     }
225 
226     /**
227      * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.

228      * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
229      * @throws IOException if an I/O error occurs
230      */
231     public static void dumpThreadsToJson(OutputStream out) throws IOException {
232         var writer = new TextWriter(out);

233         try {
234             dumpThreadsToJson(writer);
235             writer.flush();
236         } catch (UncheckedIOException e) {
237             IOException ioe = e.getCause();
238             throw ioe;
239         }
240     }
241 
242     /**
243      * Generate a thread dump to the given text stream in JSON format.
244      * @throws UncheckedIOException if an I/O error occurs
245      */
246     private static void dumpThreadsToJson(TextWriter textWriter) {
247         var jsonWriter = new JsonWriter(textWriter);
248 
249         jsonWriter.startObject();  // top-level object
250 
251         jsonWriter.startObject("threadDump");
252 
253         jsonWriter.writeProperty("processId", processId());
254         jsonWriter.writeProperty("time", Instant.now());
255         jsonWriter.writeProperty("runtimeVersion", Runtime.version());
256 
257         jsonWriter.startArray("threadContainers");
258         dumpThreads(ThreadContainers.root(), jsonWriter);
259         jsonWriter.endArray();
260 
261         jsonWriter.endObject();  // threadDump
262 
263         jsonWriter.endObject();  // end of top-level object




264     }
265 
266     /**
267      * Write a thread container to the given JSON writer.
268      * @throws UncheckedIOException if an I/O error occurs
269      */
270     private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
271         jsonWriter.startObject();
272         jsonWriter.writeProperty("container", container);
273         jsonWriter.writeProperty("parent", container.parent());








274 
275         Thread owner = container.owner();
276         jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);




277 
278         long threadCount = 0;
279         jsonWriter.startArray("threads");
280         Iterator<Thread> threads = container.threads().iterator();
281         while (threads.hasNext()) {
282             Thread thread = threads.next();
283             dumpThread(thread, jsonWriter);
284             threadCount++;
285         }
286         jsonWriter.endArray(); // threads
287 
288         // thread count
289         if (!ThreadContainers.trackAllThreads()) {
290             threadCount = Long.max(threadCount, container.threadCount());
291         }
292         jsonWriter.writeProperty("threadCount", threadCount);
293 
294         jsonWriter.endObject();
295 
296         // the children of the thread container follow
297         container.children().forEach(c -> dumpThreads(c, jsonWriter));

298     }
299 
300     /**
301      * Write a thread to the given JSON writer.
302      * @throws UncheckedIOException if an I/O error occurs
303      */
304     private static void dumpThread(Thread thread, JsonWriter jsonWriter) {
305         Instant now = Instant.now();
306         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
307         Thread.State state = snapshot.threadState();
308         StackTraceElement[] stackTrace = snapshot.stackTrace();
309 
310         jsonWriter.startObject();
311         jsonWriter.writeProperty("tid", thread.threadId());
312         jsonWriter.writeProperty("time", now);
313         if (thread.isVirtual()) {
314             jsonWriter.writeProperty("virtual", Boolean.TRUE);
315         }
316         jsonWriter.writeProperty("name", snapshot.threadName());
317         jsonWriter.writeProperty("state", state);
318 
319         // park blocker
320         Object parkBlocker = snapshot.parkBlocker();
321         if (parkBlocker != null) {
322             jsonWriter.startObject("parkBlocker");
323             jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
324             if (parkBlocker instanceof AbstractOwnableSynchronizer
325                     && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
326                 jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId());
327             }
328             jsonWriter.endObject();
329         }
330 
331         // blocked on monitor enter or Object.wait
332         if (state == Thread.State.BLOCKED) {
333             Object obj = snapshot.blockedOn();
334             if (obj != null) {
335                 jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
336             }
337         } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
338             Object obj = snapshot.waitingOn();
339             if (obj != null) {
340                 jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
341             }
342         }

343 
344         // stack trace
345         jsonWriter.startArray("stack");
346         Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
347         jsonWriter.endArray();
348 
349         // monitors owned, skip if none
350         if (snapshot.ownsMonitors()) {
351             jsonWriter.startArray("monitorsOwned");
352             int depth = 0;
353             while (depth < stackTrace.length) {
354                 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
355                 if (!objs.isEmpty()) {
356                     jsonWriter.startObject();
357                     jsonWriter.writeProperty("depth", depth);
358                     jsonWriter.startArray("locks");
359                     snapshot.ownedMonitorsAt(depth)
360                             .map(Objects::toIdentityString)
361                             .forEach(jsonWriter::writeProperty);
362                     jsonWriter.endArray();
363                     jsonWriter.endObject();
364                 }
365                 depth++;
366             }
367             jsonWriter.endArray();
368         }
369 
370         // thread identifier of carrier, when mounted
371         if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
372             jsonWriter.writeProperty("carrier", carrier.threadId());
373         }
374 
375         jsonWriter.endObject();


376     }
377 
378     /**
379      * Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
380      * This class is not intended to be a fully featured JSON writer.
381      */
382     private static class JsonWriter {
383         private static class Node {
384             final boolean isArray;
385             int propertyCount;
386             Node(boolean isArray) {
387                 this.isArray = isArray;
388             }
389             boolean isArray() {
390                 return isArray;
391             }
392             int propertyCount() {
393                 return propertyCount;
394             }
395             int getAndIncrementPropertyCount() {
396                 int old = propertyCount;
397                 propertyCount++;
398                 return old;
399             }
400         }
401         private final Deque<Node> stack = new ArrayDeque<>();
402         private final TextWriter writer;
403 
404         JsonWriter(TextWriter writer) {
405             this.writer = writer;
406         }
407 
408         private void indent() {
409             int indent = stack.size() * 2;
410             writer.print(" ".repeat(indent));
411         }
412 
413         /**
414          * Start of object or array.
415          */
416         private void startObject(String name, boolean isArray) {
417             if (!stack.isEmpty()) {
418                 Node node = stack.peek();
419                 if (node.getAndIncrementPropertyCount() > 0) {
420                     writer.println(",");
421                 }
422             }
423             indent();
424             if (name != null) {
425                 writer.print("\"" + name + "\": ");
426             }
427             writer.println(isArray ? "[" : "{");
428             stack.push(new Node(isArray));
429         }
430 
431         /**
432          * End of object or array.
433          */
434         private void endObject(boolean isArray) {
435             Node node = stack.pop();
436             if (node.isArray() != isArray)
437                 throw new IllegalStateException();
438             if (node.propertyCount() > 0) {
439                 writer.println();
440             }
441             indent();
442             writer.print(isArray ? "]" : "}");
443         }
444 
445         /**
446          * Write a property.
447          * @param name the property name, null for an unnamed property
448          * @param obj the value or null
449          */
450         void writeProperty(String name, Object obj) {
451             Node node = stack.peek();
452             if (node.getAndIncrementPropertyCount() > 0) {
453                 writer.println(",");
454             }
455             indent();
456             if (name != null) {
457                 writer.print("\"" + name + "\": ");
458             }
459             switch (obj) {
460                 // Long may be larger than safe range of JSON integer value
461                 case Long   _  -> writer.print("\"" + obj + "\"");
462                 case Number _  -> writer.print(obj);
463                 case Boolean _ -> writer.print(obj);
464                 case null      -> writer.print("null");
465                 default        -> writer.print("\"" + escape(obj.toString()) + "\"");
466             }
467         }
468 
469         /**
470          * Write an unnamed property.
471          */
472         void writeProperty(Object obj) {
473             writeProperty(null, obj);
474         }
475 
476         /**
477          * Start named object.
478          */
479         void startObject(String name) {
480             startObject(name, false);
481         }
482 
483         /**
484          * Start unnamed object.
485          */
486         void startObject() {
487             startObject(null);
488         }
489 
490         /**
491          * End of object.
492          */
493         void endObject() {
494             endObject(false);
495         }
496 
497         /**
498          * Start named array.
499          */
500         void startArray(String name) {
501             startObject(name, true);
502         }
503 
504         /**
505          * End of array.
506          */
507         void endArray() {
508             endObject(true);
509         }
510 
511         /**
512          * Escape any characters that need to be escape in the JSON output.
513          */
514         private static String escape(String value) {
515             StringBuilder sb = new StringBuilder();
516             for (int i = 0; i < value.length(); i++) {
517                 char c = value.charAt(i);
518                 switch (c) {
519                     case '"'  -> sb.append("\\\"");
520                     case '\\' -> sb.append("\\\\");
521                     case '/'  -> sb.append("\\/");
522                     case '\b' -> sb.append("\\b");
523                     case '\f' -> sb.append("\\f");
524                     case '\n' -> sb.append("\\n");
525                     case '\r' -> sb.append("\\r");
526                     case '\t' -> sb.append("\\t");
527                     default -> {
528                         if (c <= 0x1f) {
529                             sb.append(String.format("\\u%04x", c));
530                         } else {
531                             sb.append(c);
532                         }
533                     }
534                 }
535             }
536             return sb.toString();
537         }

538     }
539 
540     /**
541      * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
542      * written the subsequent bytes are discarded.
543      */
544     private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
545         final int max;
546         BoundedByteArrayOutputStream(int max) {
547             this.max = max;
548         }
549         @Override
550         public void write(int b) {
551             if (max < count) {
552                 super.write(b);
553             }
554         }
555         @Override
556         public void write(byte[] b, int off, int len) {
557             int remaining = max - count;
558             if (remaining > 0) {
559                 super.write(b, off, Integer.min(len, remaining));
560             }
561         }
562         @Override
563         public void close() {
564         }
565     }
566 
567     /**
568      * Simple Writer implementation for printing text. The print/println methods
569      * throw UncheckedIOException if an I/O error occurs.
570      */
571     private static class TextWriter extends Writer {
572         private final Writer delegate;
573 
574         TextWriter(OutputStream out) {
575             delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
576         }
577 
578         @Override
579         public void write(char[] cbuf, int off, int len) throws IOException {
580             delegate.write(cbuf, off, len);
581         }
582 
583         void print(Object obj) {
584             String s = String.valueOf(obj);
585             try {
586                 write(s, 0, s.length());
587             } catch (IOException ioe) {
588                 throw new UncheckedIOException(ioe);
589             }
590         }
591 
592         void println() {
593             print(System.lineSeparator());
594         }
595 
596         void println(String s) {
597             print(s);
598             println();
599         }
600 
601         void println(Object obj) {
602             print(obj);
603             println();
604         }
605 
606         @Override
607         public void flush() throws IOException {
608             delegate.flush();
609         }
610 
611         @Override
612         public void close() throws IOException {
613             delegate.close();
614         }
615     }
616 
617     /**
618      * Returns the process ID or -1 if not supported.
619      */
620     private static long processId() {
621         try {
622             return ProcessHandle.current().pid();
623         } catch (UnsupportedOperationException e) {
624             return -1L;
625         }
626     }
627 }
< prev index next >