| // Copyright 2025 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.util; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStream; | |
| import java.io.PrintWriter; | |
| import java.util.ArrayList; | |
| import java.util.Comparator; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.regex.Pattern; | |
| /** | |
| * Analyzes thread dumps from {@code jcmd Thread.dump_to_file}, or {@link | |
| * com.google.devtools.build.lib.util.ThreadDumper}. | |
| * | |
| * <p>The analyzer groups threads with the same stack trace and sorts them by name. | |
| */ | |
| public final class ThreadDumpAnalyzer { | |
| private static final Pattern THREAD_PATTERN = Pattern.compile("#(\\d+)\\s\"([^\"]+)\".*"); | |
| private static final Pattern THREAD_STATE_PATTERN = | |
| Pattern.compile("\\s+-\\s(locked|lock|parking|waiting).*"); | |
| private final Map<String, List<ThreadLine>> threadsPerStackTrace = new HashMap<>(); | |
| private record ThreadLine(String raw, String id, String name, List<String> states) {} | |
| /** | |
| * The result of the thread dump analysis. | |
| * | |
| * @param otherLines The lines that are not related to threads. | |
| * @param groupedStackTraces The stack traces of the threads that have the same stack trace. The | |
| * threads in each stack trace are sorted by name. | |
| */ | |
| public record AnalyzedThreadDump(List<String> otherLines, List<String> groupedStackTraces) {} | |
| /** | |
| * Analyzes the given thread dump from the given input stream. | |
| * | |
| * @return The analyzed thread dump. | |
| */ | |
| public AnalyzedThreadDump analyze(InputStream in) throws IOException { | |
| var otherLines = new ArrayList<String>(); | |
| var reader = new BufferedReader(new InputStreamReader(in, UTF_8)); | |
| while (true) { | |
| var line = reader.readLine(); | |
| if (line == null) { | |
| break; | |
| } | |
| var threadMatcher = THREAD_PATTERN.matcher(line); | |
| if (threadMatcher.matches()) { | |
| var threadLine = | |
| new ThreadLine(line, threadMatcher.group(1), threadMatcher.group(2), new ArrayList<>()); | |
| if (groupStackTrace(threadLine, reader)) { | |
| break; | |
| } | |
| } else { | |
| otherLines.add(line); | |
| } | |
| } | |
| // Sort the threads with the same stack trace by name | |
| for (var threads : threadsPerStackTrace.values()) { | |
| threads.sort((a, b) -> a.name().compareTo(b.name())); | |
| } | |
| var sortedEntries = new ArrayList<>(threadsPerStackTrace.entrySet()); | |
| // Sort the entries by the first thread's name in the group. | |
| sortedEntries.sort(Comparator.comparing(x -> x.getValue().getFirst().name())); | |
| var groupedStackTraces = new ArrayList<String>(); | |
| for (var entry : sortedEntries) { | |
| var sb = new StringBuilder(); | |
| var stackTrace = entry.getKey(); | |
| var threads = entry.getValue(); | |
| for (var thread : threads) { | |
| sb.append(thread.raw()).append(System.lineSeparator()); | |
| for (var state : thread.states()) { | |
| sb.append(state).append(System.lineSeparator()); | |
| } | |
| } | |
| sb.append(stackTrace); | |
| groupedStackTraces.add(sb.toString()); | |
| } | |
| return new AnalyzedThreadDump(otherLines, groupedStackTraces); | |
| } | |
| /** | |
| * Analyzes the given thread dump from the given input stream and writes the analysis to the given | |
| * output stream. | |
| */ | |
| public void analyze(InputStream in, OutputStream out) throws IOException { | |
| var analyzedThreadDump = analyze(in); | |
| try (var writer = new PrintWriter(out, false, UTF_8)) { | |
| for (var line : analyzedThreadDump.otherLines) { | |
| writer.println(line); | |
| } | |
| for (var stackTrace : analyzedThreadDump.groupedStackTraces) { | |
| writer.println(stackTrace); | |
| } | |
| } | |
| } | |
| /** | |
| * Groups the stack trace of the given thread with other threads having the same stack trace. | |
| * | |
| * @return true if reached EOF. | |
| */ | |
| private boolean groupStackTrace(ThreadLine threadLine, BufferedReader reader) throws IOException { | |
| StringBuilder sb = new StringBuilder(); | |
| boolean eof = false; | |
| while (true) { | |
| var line = reader.readLine(); | |
| if (line == null) { | |
| eof = true; | |
| break; | |
| } | |
| if (line.isBlank()) { | |
| break; | |
| } | |
| if (THREAD_STATE_PATTERN.matcher(line).matches()) { | |
| threadLine.states.add(line); | |
| } else { | |
| sb.append(line).append(System.lineSeparator()); | |
| } | |
| } | |
| var stackTrace = sb.toString(); | |
| var threads = threadsPerStackTrace.computeIfAbsent(stackTrace, t -> new ArrayList<>()); | |
| threads.add(threadLine); | |
| return eof; | |
| } | |
| } |