1 /*
  2  * Copyright (c) 2020, 2021, 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.IOException;
 28 import java.io.OutputStream;
 29 import java.io.PrintStream;
 30 import java.nio.charset.StandardCharsets;
 31 import java.nio.file.FileAlreadyExistsException;
 32 import java.nio.file.Files;
 33 import java.nio.file.OpenOption;
 34 import java.nio.file.Path;
 35 import java.nio.file.StandardOpenOption;
 36 import java.util.ArrayList;
 37 import java.util.Iterator;
 38 import java.util.List;
 39 
 40 /**
 41  * Thread dump support.
 42  *
 43  * This class defines methods to dump threads to an output stream or file in
 44  * plain text or JSON format. Virtual threads are located if they are created in
 45  * a structured way with a ThreadExecutor.
 46  */
 47 public class ThreadDumper {
 48     private ThreadDumper() { }
 49 
 50     /**
 51      * Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
 52      */
 53     private static byte[] dumpThreads(String file, boolean okayToOverwrite, boolean json) {
 54         Path path = Path.of(file).toAbsolutePath();
 55         OpenOption[] options = (okayToOverwrite) ?
 56                 new OpenOption[0] : new OpenOption[] { StandardOpenOption.CREATE_NEW };
 57         String reply;
 58         try (OutputStream out = Files.newOutputStream(path, options);
 59              PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
 60             if (json) {
 61                 dumpThreadsToJson(ps);
 62             } else {
 63                 dumpThreads(ps);
 64             }
 65             reply = String.format("Created %s%n", path);
 66         } catch (FileAlreadyExistsException e) {
 67             reply = String.format("%s exists, use -overwrite to overwrite%n", path);
 68         } catch (IOException ioe) {
 69             reply = String.format("Failed: %s%n", ioe);
 70         }
 71         return reply.getBytes(StandardCharsets.UTF_8);
 72     }
 73 
 74     /**
 75      * Generate a thread dump in plain text format to the given file, UTF-8 encoded.
 76      */
 77     public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
 78         return dumpThreads(file, okayToOverwrite, false);
 79     }
 80 
 81     /**
 82      * Generate a thread dump in JSON format to the given file, UTF-8 encoded.
 83      */
 84     public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
 85         return dumpThreads(file, okayToOverwrite, true);
 86     }
 87 
 88     /**
 89      * Generate a thread dump in plain text format to the given output stream,
 90      * UTF-8 encoded.
 91      */
 92     public static void dumpThreads(OutputStream out) {
 93         PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8);
 94         dumpThreads(ThreadContainers.root(), ps);
 95         ps.flush();
 96     }
 97 
 98     private static void dumpThreads(ThreadContainer container, PrintStream ps) {
 99         container.threads().forEach(t -> dumpThread(t, ps));
100         ThreadContainers.children(container).forEach(c -> dumpThreads(c, ps));
101     }
102 
103     private static void dumpThread(Thread thread, PrintStream ps) {
104         String suffix = thread.isVirtual() ? " virtual" : "";
105         ps.format("\"%s\" #%d%s%n", thread.getName(), thread.getId(), suffix);
106         for (StackTraceElement ste : thread.getStackTrace()) {
107             ps.format("      %s%n", ste);
108         }
109         ps.println();
110     }
111 
112     /**
113      * Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
114      */
115     public static void dumpThreadsToJson(OutputStream out) {
116         PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8);
117         dumpThreadsToJson(ps);
118         ps.flush();
119     }
120 
121     /**
122      * Generate a thread dump to the given print stream in JSON format.
123      */
124     private static void dumpThreadsToJson(PrintStream out) {
125         out.println("{");
126         out.println("  \"threadDump\": {");
127         out.println("    \"threadContainers\": [");
128 
129         List<ThreadContainer> containers = allContainers();
130         Iterator<ThreadContainer> iterator = containers.iterator();
131         while (iterator.hasNext()) {
132             ThreadContainer container = iterator.next();
133             boolean more = iterator.hasNext();
134             dumpThreadsToJson(container, out, more);
135         }
136 
137         out.println("    ]");   // end of threadContainers
138         out.println("  }");   // end threadDump
139         out.println("}");  // end object
140     }
141 
142     /**
143      * Dump the given thread container to the print stream in JSON format.
144      */
145     private static void dumpThreadsToJson(ThreadContainer container,
146                                           PrintStream out,
147                                           boolean more) {
148         out.println("      {");
149         out.format("        \"container\": \"%s\",%n", escape(container.toString()));
150 
151         ThreadContainer parent = ThreadContainers.parent(container);
152         if (parent == null) {
153             out.format("        \"parent\": null,%n");
154         } else {
155             out.format("        \"parent\": \"%s\",%n", escape(parent.toString()));
156         }
157 
158         Thread owner = container.owner();
159         if (owner == null) {
160             out.format("        \"owner\": null,%n");
161         } else {
162             out.format("        \"owner\": %d,%n", owner.getId());
163         }
164 
165         long threadCount = 0;
166         out.println("        \"threads\": [");
167         Iterator<Thread> threads = container.threads().iterator();
168         while (threads.hasNext()) {
169             Thread thread = threads.next();
170             dumpThreadToJson(thread, out, threads.hasNext());
171             threadCount++;
172         }
173         out.println("        ],");   // end of threads
174 
175         // thread count
176         threadCount = Long.max(threadCount, container.threadCount());
177         out.format("        \"threadCount\": %d%n", threadCount);
178 
179         if (more) {
180             out.println("      },");
181         } else {
182             out.println("      }");  // last container, no trailing comma
183         }
184     }
185 
186     /**
187      * Dump the given thread and its stack trace to the print stream in JSON format.
188      */
189     private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
190         out.println("         {");
191         out.format("           \"name\": \"%s\",%n", escape(thread.getName()));
192         out.format("           \"tid\": %d,%n", thread.getId());
193         out.format("           \"stack\": [%n");
194         int i = 0;
195         StackTraceElement[] stackTrace = thread.getStackTrace();
196         while (i < stackTrace.length) {
197             out.format("              \"%s\"", escape(stackTrace[i].toString()));
198             i++;
199             if (i < stackTrace.length) {
200                 out.println(",");
201             } else {
202                 out.println();  // last element, no trailing comma
203             }
204         }
205         out.println("           ]");
206         if (more) {
207             out.println("         },");
208         } else {
209             out.println("         }");  // last thread, no trailing comma
210         }
211     }
212 
213     /**
214      * Returns a list of all thread containers that are "reachable" from
215      * the root container.
216      */
217     private static List<ThreadContainer> allContainers() {
218         List<ThreadContainer> containers = new ArrayList<>();
219         collect(ThreadContainers.root(), containers);
220         return containers;
221     }
222 
223     private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
224         containers.add(container);
225         ThreadContainers.children(container).forEach(c -> collect(c, containers));
226     }
227 
228     /**
229      * Escape any characters that need to be escape in the JSON output.
230      */
231     private static String escape(String value) {
232         StringBuilder sb = new StringBuilder();
233         for (int i = 0; i < value.length(); i++) {
234             char c = value.charAt(i);
235             switch (c) {
236                 case '"'  -> sb.append("\\\"");
237                 case '\\' -> sb.append("\\\\");
238                 case '/'  -> sb.append("\\/");
239                 case '\b' -> sb.append("\\b");
240                 case '\f' -> sb.append("\\f");
241                 case '\n' -> sb.append("\\n");
242                 case '\r' -> sb.append("\\r");
243                 case '\t' -> sb.append("\\t");
244                 default -> {
245                     if (c <= 0x1f) {
246                         sb.append(String.format("\\u%04x", c));
247                     } else {
248                         sb.append(c);
249                     }
250                 }
251             }
252         }
253         return sb.toString();
254     }
255 }