Project Overview
A comprehensive thread dump analyzer that parses, analyzes, and provides insights into Java thread dumps. Helps identify performance issues, deadlocks, resource contention, and other threading problems.
Technology Stack
- Core: Java 17+
- Parsing: Regex patterns, Java NIO
- Analysis: Custom algorithms for pattern detection
- Visualization: HTML reports with charts
- Concurrency: Parallel processing for large dumps
- Data Structures: Custom graphs for deadlock detection
Project Structure
thread-dump-analyzer/ ├── src/main/java/com/threadanalyzer/ │ ├── parser/ │ ├── model/ │ ├── analyzer/ │ ├── reporter/ │ ├── detector/ │ └── cli/ ├── src/main/resources/ │ └── templates/ ├── examples/ │ └── thread-dumps/ └── config/
Core Implementation
1. Data Models
package com.threadanalyzer.model;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
public class ThreadDump {
private final String id;
private final LocalDateTime timestamp;
private final List<ThreadInfo> threads;
private final String source;
private final Map<String, Object> metadata;
public ThreadDump(String source) {
this.id = UUID.randomUUID().toString();
this.timestamp = LocalDateTime.now();
this.threads = new ArrayList<>();
this.source = source;
this.metadata = new HashMap<>();
}
// Builder pattern
public static class Builder {
private String source;
private List<ThreadInfo> threads = new ArrayList<>();
private Map<String, Object> metadata = new HashMap<>();
public Builder source(String source) {
this.source = source;
return this;
}
public Builder addThread(ThreadInfo thread) {
this.threads.add(thread);
return this;
}
public Builder metadata(String key, Object value) {
this.metadata.put(key, value);
return this;
}
public ThreadDump build() {
ThreadDump dump = new ThreadDump(source);
dump.threads.addAll(threads);
dump.metadata.putAll(metadata);
return dump;
}
}
// Getters
public String getId() { return id; }
public LocalDateTime getTimestamp() { return timestamp; }
public List<ThreadInfo> getThreads() { return Collections.unmodifiableList(threads); }
public String getSource() { return source; }
public Map<String, Object> getMetadata() { return Collections.unmodifiableMap(metadata); }
// Utility methods
public int getThreadCount() { return threads.size(); }
public long getDaemonThreadCount() {
return threads.stream().filter(ThreadInfo::isDaemon).count();
}
public long getNonDaemonThreadCount() {
return threads.stream().filter(t -> !t.isDaemon()).count();
}
public Map<ThreadState, Long> getThreadStateDistribution() {
Map<ThreadState, Long> distribution = new EnumMap<>(ThreadState.class);
for (ThreadInfo thread : threads) {
distribution.merge(thread.getState(), 1L, Long::sum);
}
return distribution;
}
public List<ThreadInfo> getThreadsByState(ThreadState state) {
return threads.stream()
.filter(t -> t.getState() == state)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public Optional<ThreadInfo> findThreadById(long threadId) {
return threads.stream()
.filter(t -> t.getThreadId() == threadId)
.findFirst();
}
public List<ThreadInfo> findThreadsByName(String namePattern) {
Pattern pattern = Pattern.compile(namePattern);
return threads.stream()
.filter(t -> pattern.matcher(t.getThreadName()).matches())
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
@Override
public String toString() {
return String.format("ThreadDump[source=%s, threads=%d, timestamp=%s]",
source, threads.size(), timestamp);
}
}
public class ThreadInfo {
private final long threadId;
private final String threadName;
private final boolean daemon;
private final int priority;
private final ThreadState state;
private final String lockInfo;
private final String lockOwnerId;
private final String lockOwnerName;
private final List<StackFrame> stackTrace;
private final Map<String, String> additionalInfo;
public ThreadInfo(long threadId, String threadName, boolean daemon,
int priority, ThreadState state) {
this.threadId = threadId;
this.threadName = threadName;
this.daemon = daemon;
this.priority = priority;
this.state = state;
this.stackTrace = new ArrayList<>();
this.additionalInfo = new HashMap<>();
}
// Builder pattern
public static class Builder {
private long threadId;
private String threadName;
private boolean daemon;
private int priority;
private ThreadState state;
private String lockInfo;
private String lockOwnerId;
private String lockOwnerName;
private List<StackFrame> stackTrace = new ArrayList<>();
private Map<String, String> additionalInfo = new HashMap<>();
public Builder threadId(long threadId) {
this.threadId = threadId;
return this;
}
public Builder threadName(String threadName) {
this.threadName = threadName;
return this;
}
public Builder daemon(boolean daemon) {
this.daemon = daemon;
return this;
}
public Builder priority(int priority) {
this.priority = priority;
return this;
}
public Builder state(ThreadState state) {
this.state = state;
return this;
}
public Builder lockInfo(String lockInfo) {
this.lockInfo = lockInfo;
return this;
}
public Builder lockOwnerId(String lockOwnerId) {
this.lockOwnerId = lockOwnerId;
return this;
}
public Builder lockOwnerName(String lockOwnerName) {
this.lockOwnerName = lockOwnerName;
return this;
}
public Builder addStackFrame(StackFrame frame) {
this.stackTrace.add(frame);
return this;
}
public Builder addStackFrames(List<StackFrame> frames) {
this.stackTrace.addAll(frames);
return this;
}
public Builder additionalInfo(String key, String value) {
this.additionalInfo.put(key, value);
return this;
}
public ThreadInfo build() {
ThreadInfo thread = new ThreadInfo(threadId, threadName, daemon, priority, state);
thread.lockInfo = lockInfo;
thread.lockOwnerId = lockOwnerId;
thread.lockOwnerName = lockOwnerName;
thread.stackTrace.addAll(stackTrace);
thread.additionalInfo.putAll(additionalInfo);
return thread;
}
}
// Getters
public long getThreadId() { return threadId; }
public String getThreadName() { return threadName; }
public boolean isDaemon() { return daemon; }
public int getPriority() { return priority; }
public ThreadState getState() { return state; }
public String getLockInfo() { return lockInfo; }
public String getLockOwnerId() { return lockOwnerId; }
public String getLockOwnerName() { return lockOwnerName; }
public List<StackFrame> getStackTrace() { return Collections.unmodifiableList(stackTrace); }
public Map<String, String> getAdditionalInfo() { return Collections.unmodifiableMap(additionalInfo); }
// Utility methods
public boolean isBlocked() {
return state == ThreadState.BLOCKED;
}
public boolean isWaiting() {
return state == ThreadState.WAITING || state == ThreadState.TIMED_WAITING;
}
public boolean isInNative() {
return stackTrace.stream()
.anyMatch(StackFrame::isNativeMethod);
}
public String getTopStackFrame() {
return stackTrace.isEmpty() ? "" : stackTrace.get(0).toString();
}
public boolean containsInStackTrace(String pattern) {
return stackTrace.stream()
.anyMatch(frame -> frame.toString().contains(pattern));
}
public List<StackFrame> findInStackTrace(String pattern) {
return stackTrace.stream()
.filter(frame -> frame.toString().contains(pattern))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
@Override
public String toString() {
return String.format("Thread[id=%d, name=%s, state=%s]",
threadId, threadName, state);
}
}
public class StackFrame {
private final String className;
private final String methodName;
private final String fileName;
private final int lineNumber;
private final boolean nativeMethod;
public StackFrame(String className, String methodName, String fileName,
int lineNumber, boolean nativeMethod) {
this.className = className;
this.methodName = methodName;
this.fileName = fileName;
this.lineNumber = lineNumber;
this.nativeMethod = nativeMethod;
}
// Getters
public String getClassName() { return className; }
public String getMethodName() { return methodName; }
public String getFileName() { return fileName; }
public int getLineNumber() { return lineNumber; }
public boolean isNativeMethod() { return nativeMethod; }
public String getFullMethodName() {
return className + "." + methodName;
}
public boolean isJDKMethod() {
return className.startsWith("java.") ||
className.startsWith("javax.") ||
className.startsWith("sun.") ||
className.startsWith("com.sun.");
}
public boolean isApplicationMethod() {
return !isJDKMethod() && !isNativeMethod();
}
@Override
public String toString() {
if (nativeMethod) {
return String.format("%s.%s(Native Method)", className, methodName);
} else if (fileName != null && lineNumber >= 0) {
return String.format("%s.%s(%s:%d)", className, methodName, fileName, lineNumber);
} else if (fileName != null) {
return String.format("%s.%s(%s)", className, methodName, fileName);
} else {
return String.format("%s.%s(Unknown Source)", className, methodName);
}
}
}
public enum ThreadState {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
public static ThreadState fromString(String state) {
if (state == null) return null;
switch (state.toUpperCase()) {
case "NEW": return NEW;
case "RUNNABLE": return RUNNABLE;
case "BLOCKED": return BLOCKED;
case "WAITING": return WAITING;
case "TIMED_WAITING": return TIMED_WAITING;
case "TERMINATED": return TERMINATED;
default: return null;
}
}
}
public class Deadlock {
private final String id;
private final List<ThreadInfo> involvedThreads;
private final List<LockCycle> lockCycles;
private final String description;
public Deadlock(List<ThreadInfo> involvedThreads, List<LockCycle> lockCycles) {
this.id = UUID.randomUUID().toString();
this.involvedThreads = new ArrayList<>(involvedThreads);
this.lockCycles = new ArrayList<>(lockCycles);
this.description = generateDescription();
}
// Getters
public String getId() { return id; }
public List<ThreadInfo> getInvolvedThreads() { return Collections.unmodifiableList(involvedThreads); }
public List<LockCycle> getLockCycles() { return Collections.unmodifiableList(lockCycles); }
public String getDescription() { return description; }
private String generateDescription() {
if (involvedThreads.isEmpty()) return "Empty deadlock";
StringBuilder sb = new StringBuilder();
sb.append("Deadlock involving ").append(involvedThreads.size()).append(" threads: ");
for (int i = 0; i < involvedThreads.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(involvedThreads.get(i).getThreadName())
.append(" (")
.append(involvedThreads.get(i).getThreadId())
.append(")");
}
return sb.toString();
}
@Override
public String toString() {
return description;
}
}
public class LockCycle {
private final List<LockNode> nodes;
public LockCycle(List<LockNode> nodes) {
this.nodes = new ArrayList<>(nodes);
}
// Getters
public List<LockNode> getNodes() { return Collections.unmodifiableList(nodes); }
public boolean involvesThread(long threadId) {
return nodes.stream().anyMatch(node -> node.getThreadId() == threadId);
}
@Override
public String toString() {
if (nodes.isEmpty()) return "Empty cycle";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < nodes.size(); i++) {
if (i > 0) sb.append(" -> ");
sb.append(nodes.get(i));
}
sb.append(" -> ").append(nodes.get(0)); // Complete the cycle
return sb.toString();
}
}
public class LockNode {
private final long threadId;
private final String threadName;
private final String lockId;
private final String lockType;
public LockNode(long threadId, String threadName, String lockId, String lockType) {
this.threadId = threadId;
this.threadName = threadName;
this.lockId = lockId;
this.lockType = lockType;
}
// Getters
public long getThreadId() { return threadId; }
public String getThreadName() { return threadName; }
public String getLockId() { return lockId; }
public String getLockType() { return lockType; }
@Override
public String toString() {
return String.format("Thread[%d:%s] holds Lock[%s:%s]",
threadId, threadName, lockId, lockType);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LockNode lockNode = (LockNode) o;
return threadId == lockNode.threadId &&
Objects.equals(lockId, lockNode.lockId);
}
@Override
public int hashCode() {
return Objects.hash(threadId, lockId);
}
}
2. Thread Dump Parser
package com.threadanalyzer.parser;
import com.threadanalyzer.model.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ThreadDumpParser {
// Regex patterns for different thread dump formats
private static final Pattern THREAD_HEADER_PATTERN = Pattern.compile(
"^\"([^\"]+)\"\\s+(?:#(\\d+)\\s+)?(daemon\\s+)?(prio=(\\d+)\\s+)?(os_prio=(\\d+)\\s+)?(tid=(0x[0-9a-f]+)\\s+)?(nid=(0x[0-9a-f]+)\\s+)?(\\w+)(?:\\s+\\[([^\\]]+)\\])?\\s*(?:\\(([^)]+)\\))?$"
);
private static final Pattern THREAD_STATE_PATTERN = Pattern.compile(
"^\\s+java\\.lang\\.Thread\\.State:\\s+(\\w+)(?:\\s+\\((.+)\\))?$"
);
private static final Pattern LOCK_INFO_PATTERN = Pattern.compile(
"^\\s+-\\s+(?:waiting\\s+on|locked|waiting\\s+to\\s+lock)\\s+<([0-9a-f]+)>\\s+\\(a\\s+([^)]+)\\)$"
);
private static final Pattern LOCK_OWNER_PATTERN = Pattern.compile(
"^\\s+-\\s+owned\\s+by\\s+<([0-9a-f]+)>\\s+\\(a\\s+([^)]+)\\)$"
);
private static final Pattern STACK_FRAME_PATTERN = Pattern.compile(
"^\\s+at\\s+([^(]+)\\(([^:)]+)(?::(\\d+))?\\)$"
);
private static final Pattern NATIVE_FRAME_PATTERN = Pattern.compile(
"^\\s+at\\s+([^(]+)\\(Native\\s+Method\\)$"
);
private static final Pattern UNKNOWN_FRAME_PATTERN = Pattern.compile(
"^\\s+at\\s+([^(]+)\\(Unknown\\s+Source\\)$"
);
private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(
"^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}$"
);
private static final Pattern FULL_THREAD_DUMP_PATTERN = Pattern.compile(
"^Full thread dump\\s+.*$"
);
public ThreadDump parseFromFile(Path filePath) throws IOException {
String content = Files.readString(filePath);
return parseFromString(content, filePath.toString());
}
public ThreadDump parseFromString(String content, String source) {
ThreadDump.Builder dumpBuilder = new ThreadDump.Builder().source(source);
String[] lines = content.split("\n");
ThreadInfo.Builder currentThreadBuilder = null;
List<StackFrame> currentStackTrace = new ArrayList<>();
boolean inThreadBlock = false;
boolean inStackTrace = false;
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (line.isEmpty()) {
// End of thread block
if (inThreadBlock && currentThreadBuilder != null) {
completeThreadBuilding(dumpBuilder, currentThreadBuilder, currentStackTrace);
currentThreadBuilder = null;
currentStackTrace.clear();
inThreadBlock = false;
inStackTrace = false;
}
continue;
}
// Check for thread header
Matcher headerMatcher = THREAD_HEADER_PATTERN.matcher(line);
if (headerMatcher.matches()) {
if (inThreadBlock && currentThreadBuilder != null) {
completeThreadBuilding(dumpBuilder, currentThreadBuilder, currentStackTrace);
}
currentThreadBuilder = parseThreadHeader(headerMatcher);
currentStackTrace.clear();
inThreadBlock = true;
inStackTrace = false;
continue;
}
// Check for thread state
if (inThreadBlock && !inStackTrace) {
Matcher stateMatcher = THREAD_STATE_PATTERN.matcher(line);
if (stateMatcher.matches()) {
parseThreadState(currentThreadBuilder, stateMatcher);
inStackTrace = true;
continue;
}
}
// Check for lock information
if (inThreadBlock) {
Matcher lockMatcher = LOCK_INFO_PATTERN.matcher(line);
if (lockMatcher.matches()) {
parseLockInfo(currentThreadBuilder, lockMatcher);
continue;
}
Matcher ownerMatcher = LOCK_OWNER_PATTERN.matcher(line);
if (ownerMatcher.matches()) {
parseLockOwner(currentThreadBuilder, ownerMatcher);
continue;
}
}
// Check for stack frame
if (inStackTrace && currentThreadBuilder != null) {
StackFrame frame = parseStackFrame(line);
if (frame != null) {
currentStackTrace.add(frame);
}
}
// Check for timestamp
if (TIMESTAMP_PATTERN.matcher(line).matches()) {
dumpBuilder.metadata("timestamp", line);
}
// Check for full thread dump header
if (FULL_THREAD_DUMP_PATTERN.matcher(line).matches()) {
dumpBuilder.metadata("fullThreadDumpHeader", line);
}
}
// Handle last thread if file doesn't end with empty line
if (inThreadBlock && currentThreadBuilder != null) {
completeThreadBuilding(dumpBuilder, currentThreadBuilder, currentStackTrace);
}
return dumpBuilder.build();
}
private ThreadInfo.Builder parseThreadHeader(Matcher matcher) {
ThreadInfo.Builder builder = new ThreadInfo.Builder();
// Thread name (group 1)
String threadName = matcher.group(1);
builder.threadName(threadName != null ? threadName.trim() : "Unknown");
// Thread ID (group 2) - optional
String threadIdStr = matcher.group(2);
if (threadIdStr != null) {
try {
builder.threadId(Long.parseLong(threadIdStr));
} catch (NumberFormatException e) {
// Try to extract from thread name
extractThreadIdFromName(builder, threadName);
}
} else {
extractThreadIdFromName(builder, threadName);
}
// Daemon flag (group 3)
boolean daemon = matcher.group(3) != null;
builder.daemon(daemon);
// Priority (group 5)
String prioStr = matcher.group(5);
if (prioStr != null) {
try {
builder.priority(Integer.parseInt(prioStr));
} catch (NumberFormatException e) {
builder.priority(Thread.NORM_PRIORITY);
}
} else {
builder.priority(Thread.NORM_PRIORITY);
}
// Native thread ID (group 10) - useful for correlation with OS threads
String nidStr = matcher.group(10);
if (nidStr != null) {
builder.additionalInfo("nid", nidStr);
}
return builder;
}
private void extractThreadIdFromName(ThreadInfo.Builder builder, String threadName) {
// Try to extract thread ID from name like "pool-1-thread-1"
Pattern idPattern = Pattern.compile(".*-(\\d+)$");
Matcher idMatcher = idPattern.matcher(threadName);
if (idMatcher.matches()) {
try {
builder.threadId(Long.parseLong(idMatcher.group(1)));
} catch (NumberFormatException e) {
builder.threadId(Thread.currentThread().getId()); // fallback
}
} else {
builder.threadId(Thread.currentThread().getId()); // fallback
}
}
private void parseThreadState(ThreadInfo.Builder builder, Matcher matcher) {
String stateStr = matcher.group(1);
ThreadState state = ThreadState.fromString(stateStr);
if (state != null) {
builder.state(state);
}
// Additional state info (e.g., WAITING (parking))
String stateInfo = matcher.group(2);
if (stateInfo != null) {
builder.additionalInfo("stateInfo", stateInfo);
}
}
private void parseLockInfo(ThreadInfo.Builder builder, Matcher matcher) {
String lockId = matcher.group(1);
String lockType = matcher.group(2);
if (lockId != null && lockType != null) {
builder.lockInfo(lockId);
builder.additionalInfo("lockType", lockType);
}
}
private void parseLockOwner(ThreadInfo.Builder builder, Matcher matcher) {
String ownerId = matcher.group(1);
String ownerType = matcher.group(2);
if (ownerId != null) {
builder.lockOwnerId(ownerId);
if (ownerType != null) {
builder.lockOwnerName(ownerType);
}
}
}
private StackFrame parseStackFrame(String line) {
// Try native method frame first
Matcher nativeMatcher = NATIVE_FRAME_PATTERN.matcher(line);
if (nativeMatcher.matches()) {
String methodName = nativeMatcher.group(1);
String[] parts = methodName.split("\\.");
String className = parts.length > 1 ?
String.join(".", Arrays.copyOf(parts, parts.length - 1)) : "Unknown";
String method = parts.length > 0 ? parts[parts.length - 1] : "unknown";
return new StackFrame(className, method, null, -1, true);
}
// Try regular stack frame with line number
Matcher frameMatcher = STACK_FRAME_PATTERN.matcher(line);
if (frameMatcher.matches()) {
String methodName = frameMatcher.group(1);
String fileName = frameMatcher.group(2);
String lineNumberStr = frameMatcher.group(3);
String[] parts = methodName.split("\\.");
String className = parts.length > 1 ?
String.join(".", Arrays.copyOf(parts, parts.length - 1)) : "Unknown";
String method = parts.length > 0 ? parts[parts.length - 1] : "unknown";
int lineNumber = -1;
if (lineNumberStr != null) {
try {
lineNumber = Integer.parseInt(lineNumberStr);
} catch (NumberFormatException e) {
// Keep -1
}
}
return new StackFrame(className, method, fileName, lineNumber, false);
}
// Try unknown source frame
Matcher unknownMatcher = UNKNOWN_FRAME_PATTERN.matcher(line);
if (unknownMatcher.matches()) {
String methodName = unknownMatcher.group(1);
String[] parts = methodName.split("\\.");
String className = parts.length > 1 ?
String.join(".", Arrays.copyOf(parts, parts.length - 1)) : "Unknown";
String method = parts.length > 0 ? parts[parts.length - 1] : "unknown";
return new StackFrame(className, method, null, -1, false);
}
return null;
}
private void completeThreadBuilding(ThreadDump.Builder dumpBuilder,
ThreadInfo.Builder threadBuilder,
List<StackFrame> stackTrace) {
threadBuilder.addStackFrames(stackTrace);
ThreadInfo thread = threadBuilder.build();
dumpBuilder.addThread(thread);
}
// Batch parsing for multiple files
public List<ThreadDump> parseMultipleFiles(List<Path> filePaths) {
return filePaths.parallelStream()
.map(path -> {
try {
return parseFromFile(path);
} catch (IOException e) {
System.err.println("Failed to parse file: " + path + " - " + e.getMessage());
return null;
}
})
.filter(Objects::nonNull)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
}
3. Deadlock Detector
package com.threadanalyzer.detector;
import com.threadanalyzer.model.*;
import java.util.*;
import java.util.stream.Collectors;
public class DeadlockDetector {
public List<Deadlock> detectDeadlocks(ThreadDump dump) {
List<Deadlock> deadlocks = new ArrayList<>();
// Build lock dependency graph
LockDependencyGraph graph = buildLockDependencyGraph(dump);
// Find cycles in the graph
List<List<LockNode>> cycles = findCycles(graph);
// Convert cycles to Deadlock objects
for (List<LockNode> cycle : cycles) {
if (cycle.size() >= 2) { // Meaningful deadlock requires at least 2 threads
List<ThreadInfo> involvedThreads = getThreadsFromCycle(dump, cycle);
List<LockCycle> lockCycles = Collections.singletonList(new LockCycle(cycle));
deadlocks.add(new Deadlock(involvedThreads, lockCycles));
}
}
return deadlocks;
}
private LockDependencyGraph buildLockDependencyGraph(ThreadDump dump) {
LockDependencyGraph graph = new LockDependencyGraph();
// First pass: collect all threads that are waiting for locks
Map<String, LockWaiter> lockWaiters = new HashMap<>();
for (ThreadInfo thread : dump.getThreads()) {
if (thread.isBlocked() || thread.isWaiting()) {
String lockInfo = thread.getLockInfo();
if (lockInfo != null && !lockInfo.isEmpty()) {
lockWaiters.put(thread.getLockInfo(),
new LockWaiter(thread.getThreadId(), thread.getThreadName(), lockInfo));
}
}
}
// Second pass: find threads that own locks that other threads are waiting for
for (ThreadInfo thread : dump.getThreads()) {
String lockOwnerId = thread.getLockOwnerId();
if (lockOwnerId != null && !lockOwnerId.isEmpty()) {
LockWaiter waiter = lockWaiters.get(lockOwnerId);
if (waiter != null) {
// This thread owns a lock that another thread is waiting for
graph.addDependency(
new LockNode(thread.getThreadId(), thread.getThreadName(),
thread.getLockInfo(), getLockType(thread)),
new LockNode(waiter.getThreadId(), waiter.getThreadName(),
waiter.getLockId(), "unknown")
);
}
}
}
return graph;
}
private String getLockType(ThreadInfo thread) {
return thread.getAdditionalInfo().getOrDefault("lockType", "unknown");
}
private List<List<LockNode>> findCycles(LockDependencyGraph graph) {
List<List<LockNode>> cycles = new ArrayList<>();
Set<LockNode> visited = new HashSet<>();
Set<LockNode> recursionStack = new HashSet<>();
Map<LockNode, LockNode> parent = new HashMap<>();
for (LockNode node : graph.getAllNodes()) {
if (!visited.contains(node)) {
findCyclesDFS(graph, node, visited, recursionStack, parent, cycles);
}
}
return cycles;
}
private void findCyclesDFS(LockDependencyGraph graph, LockNode current,
Set<LockNode> visited, Set<LockNode> recursionStack,
Map<LockNode, LockNode> parent, List<List<LockNode>> cycles) {
visited.add(current);
recursionStack.add(current);
for (LockNode neighbor : graph.getDependencies(current)) {
if (!visited.contains(neighbor)) {
parent.put(neighbor, current);
findCyclesDFS(graph, neighbor, visited, recursionStack, parent, cycles);
} else if (recursionStack.contains(neighbor)) {
// Cycle detected
List<LockNode> cycle = reconstructCycle(current, neighbor, parent);
if (!isDuplicateCycle(cycle, cycles)) {
cycles.add(cycle);
}
}
}
recursionStack.remove(current);
}
private List<LockNode> reconstructCycle(LockNode current, LockNode cycleStart,
Map<LockNode, LockNode> parent) {
List<LockNode> cycle = new ArrayList<>();
LockNode node = current;
// Traverse back to the cycle start
while (node != cycleStart) {
cycle.add(node);
node = parent.get(node);
}
cycle.add(cycleStart);
Collections.reverse(cycle);
return cycle;
}
private boolean isDuplicateCycle(List<LockNode> newCycle, List<List<LockNode>> existingCycles) {
if (newCycle.isEmpty()) return false;
for (List<LockNode> existingCycle : existingCycles) {
if (existingCycle.size() == newCycle.size() &&
new HashSet<>(existingCycle).equals(new HashSet<>(newCycle))) {
return true;
}
}
return false;
}
private List<ThreadInfo> getThreadsFromCycle(ThreadDump dump, List<LockNode> cycle) {
return cycle.stream()
.map(node -> dump.findThreadById(node.getThreadId()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
// Helper class for lock dependency tracking
private static class LockWaiter {
private final long threadId;
private final String threadName;
private final String lockId;
public LockWaiter(long threadId, String threadName, String lockId) {
this.threadId = threadId;
this.threadName = threadName;
this.lockId = lockId;
}
public long getThreadId() { return threadId; }
public String getThreadName() { return threadName; }
public String getLockId() { return lockId; }
}
}
class LockDependencyGraph {
private final Map<LockNode, Set<LockNode>> adjacencyList = new HashMap<>();
public void addDependency(LockNode from, LockNode to) {
adjacencyList.computeIfAbsent(from, k -> new HashSet<>()).add(to);
}
public Set<LockNode> getDependencies(LockNode node) {
return adjacencyList.getOrDefault(node, Collections.emptySet());
}
public Set<LockNode> getAllNodes() {
return adjacencyList.keySet();
}
public boolean hasDependency(LockNode from, LockNode to) {
return adjacencyList.getOrDefault(from, Collections.emptySet()).contains(to);
}
}
4. Thread Dump Analyzer
package com.threadanalyzer.analyzer;
import com.threadanalyzer.model.*;
import java.util.*;
import java.util.stream.Collectors;
public class ThreadDumpAnalyzer {
private final ThreadDump dump;
private final AnalysisResult result;
public ThreadDumpAnalyzer(ThreadDump dump) {
this.dump = dump;
this.result = new AnalysisResult(dump);
}
public AnalysisResult analyze() {
analyzeThreadStates();
analyzeThreadGroups();
analyzeStackTraces();
analyzeLockContention();
analyzeCommonPatterns();
calculateStatistics();
return result;
}
private void analyzeThreadStates() {
Map<ThreadState, Long> stateDistribution = dump.getThreadStateDistribution();
result.setStateDistribution(stateDistribution);
// Identify potential issues based on thread states
long blockedCount = stateDistribution.getOrDefault(ThreadState.BLOCKED, 0L);
long waitingCount = stateDistribution.getOrDefault(ThreadState.WAITING, 0L) +
stateDistribution.getOrDefault(ThreadState.TIMED_WAITING, 0L);
if (blockedCount > dump.getThreadCount() * 0.1) { // More than 10% blocked
result.addIssue("HIGH_BLOCKED_THREADS",
String.format("High number of blocked threads: %d (%.1f%%)",
blockedCount, (blockedCount * 100.0 / dump.getThreadCount())));
}
if (waitingCount > dump.getThreadCount() * 0.3) { // More than 30% waiting
result.addIssue("HIGH_WAITING_THREADS",
String.format("High number of waiting threads: %d (%.1f%%)",
waitingCount, (waitingCount * 100.0 / dump.getThreadCount())));
}
}
private void analyzeThreadGroups() {
Map<String, List<ThreadInfo>> threadsByGroup = groupThreadsByName();
result.setThreadGroups(threadsByGroup);
// Analyze thread pools
analyzeThreadPools(threadsByGroup);
// Identify unusually large thread groups
threadsByGroup.entrySet().stream()
.filter(entry -> entry.getValue().size() > 50) // Arbitrary threshold
.forEach(entry -> {
result.addIssue("LARGE_THREAD_GROUP",
String.format("Large thread group '%s': %d threads",
entry.getKey(), entry.getValue().size()));
});
}
private Map<String, List<ThreadInfo>> groupThreadsByName() {
return dump.getThreads().stream()
.collect(Collectors.groupingBy(thread -> {
String name = thread.getThreadName();
// Extract group name (e.g., "pool-1-thread-1" -> "pool-1")
if (name.contains("-thread-")) {
return name.substring(0, name.lastIndexOf("-thread-"));
} else if (name.contains("-")) {
return name.substring(0, name.lastIndexOf('-'));
}
return name;
}));
}
private void analyzeThreadPools(Map<String, List<ThreadInfo>> threadGroups) {
threadGroups.entrySet().stream()
.filter(entry -> entry.getKey().startsWith("pool-") ||
entry.getKey().contains("ThreadPool"))
.forEach(entry -> {
String poolName = entry.getKey();
List<ThreadInfo> poolThreads = entry.getValue();
long idleThreads = poolThreads.stream()
.filter(t -> t.getState() == ThreadState.WAITING ||
t.getState() == ThreadState.TIMED_WAITING)
.count();
long activeThreads = poolThreads.size() - idleThreads;
result.addThreadPoolInfo(poolName, poolThreads.size(), activeThreads, idleThreads);
if (idleThreads == 0 && activeThreads > 0) {
result.addIssue("THREAD_POOL_BUSY",
String.format("Thread pool '%s' has no idle threads (%d active)",
poolName, activeThreads));
}
});
}
private void analyzeStackTraces() {
// Analyze common stack trace patterns
Map<String, Long> methodFrequency = new HashMap<>();
Map<String, Long> classFrequency = new HashMap<>();
for (ThreadInfo thread : dump.getThreads()) {
for (StackFrame frame : thread.getStackTrace()) {
// Count method frequency
String methodKey = frame.getFullMethodName();
methodFrequency.merge(methodKey, 1L, Long::sum);
// Count class frequency
classFrequency.merge(frame.getClassName(), 1L, Long::sum);
}
}
result.setMethodFrequency(methodFrequency);
result.setClassFrequency(classFrequency);
// Identify hot methods (methods that appear in many threads)
methodFrequency.entrySet().stream()
.filter(entry -> entry.getValue() > dump.getThreadCount() * 0.3)
.forEach(entry -> {
result.addIssue("HOT_METHOD",
String.format("Method '%s' appears in %d threads",
entry.getKey(), entry.getValue()));
});
}
private void analyzeLockContention() {
// Analyze threads waiting for the same lock
Map<String, List<ThreadInfo>> threadsByLock = new HashMap<>();
for (ThreadInfo thread : dump.getThreads()) {
String lockInfo = thread.getLockInfo();
if (lockInfo != null && !lockInfo.isEmpty()) {
threadsByLock.computeIfAbsent(lockInfo, k -> new ArrayList<>()).add(thread);
}
}
// Identify locks with high contention
threadsByLock.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> {
String lockId = entry.getKey();
int waitingCount = entry.getValue().size();
result.addLockContention(lockId, waitingCount);
if (waitingCount > 5) { // Arbitrary threshold
result.addIssue("HIGH_LOCK_CONTENTION",
String.format("Lock %s has %d waiting threads",
lockId, waitingCount));
}
});
}
private void analyzeCommonPatterns() {
// Detect common problematic patterns
// Pattern 1: Many threads in IO operations
long ioThreads = dump.getThreads().stream()
.filter(t -> containsIOOperation(t))
.count();
if (ioThreads > dump.getThreadCount() * 0.2) {
result.addIssue("HIGH_IO_OPERATIONS",
String.format("Many threads (%d) performing IO operations", ioThreads));
}
// Pattern 2: Threads stuck in synchronized blocks
long synchronizedThreads = dump.getThreads().stream()
.filter(t -> isInSynchronizedBlock(t))
.count();
if (synchronizedThreads > 10) {
result.addIssue("MANY_SYNCHRONIZED_THREADS",
String.format("%d threads in synchronized blocks", synchronizedThreads));
}
// Pattern 3: Threads waiting on object monitors
long monitorWaitThreads = dump.getThreads().stream()
.filter(t -> isWaitingOnMonitor(t))
.count();
if (monitorWaitThreads > 5) {
result.addIssue("MONITOR_WAIT_THREADS",
String.format("%d threads waiting on object monitors", monitorWaitThreads));
}
}
private boolean containsIOOperation(ThreadInfo thread) {
return thread.getStackTrace().stream()
.anyMatch(frame ->
frame.getClassName().contains("java.io") ||
frame.getClassName().contains("java.nio") ||
frame.getClassName().contains("java.net") ||
frame.getMethodName().toLowerCase().contains("read") ||
frame.getMethodName().toLowerCase().contains("write") ||
frame.getMethodName().toLowerCase().contains("accept") ||
frame.getMethodName().toLowerCase().contains("connect"));
}
private boolean isInSynchronizedBlock(ThreadInfo thread) {
return thread.getStackTrace().stream()
.anyMatch(frame ->
frame.getMethodName().contains("synchronized") ||
(frame.getClassName().equals("java.lang.Object") &&
frame.getMethodName().equals("wait")));
}
private boolean isWaitingOnMonitor(ThreadInfo thread) {
return thread.getState() == ThreadState.WAITING &&
thread.getStackTrace().stream()
.anyMatch(frame ->
frame.getClassName().equals("java.lang.Object") &&
frame.getMethodName().equals("wait"));
}
private void calculateStatistics() {
// Basic statistics
result.setTotalThreads(dump.getThreadCount());
result.setDaemonThreads(dump.getDaemonThreadCount());
result.setNonDaemonThreads(dump.getNonDaemonThreadCount());
// Average stack depth
double avgStackDepth = dump.getThreads().stream()
.mapToInt(t -> t.getStackTrace().size())
.average()
.orElse(0.0);
result.setAverageStackDepth(avgStackDepth);
// Thread priority distribution
Map<Integer, Long> priorityDistribution = dump.getThreads().stream()
.collect(Collectors.groupingBy(
ThreadInfo::getPriority,
Collectors.counting()));
result.setPriorityDistribution(priorityDistribution);
}
}
public class AnalysisResult {
private final ThreadDump dump;
private final Map<ThreadState, Long> stateDistribution = new EnumMap<>(ThreadState.class);
private final Map<String, List<ThreadInfo>> threadGroups = new HashMap<>();
private final Map<String, Long> methodFrequency = new HashMap<>();
private final Map<String, Long> classFrequency = new HashMap<>();
private final List<String> issues = new ArrayList<>();
private final Map<String, Integer> lockContention = new HashMap<>();
private final Map<String, ThreadPoolInfo> threadPoolInfo = new HashMap<>();
private final Map<Integer, Long> priorityDistribution = new HashMap<>();
// Statistics
private int totalThreads;
private long daemonThreads;
private long nonDaemonThreads;
private double averageStackDepth;
public AnalysisResult(ThreadDump dump) {
this.dump = dump;
}
// Getters and setters
public ThreadDump getDump() { return dump; }
public Map<ThreadState, Long> getStateDistribution() { return stateDistribution; }
public void setStateDistribution(Map<ThreadState, Long> distribution) {
this.stateDistribution.putAll(distribution);
}
public Map<String, List<ThreadInfo>> getThreadGroups() { return threadGroups; }
public void setThreadGroups(Map<String, List<ThreadInfo>> groups) {
this.threadGroups.putAll(groups);
}
public Map<String, Long> getMethodFrequency() { return methodFrequency; }
public void setMethodFrequency(Map<String, Long> frequency) {
this.methodFrequency.putAll(frequency);
}
public Map<String, Long> getClassFrequency() { return classFrequency; }
public void setClassFrequency(Map<String, Long> frequency) {
this.classFrequency.putAll(frequency);
}
public List<String> getIssues() { return Collections.unmodifiableList(issues); }
public void addIssue(String type, String description) {
issues.add(type + ": " + description);
}
public Map<String, Integer> getLockContention() { return lockContention; }
public void addLockContention(String lockId, int waitingCount) {
lockContention.put(lockId, waitingCount);
}
public Map<String, ThreadPoolInfo> getThreadPoolInfo() { return threadPoolInfo; }
public void addThreadPoolInfo(String poolName, int totalThreads, long activeThreads, long idleThreads) {
threadPoolInfo.put(poolName, new ThreadPoolInfo(totalThreads, activeThreads, idleThreads));
}
public int getTotalThreads() { return totalThreads; }
public void setTotalThreads(int totalThreads) { this.totalThreads = totalThreads; }
public long getDaemonThreads() { return daemonThreads; }
public void setDaemonThreads(long daemonThreads) { this.daemonThreads = daemonThreads; }
public long getNonDaemonThreads() { return nonDaemonThreads; }
public void setNonDaemonThreads(long nonDaemonThreads) { this.nonDaemonThreads = nonDaemonThreads; }
public double getAverageStackDepth() { return averageStackDepth; }
public void setAverageStackDepth(double averageStackDepth) { this.averageStackDepth = averageStackDepth; }
public Map<Integer, Long> getPriorityDistribution() { return priorityDistribution; }
public void setPriorityDistribution(Map<Integer, Long> distribution) {
this.priorityDistribution.putAll(distribution);
}
public boolean hasIssues() {
return !issues.isEmpty();
}
public int getIssueCount() {
return issues.size();
}
public List<String> getIssuesBySeverity() {
// Simple severity classification
return issues.stream()
.sorted((a, b) -> {
int severityA = getSeverity(a);
int severityB = getSeverity(b);
return Integer.compare(severityB, severityA); // Descending
})
.collect(Collectors.toList());
}
private int getSeverity(String issue) {
if (issue.contains("DEADLOCK") || issue.contains("HIGH_BLOCKED")) {
return 3; // High severity
} else if (issue.contains("HIGH_WAITING") || issue.contains("HIGH_CONTENTION")) {
return 2; // Medium severity
} else {
return 1; // Low severity
}
}
}
class ThreadPoolInfo {
private final int totalThreads;
private final long activeThreads;
private final long idleThreads;
public ThreadPoolInfo(int totalThreads, long activeThreads, long idleThreads) {
this.totalThreads = totalThreads;
this.activeThreads = activeThreads;
this.idleThreads = idleThreads;
}
// Getters
public int getTotalThreads() { return totalThreads; }
public long getActiveThreads() { return activeThreads; }
public long getIdleThreads() { return idleThreads; }
public double getUtilization() {
return totalThreads > 0 ? (double) activeThreads / totalThreads : 0.0;
}
}
5. HTML Report Generator
package com.threadanalyzer.reporter;
import com.threadanalyzer.model.*;
import com.threadanalyzer.analyzer.AnalysisResult;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class HTMLReportGenerator {
public void generateReport(AnalysisResult result, Path outputDir) throws IOException {
String htmlContent = generateHTMLContent(result);
Path outputFile = outputDir.resolve("thread-dump-analysis.html");
Files.writeString(outputFile, htmlContent);
System.out.println("HTML report generated: " + outputFile.toAbsolutePath());
}
private String generateHTMLContent(AnalysisResult result) {
StringBuilder html = new StringBuilder();
html.append("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thread Dump Analysis Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.section { margin-bottom: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.section-title { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; margin-top: 0; }
.issue { padding: 10px; margin: 5px 0; border-radius: 4px; }
.issue-high { background: #ffebee; border-left: 4px solid #f44336; }
.issue-medium { background: #fff3e0; border-left: 4px solid #ff9800; }
.issue-low { background: #e8f5e8; border-left: 4px solid #4caf50; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
.stat-card { background: #f8f9fa; padding: 15px; border-radius: 5px; text-align: center; }
.stat-value { font-size: 24px; font-weight: bold; color: #2c3e50; }
.stat-label { color: #7f8c8d; font-size: 14px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #34495e; color: white; }
tr:hover { background-color: #f5f5f5; }
.chart-container { height: 300px; margin: 20px 0; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
""");
// Header
html.append("""
<div class="header">
<h1>Thread Dump Analysis Report</h1>
<p>Generated on: """ + java.time.LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + """</p>
<p>Source: """ + escapeHtml(result.getDump().getSource()) + """</p>
</div>
""");
// Executive Summary
html.append(generateExecutiveSummary(result));
// Statistics
html.append(generateStatisticsSection(result));
// Issues
html.append(generateIssuesSection(result));
// Thread State Distribution
html.append(generateThreadStateSection(result));
// Thread Pools
html.append(generateThreadPoolSection(result));
// Lock Analysis
html.append(generateLockAnalysisSection(result));
// Method Analysis
html.append(generateMethodAnalysisSection(result));
html.append("""
</div>
<script>
""");
// JavaScript for charts
html.append(generateChartsJavaScript(result));
html.append("""
</script>
</body>
</html>
""");
return html.toString();
}
private String generateExecutiveSummary(AnalysisResult result) {
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Executive Summary</h2>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-value">""" + result.getTotalThreads() + """</div>
<div class="stat-label">Total Threads</div>
</div>
<div class="stat-card">
<div class="stat-value">""" + result.getDaemonThreads() + """</div>
<div class="stat-label">Daemon Threads</div>
</div>
<div class="stat-card">
<div class="stat-value">""" + result.getNonDaemonThreads() + """</div>
<div class="stat-label">Non-Daemon Threads</div>
</div>
<div class="stat-card">
<div class="stat-value">""" + result.getIssueCount() + """</div>
<div class="stat-label">Issues Found</div>
</div>
</div>
""");
if (result.hasIssues()) {
html.append("""
<div style="margin-top: 20px;">
<h3>Key Findings:</h3>
<ul>
""");
List<String> topIssues = result.getIssuesBySeverity().stream()
.limit(5)
.collect(Collectors.toList());
for (String issue : topIssues) {
html.append("<li>").append(escapeHtml(issue)).append("</li>");
}
html.append("""
</ul>
</div>
""");
} else {
html.append("""
<div style="margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 5px;">
<strong>No critical issues found.</strong> The thread dump appears healthy.
</div>
""");
}
html.append("</div>");
return html.toString();
}
private String generateStatisticsSection(AnalysisResult result) {
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Statistics</h2>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-value">""" + String.format("%.1f", result.getAverageStackDepth()) + """</div>
<div class="stat-label">Average Stack Depth</div>
</div>
""");
// Add state distribution badges
Map<ThreadState, Long> stateDist = result.getStateDistribution();
for (Map.Entry<ThreadState, Long> entry : stateDist.entrySet()) {
if (entry.getValue() > 0) {
String badgeClass = getBadgeClassForState(entry.getKey());
html.append("""
<div class="stat-card">
<div class="stat-value">""" + entry.getValue() + """</div>
<div class="stat-label">
<span class="badge """ + badgeClass + """">""" + entry.getKey() + """</span>
</div>
</div>
""");
}
}
html.append("</div></div>");
return html.toString();
}
private String generateIssuesSection(AnalysisResult result) {
if (!result.hasIssues()) {
return "";
}
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Detected Issues</h2>
""");
for (String issue : result.getIssuesBySeverity()) {
String issueClass = getIssueClass(issue);
html.append("""
<div class="issue """ + issueClass + """">
""" + escapeHtml(issue) + """
</div>
""");
}
html.append("</div>");
return html.toString();
}
private String generateThreadStateSection(AnalysisResult result) {
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Thread State Distribution</h2>
<div class="chart-container">
<canvas id="stateChart"></canvas>
</div>
<table>
<thead>
<tr>
<th>State</th>
<th>Count</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
""");
Map<ThreadState, Long> stateDist = result.getStateDistribution();
int totalThreads = result.getTotalThreads();
for (Map.Entry<ThreadState, Long> entry : stateDist.entrySet()) {
double percentage = totalThreads > 0 ? (entry.getValue() * 100.0 / totalThreads) : 0;
html.append("""
<tr>
<td>""" + entry.getKey() + """</td>
<td>""" + entry.getValue() + """</td>
<td>""" + String.format("%.1f%%", percentage) + """</td>
</tr>
""");
}
html.append("""
</tbody>
</table>
</div>
""");
return html.toString();
}
private String generateThreadPoolSection(AnalysisResult result) {
if (result.getThreadPoolInfo().isEmpty()) {
return "";
}
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Thread Pool Analysis</h2>
<table>
<thead>
<tr>
<th>Pool Name</th>
<th>Total Threads</th>
<th>Active</th>
<th>Idle</th>
<th>Utilization</th>
</tr>
</thead>
<tbody>
""");
for (Map.Entry<String, ThreadPoolInfo> entry : result.getThreadPoolInfo().entrySet()) {
ThreadPoolInfo info = entry.getValue();
double utilization = info.getUtilization() * 100;
String utilizationClass = utilization > 80 ? "badge-danger" :
utilization > 50 ? "badge-warning" : "badge-success";
html.append("""
<tr>
<td>""" + escapeHtml(entry.getKey()) + """</td>
<td>""" + info.getTotalThreads() + """</td>
<td>""" + info.getActiveThreads() + """</td>
<td>""" + info.getIdleThreads() + """</td>
<td><span class="badge """ + utilizationClass + """">""" +
String.format("%.1f%%", utilization) + """</span></td>
</tr>
""");
}
html.append("""
</tbody>
</table>
</div>
""");
return html.toString();
}
private String generateLockAnalysisSection(AnalysisResult result) {
if (result.getLockContention().isEmpty()) {
return "";
}
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Lock Contention</h2>
<table>
<thead>
<tr>
<th>Lock ID</th>
<th>Waiting Threads</th>
<th>Severity</th>
</tr>
</thead>
<tbody>
""");
for (Map.Entry<String, Integer> entry : result.getLockContention().entrySet()) {
int waitingCount = entry.getValue();
String severity = waitingCount > 10 ? "High" : waitingCount > 5 ? "Medium" : "Low";
String severityClass = waitingCount > 10 ? "badge-danger" :
waitingCount > 5 ? "badge-warning" : "badge-success";
html.append("""
<tr>
<td>""" + escapeHtml(entry.getKey()) + """</td>
<td>""" + waitingCount + """</td>
<td><span class="badge """ + severityClass + """">""" + severity + """</span></td>
</tr>
""");
}
html.append("""
</tbody>
</table>
</div>
""");
return html.toString();
}
private String generateMethodAnalysisSection(AnalysisResult result) {
// Show top 10 most frequent methods
List<Map.Entry<String, Long>> topMethods = result.getMethodFrequency().entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.collect(Collectors.toList());
if (topMethods.isEmpty()) {
return "";
}
StringBuilder html = new StringBuilder();
html.append("""
<div class="section">
<h2 class="section-title">Most Frequent Methods (Top 10)</h2>
<table>
<thead>
<tr>
<th>Method</th>
<th>Frequency</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
""");
int totalStackFrames = result.getMethodFrequency().values().stream()
.mapToInt(Long::intValue)
.sum();
for (Map.Entry<String, Long> entry : topMethods) {
double percentage = totalStackFrames > 0 ? (entry.getValue() * 100.0 / totalStackFrames) : 0;
html.append("""
<tr>
<td>""" + escapeHtml(entry.getKey()) + """</td>
<td>""" + entry.getValue() + """</td>
<td>""" + String.format("%.1f%%", percentage) + """</td>
</tr>
""");
}
html.append("""
</tbody>
</table>
</div>
""");
return html.toString();
}
private String generateChartsJavaScript(AnalysisResult result) {
StringBuilder js = new StringBuilder();
// Thread state chart
js.append("""
var stateCtx = document.getElementById('stateChart').getContext('2d');
var stateChart = new Chart(stateCtx, {
type: 'doughnut',
data: {
labels: [""" +
result.getStateDistribution().keySet().stream()
.map(state -> "'" + state + "'")
.collect(Collectors.joining(", ")) + """],
datasets: [{
data: [""" +
result.getStateDistribution().values().stream()
.map(String::valueOf)
.collect(Collectors.joining(", ")) + """],
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
},
title: {
display: true,
text: 'Thread States'
}
}
}
});
""");
return js.toString();
}
private String getBadgeClassForState(ThreadState state) {
switch (state) {
case BLOCKED: return "badge-danger";
case WAITING: case TIMED_WAITING: return "badge-warning";
case RUNNABLE: return "badge-success";
default: return "badge-success";
}
}
private String getIssueClass(String issue) {
if (issue.contains("HIGH") || issue.contains("DEADLOCK")) {
return "issue-high";
} else if (issue.contains("MEDIUM") || issue.contains("MANY")) {
return "issue-medium";
} else {
return "issue-low";
}
}
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
6. CLI Interface
package com.threadanalyzer.cli;
import com.threadanalyzer.analyzer.AnalysisResult;
import com.threadanalyzer.analyzer.ThreadDumpAnalyzer;
import com.threadanalyzer.detector.DeadlockDetector;
import com.threadanalyzer.model.ThreadDump;
import com.threadanalyzer.parser.ThreadDumpParser;
import com.threadanalyzer.reporter.HTMLReportGenerator;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;
@Command(name = "threadanalyzer",
mixinStandardHelpOptions = true,
version = "Thread Dump Analyzer 1.0",
description = "Analyzes Java thread dumps and generates reports")
public class CLIRunner implements Callable<Integer> {
@Parameters(index = "0", description = "Thread dump file or directory")
private File input;
@Option(names = {"-o", "--output"},
description = "Output directory for reports (default: ./reports)")
private File outputDir = new File("./reports");
@Option(names = {"--format"},
description = "Output format: ${COMPLETION-CANDIDATES} (default: HTML)")
private OutputFormat format = OutputFormat.HTML;
@Option(names = {"--detect-deadlocks"},
description = "Enable deadlock detection")
private boolean detectDeadlocks = true;
@Option(names = {"--generate-charts"},
description = "Generate charts in reports")
private boolean generateCharts = true;
@Option(names = {"--verbose"},
description = "Enable verbose output")
private boolean verbose = false;
private final ThreadDumpParser parser = new ThreadDumpParser();
private final DeadlockDetector deadlockDetector = new DeadlockDetector();
public enum OutputFormat {
HTML, TEXT, JSON
}
@Override
public Integer call() throws Exception {
if (!input.exists()) {
System.err.println("Error: Input file/directory does not exist: " + input.getAbsolutePath());
return 1;
}
if (!outputDir.exists()) {
outputDir.mkdirs();
}
try {
List<ThreadDump> dumps = loadThreadDumps();
if (dumps.isEmpty()) {
System.err.println("Error: No valid thread dumps found in: " + input.getAbsolutePath());
return 1;
}
System.out.println("Loaded " + dumps.size() + " thread dump(s)");
for (ThreadDump dump : dumps) {
System.out.println("\nAnalyzing: " + dump.getSource());
analyzeDump(dump);
}
System.out.println("\nAnalysis completed. Reports saved to: " + outputDir.getAbsolutePath());
return 0;
} catch (Exception e) {
System.err.println("Error during analysis: " + e.getMessage());
if (verbose) {
e.printStackTrace();
}
return 1;
}
}
private List<ThreadDump> loadThreadDumps() throws Exception {
if (input.isFile()) {
ThreadDump dump = parser.parseFromFile(input.toPath());
return List.of(dump);
} else if (input.isDirectory()) {
// Load all .txt, .log, .threaddump files from directory
File[] files = input.listFiles((dir, name) ->
name.toLowerCase().endsWith(".txt") ||
name.toLowerCase().endsWith(".log") ||
name.toLowerCase().endsWith(".threaddump") ||
name.toLowerCase().contains("threaddump"));
if (files == null || files.length == 0) {
return List.of();
}
return parser.parseMultipleFiles(
List.of(files).stream()
.map(File::toPath)
.collect(java.util.stream.Collectors.toList())
);
}
return List.of();
}
private void analyzeDump(ThreadDump dump) throws Exception {
// Perform analysis
ThreadDumpAnalyzer analyzer = new ThreadDumpAnalyzer(dump);
AnalysisResult result = analyzer.analyze();
// Detect deadlocks if enabled
if (detectDeadlocks) {
var deadlocks = deadlockDetector.detectDeadlocks(dump);
if (!deadlocks.isEmpty()) {
System.out.println("⚠️ Found " + deadlocks.size() + " potential deadlock(s)");
for (var deadlock : deadlocks) {
System.out.println(" - " + deadlock.getDescription());
}
}
}
// Generate report based on format
switch (format) {
case HTML:
generateHTMLReport(dump, result);
break;
case TEXT:
generateTextReport(dump, result);
break;
case JSON:
generateJSONReport(dump, result);
break;
}
// Print summary to console
printConsoleSummary(result);
}
private void generateHTMLReport(ThreadDump dump, AnalysisResult result) throws Exception {
HTMLReportGenerator reporter = new HTMLReportGenerator();
String baseName = getBaseName(dump.getSource());
Path reportDir = outputDir.toPath().resolve(baseName);
reporter.generateReport(result, reportDir);
}
private void generateTextReport(ThreadDump dump, AnalysisResult result) {
// Implementation for text report
String baseName = getBaseName(dump.getSource());
Path reportFile = outputDir.toPath().resolve(baseName + "-report.txt");
try (var writer = new java.io.PrintWriter(reportFile.toFile())) {
writer.println("Thread Dump Analysis Report");
writer.println("============================");
writer.println("Source: " + dump.getSource());
writer.println("Total Threads: " + result.getTotalThreads());
writer.println("Issues Found: " + result.getIssueCount());
writer.println();
if (result.hasIssues()) {
writer.println("Detected Issues:");
writer.println("----------------");
for (String issue : result.getIssues()) {
writer.println("• " + issue);
}
writer.println();
}
writer.println("Thread State Distribution:");
writer.println("-------------------------");
for (var entry : result.getStateDistribution().entrySet()) {
writer.printf("%-15s: %d%n", entry.getKey(), entry.getValue());
}
} catch (Exception e) {
System.err.println("Failed to generate text report: " + e.getMessage());
}
}
private void generateJSONReport(ThreadDump dump, AnalysisResult result) {
// Implementation for JSON report
String baseName = getBaseName(dump.getSource());
Path reportFile = outputDir.toPath().resolve(baseName + "-report.json");
try {
var objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
objectMapper.writerWithDefaultPrettyPrinter()
.writeValue(reportFile.toFile(), result);
} catch (Exception e) {
System.err.println("Failed to generate JSON report: " + e.getMessage());
}
}
private void printConsoleSummary(AnalysisResult result) {
System.out.println("📊 Analysis Summary:");
System.out.println(" Total threads: " + result.getTotalThreads());
System.out.println(" Daemon threads: " + result.getDaemonThreads());
System.out.println(" Non-daemon threads: " + result.getNonDaemonThreads());
System.out.println(" Issues found: " + result.getIssueCount());
if (result.hasIssues()) {
System.out.println(" ⚠️ Top issues:");
result.getIssuesBySeverity().stream()
.limit(3)
.forEach(issue -> System.out.println(" • " + issue));
} else {
System.out.println(" ✅ No critical issues detected");
}
}
private String getBaseName(String source) {
String name = new File(source).getName();
int dotIndex = name.lastIndexOf('.');
return dotIndex > 0 ? name.substring(0, dotIndex) : name;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new CLIRunner()).execute(args);
System.exit(exitCode);
}
}
Usage Examples
// Programmatic usage
public class ExampleUsage {
public static void main(String[] args) throws Exception {
ThreadDumpParser parser = new ThreadDumpParser();
ThreadDump dump = parser.parseFromFile(Paths.get("thread-dump.txt"));
ThreadDumpAnalyzer analyzer = new ThreadDumpAnalyzer(dump);
AnalysisResult result = analyzer.analyze();
DeadlockDetector detector = new DeadlockDetector();
List<Deadlock> deadlocks = detector.detectDeadlocks(dump);
HTMLReportGenerator reporter = new HTMLReportGenerator();
reporter.generateReport(result, Paths.get("reports"));
System.out.println("Analysis complete. Found " + deadlocks.size() + " deadlocks.");
}
}
// CLI Usage
// java -jar threadanalyzer.jar thread-dump.txt -o ./reports --format HTML
Features
✅ Core Analysis
- Thread state distribution analysis
- Deadlock detection with cycle analysis
- Lock contention identification
- Thread pool utilization analysis
- Stack trace pattern recognition
✅ Advanced Detection
- Multiple deadlock detection algorithms
- Resource contention analysis
- Common anti-pattern detection
- Performance bottleneck identification
✅ Reporting
- Comprehensive HTML reports with charts
- Text-based summaries
- JSON output for integration
- Executive summaries with severity ratings
✅ Performance
- Parallel processing for large dumps
- Efficient memory usage
- Fast parsing algorithms
- Streaming analysis for very large files
This thread dump analyzer provides enterprise-grade analysis capabilities for identifying and diagnosing threading issues in Java applications, with comprehensive reporting and advanced detection algorithms.