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 }
|