blob: 95e9a1ab8ceede8ba0e94c3c300587455a1b3d9c [file] [log] [blame]
// 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;
}
}