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 
 48 /**
 49  * Thread dump support.
 50  *
 51  * This class defines static methods to support the Thread.dump_to_file diagnostic command
 52  * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
 53  * thread dump to a file or byte array in plain text or JSON format.
 54  */
 55 public class ThreadDumper {
 56     private ThreadDumper() { }
 57 
 58     // the maximum byte array to return when generating the thread dump to a byte array
 59     private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
 60 
 61     /**
 62      * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.
 63      * This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
 64      *
 65      * @param file the file path to the file, null or "-" to return a byte array
 66      * @param okayToOverwrite true to overwrite an existing file
 67      * @param ignore not used
 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, boolean ignore) {
 71         if (file == null || file.equals("-")) {
 72             return dumpThreadsToByteArray(false, false, MAX_BYTE_ARRAY_SIZE);
 73         } else {
 74             return dumpThreadsToFile(file, okayToOverwrite, false, 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, boolean minify) {
 87         if (file == null || file.equals("-")) {
 88             return dumpThreadsToByteArray(true, !minify, MAX_BYTE_ARRAY_SIZE);
 89         } else {
 90             return dumpThreadsToFile(file, okayToOverwrite, true, minify);
 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 dump and/or message to
 98      * send to the tool user.
 99      */
100     private static byte[] dumpThreadsToByteArray(boolean json,  boolean minify, int maxSize) {
101         var out = new BoundedByteArrayOutputStream(maxSize);
102         try (out; var writer = new TextWriter(out)) {
103             if (json) {
104                 dumpThreadsToJson(writer, minify);
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 dump and/or message to send to the tool user.
122      */
123     private static byte[] dumpThreadsToFile(String file,
124                                             boolean okayToOverwrite,
125                                             boolean json,
126                                             boolean minify) {
127         Path path = Path.of(file).toAbsolutePath();
128         OpenOption[] options = (okayToOverwrite)
129                 ? new OpenOption[0]
130                 : new OpenOption[] { StandardOpenOption.CREATE_NEW };
131         String reply;
132         try (OutputStream out = Files.newOutputStream(path, options)) {
133             try (var writer = new TextWriter(out)) {
134                 if (json) {
135                     dumpThreadsToJson(writer, minify);
136                 } else {
137                     dumpThreads(writer);
138                 }
139                 reply = String.format("Created %s%n", path);
140             } catch (UncheckedIOException e) {
141                 reply = String.format("Failed: %s%n", e.getCause());
142             }
143         } catch (FileAlreadyExistsException _) {
144             reply = String.format("%s exists, use -overwrite to overwrite%n", path);
145         } catch (Exception ex) {
146             reply = String.format("Failed: %s%n", ex);
147         }
148         return reply.getBytes(StandardCharsets.UTF_8);
149     }
150 
151     /**
152      * Generate a thread dump in plain text format to the given output stream, UTF-8
153      * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
154      * @throws IOException if an I/O error occurs
155      */
156     public static void dumpThreads(OutputStream out) throws IOException {
157         var writer = new TextWriter(out);
158         try {
159             dumpThreads(writer);
160             writer.flush();
161         } catch (UncheckedIOException e) {
162             IOException ioe = e.getCause();
163             throw ioe;
164         }
165     }
166 
167     /**
168      * Generate a thread dump in plain text format to the given text stream.
169      * @throws UncheckedIOException if an I/O error occurs
170      */
171     private static void dumpThreads(TextWriter writer) {
172         writer.println(processId());
173         writer.println(Instant.now());
174         writer.println(Runtime.version());
175         writer.println();
176         dumpThreads(ThreadContainers.root(), writer);
177     }
178 
179     private static void dumpThreads(ThreadContainer container, TextWriter writer) {
180         container.threads().forEach(t -> dumpThread(t, writer));
181         container.children().forEach(c -> dumpThreads(c, writer));
182     }
183 
184     private static boolean dumpThread(Thread thread, TextWriter writer) {
185         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
186         if (snapshot == null) {
187             return false; // thread not alive
188         }
189         Instant now = Instant.now();
190         Thread.State state = snapshot.threadState();
191         writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
192                 + "\" " + (thread.isVirtual() ? "virtual " : "") + state + " " + now);
193 
194         StackTraceElement[] stackTrace = snapshot.stackTrace();
195         int depth = 0;
196         while (depth < stackTrace.length) {
197             writer.print("    at ");
198             writer.println(stackTrace[depth]);
199             snapshot.ownedMonitorsAt(depth).forEach(o -> {
200                 if (o != null) {
201                     writer.println("    - locked " + decorateObject(o));
202                 } else {
203                     writer.println("    - lock is eliminated");
204                 }
205             });
206 
207             // if parkBlocker set, or blocked/waiting on monitor, then print after top frame
208             if (depth == 0) {
209                 // park blocker
210                 Object parkBlocker = snapshot.parkBlocker();
211                 if (parkBlocker != null) {
212                     String suffix = (snapshot.parkBlockerOwner() instanceof Thread owner)
213                             ? ", owner #"  + owner.threadId()
214                             : "";
215                     writer.println("    - parking to wait for " + decorateObject(parkBlocker) + suffix);
216                 }
217 
218                 // blocked on monitor enter or Object.wait
219                 if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
220                     writer.println("    - waiting to lock " + decorateObject(obj));
221                 } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
222                         && snapshot.waitingOn() instanceof Object obj) {
223                     writer.println("    - waiting on " + decorateObject(obj));
224                 }
225             }
226 
227             depth++;
228         }
229         writer.println();
230         return true;
231     }
232 
233     /**
234      * Returns the identity string for the given object in a form suitable for the plain
235      * text format thread dump.
236      */
237     private static String decorateObject(Object obj) {
238         return "<" + Objects.toIdentityString(obj) + ">";
239     }
240 
241     /**
242      * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
243      * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
244      * @throws IOException if an I/O error occurs
245      */
246     public static void dumpThreadsToJson(OutputStream out) throws IOException {
247         var writer = new TextWriter(out);
248         try {
249             dumpThreadsToJson(writer, /*prettyPrint*/ true);
250             writer.flush();
251         } catch (UncheckedIOException e) {
252             IOException ioe = e.getCause();
253             throw ioe;
254         }
255     }
256 
257     /**
258      * Generate a thread dump to the given text stream in JSON format.
259      * @throws UncheckedIOException if an I/O error occurs
260      */
261     private static void dumpThreadsToJson(TextWriter textWriter, boolean minify) {
262         var jsonWriter = new JsonWriter(textWriter, minify);
263 
264         jsonWriter.startObject();  // top-level object
265 
266         jsonWriter.startObject("threadDump");
267 
268         jsonWriter.writeProperty("processId", processId());
269         jsonWriter.writeProperty("time", Instant.now());
270         jsonWriter.writeProperty("runtimeVersion", Runtime.version());
271 
272         jsonWriter.startArray("threadContainers");
273         dumpThreads(ThreadContainers.root(), jsonWriter);
274         jsonWriter.endArray();
275 
276         jsonWriter.endObject();  // threadDump
277 
278         jsonWriter.endObject();  // end of top-level object
279     }
280 
281     /**
282      * Write a thread container to the given JSON writer.
283      * @throws UncheckedIOException if an I/O error occurs
284      */
285     private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
286         jsonWriter.startObject();
287         jsonWriter.writeProperty("container", container);
288         jsonWriter.writeProperty("parent", container.parent());
289 
290         Thread owner = container.owner();
291         jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
292 
293         long threadCount = 0;
294         jsonWriter.startArray("threads");
295         Iterator<Thread> threads = container.threads().iterator();
296         while (threads.hasNext()) {
297             Thread thread = threads.next();
298             if (dumpThread(thread, jsonWriter)) {
299                 threadCount++;
300             }
301         }
302         jsonWriter.endArray(); // threads
303 
304         // thread count
305         if (!ThreadContainers.trackAllThreads()) {
306             threadCount = Long.max(threadCount, container.threadCount());
307         }
308         jsonWriter.writeProperty("threadCount", threadCount);
309 
310         jsonWriter.endObject();
311 
312         // the children of the thread container follow
313         container.children().forEach(c -> dumpThreads(c, jsonWriter));
314     }
315 
316     /**
317      * Write a thread to the given JSON writer.
318      * @return true if the thread dump was written, false otherwise
319      * @throws UncheckedIOException if an I/O error occurs
320      */
321     private static boolean dumpThread(Thread thread, JsonWriter jsonWriter) {
322         Instant now = Instant.now();
323         ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
324         if (snapshot == null) {
325             return false; // thread not alive
326         }
327         Thread.State state = snapshot.threadState();
328         StackTraceElement[] stackTrace = snapshot.stackTrace();
329 
330         jsonWriter.startObject();
331         jsonWriter.writeProperty("tid", thread.threadId());
332         jsonWriter.writeProperty("time", now);
333         if (thread.isVirtual()) {
334             jsonWriter.writeProperty("virtual", Boolean.TRUE);
335         }
336         jsonWriter.writeProperty("name", snapshot.threadName());
337         jsonWriter.writeProperty("state", state);
338 
339         // park blocker
340         Object parkBlocker = snapshot.parkBlocker();
341         if (parkBlocker != null) {
342             // parkBlocker is an object to allow for exclusiveOwnerThread in the future
343             jsonWriter.startObject("parkBlocker");
344             jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
345             if (snapshot.parkBlockerOwner() instanceof Thread owner) {
346                 jsonWriter.writeProperty("owner", owner.threadId());
347             }
348             jsonWriter.endObject();
349         }
350 
351         // blocked on monitor enter or Object.wait
352         if (state == Thread.State.BLOCKED && snapshot.blockedOn() instanceof Object obj) {
353             jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
354         } else if ((state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING)
355                 && snapshot.waitingOn() instanceof Object obj) {
356             jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
357         }
358 
359         // stack trace
360         jsonWriter.startArray("stack");
361         Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
362         jsonWriter.endArray();
363 
364         // monitors owned, skip if none
365         if (snapshot.ownsMonitors()) {
366             jsonWriter.startArray("monitorsOwned");
367             int depth = 0;
368             while (depth < stackTrace.length) {
369                 List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
370                 if (!objs.isEmpty()) {
371                     jsonWriter.startObject();
372                     jsonWriter.writeProperty("depth", depth);
373                     jsonWriter.startArray("locks");
374                     snapshot.ownedMonitorsAt(depth)
375                             .map(o -> (o != null) ? Objects.toIdentityString(o) : null)
376                             .forEach(jsonWriter::writeProperty);
377                     jsonWriter.endArray();
378                     jsonWriter.endObject();
379                 }
380                 depth++;
381             }
382             jsonWriter.endArray();
383         }
384 
385         // thread identifier of carrier, when mounted
386         if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
387             jsonWriter.writeProperty("carrier", carrier.threadId());
388         }
389 
390         jsonWriter.endObject();
391         return true;
392     }
393 
394     /**
395      * Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
396      * This class is not intended to be a fully featured JSON writer.
397      */
398     private static class JsonWriter {
399         private static class Node {
400             final boolean isArray;
401             int propertyCount;
402             Node(boolean isArray) {
403                 this.isArray = isArray;
404             }
405             boolean isArray() {
406                 return isArray;
407             }
408             int propertyCount() {
409                 return propertyCount;
410             }
411             int getAndIncrementPropertyCount() {
412                 int old = propertyCount;
413                 propertyCount++;
414                 return old;
415             }
416         }
417         private final Deque<Node> stack = new ArrayDeque<>();
418         private final TextWriter writer;
419         private final boolean prettyPrint;  // pretty print or minify
420 
421         JsonWriter(TextWriter writer, boolean minify) {
422             this.writer = writer;
423             this.prettyPrint = !minify;
424         }
425 
426         private void print(Object obj) {
427             writer.print(obj);
428         }
429 
430         private void println(Object obj) {
431             if (prettyPrint) {
432                 writer.println(obj);
433             } else {
434                 writer.print(obj);
435             }
436         }
437 
438         private void println() {
439             if (prettyPrint) {
440                 writer.println();
441             }
442         }
443 
444         private void indent() {
445             if (prettyPrint) {
446                 int indent = stack.size() * 2;
447                 writer.print(" ".repeat(indent));
448             }
449         }
450 
451         /**
452          * Start of object or array.
453          */
454         private void startObject(String name, boolean isArray) {
455             if (!stack.isEmpty()) {
456                 Node node = stack.peek();
457                 if (node.getAndIncrementPropertyCount() > 0) {
458                     println(",");
459                 }
460             }
461             indent();
462             if (name != null) {
463                 String gap = prettyPrint ? " " : "";
464                 print("\"" + name + "\":" + gap);
465             }
466             println(isArray ? "[" : "{");
467             stack.push(new Node(isArray));
468         }
469 
470         /**
471          * End of object or array.
472          */
473         private void endObject(boolean isArray) {
474             Node node = stack.pop();
475             if (node.isArray() != isArray)
476                 throw new IllegalStateException();
477             if (node.propertyCount() > 0) {
478                 println();
479             }
480             indent();
481             print(isArray ? "]" : "}");
482         }
483 
484         /**
485          * Write a property.
486          * @param name the property name, null for an unnamed property
487          * @param obj the value or null
488          */
489         void writeProperty(String name, Object obj) {
490             Node node = stack.peek();
491             assert node != null;
492             if (node.getAndIncrementPropertyCount() > 0) {
493                 println(",");
494             }
495             indent();
496             if (name != null) {
497                 print("\"" + name + "\": ");
498             }
499             switch (obj) {
500                 // Long may be larger than safe range of JSON integer value
501                 case Long   _  -> print("\"" + obj + "\"");
502                 case Number _  -> print(obj);
503                 case Boolean _ -> print(obj);
504                 case null      -> print("null");
505                 default        -> print("\"" + escape(obj.toString()) + "\"");
506             }
507         }
508 
509         /**
510          * Write an unnamed property.
511          */
512         void writeProperty(Object obj) {
513             writeProperty(null, obj);
514         }
515 
516         /**
517          * Start named object.
518          */
519         void startObject(String name) {
520             startObject(name, false);
521         }
522 
523         /**
524          * Start unnamed object.
525          */
526         void startObject() {
527             startObject(null);
528         }
529 
530         /**
531          * End of object.
532          */
533         void endObject() {
534             endObject(false);
535         }
536 
537         /**
538          * Start named array.
539          */
540         void startArray(String name) {
541             startObject(name, true);
542         }
543 
544         /**
545          * End of array.
546          */
547         void endArray() {
548             endObject(true);
549         }
550 
551         /**
552          * Escape any characters that need to be escape in the JSON output.
553          */
554         private static String escape(String value) {
555             StringBuilder sb = new StringBuilder();
556             for (int i = 0; i < value.length(); i++) {
557                 char c = value.charAt(i);
558                 switch (c) {
559                     case '"'  -> sb.append("\\\"");
560                     case '\\' -> sb.append("\\\\");
561                     case '/'  -> sb.append("\\/");
562                     case '\b' -> sb.append("\\b");
563                     case '\f' -> sb.append("\\f");
564                     case '\n' -> sb.append("\\n");
565                     case '\r' -> sb.append("\\r");
566                     case '\t' -> sb.append("\\t");
567                     default -> {
568                         if (c <= 0x1f) {
569                             sb.append(String.format("\\u%04x", c));
570                         } else {
571                             sb.append(c);
572                         }
573                     }
574                 }
575             }
576             return sb.toString();
577         }
578     }
579 
580     /**
581      * A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
582      * written the subsequent bytes are discarded.
583      */
584     private static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
585         final int max;
586         BoundedByteArrayOutputStream(int max) {
587             this.max = max;
588         }
589         @Override
590         public void write(int b) {
591             if (max < count) {
592                 super.write(b);
593             }
594         }
595         @Override
596         public void write(byte[] b, int off, int len) {
597             int remaining = max - count;
598             if (remaining > 0) {
599                 super.write(b, off, Integer.min(len, remaining));
600             }
601         }
602         @Override
603         public void close() {
604         }
605     }
606 
607     /**
608      * Simple Writer implementation for printing text. The print/println methods
609      * throw UncheckedIOException if an I/O error occurs.
610      */
611     private static class TextWriter extends Writer {
612         private final Writer delegate;
613 
614         TextWriter(OutputStream out) {
615             delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
616         }
617 
618         @Override
619         public void write(char[] cbuf, int off, int len) throws IOException {
620             delegate.write(cbuf, off, len);
621         }
622 
623         void print(Object obj) {
624             String s = String.valueOf(obj);
625             try {
626                 write(s, 0, s.length());
627             } catch (IOException ioe) {
628                 throw new UncheckedIOException(ioe);
629             }
630         }
631 
632         void println() {
633             print(System.lineSeparator());
634         }
635 
636         void println(String s) {
637             print(s);
638             println();
639         }
640 
641         void println(Object obj) {
642             print(obj);
643             println();
644         }
645 
646         @Override
647         public void flush() throws IOException {
648             delegate.flush();
649         }
650 
651         @Override
652         public void close() throws IOException {
653             delegate.close();
654         }
655     }
656 
657     /**
658      * Returns the process ID or -1 if not supported.
659      */
660     private static long processId() {
661         try {
662             return ProcessHandle.current().pid();
663         } catch (UnsupportedOperationException e) {
664             return -1L;
665         }
666     }
667 }