Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copilot Review Instructions: TDA - Thread Dump Analyzer

You are reviewing **TDA (Thread Dump Analyzer)**, a Java-based desktop application using **Swing**. TDA is designed to parse and visualize complex Java thread dumps to identify deadlocks, resource contention, and performance bottlenecks.

## Core Technical Principles

### 1. Concurrency & Swing (EDT)
- **Responsiveness:** Ensure that long-running parsing or analysis tasks are NEVER executed on the Event Dispatch Thread (EDT). Use `SwingWorker` or an equivalent background execution mechanism.
- **UI Updates:** Ensure all updates to Swing components are wrapped in `SwingUtilities.invokeLater` if triggered from background threads.

### 2. Memory & Performance
- **Large Files:** Thread dumps can be massive (hundreds of MBs). Prefer streaming and incremental parsing over loading entire files into memory.
- **Object Lifecycle:** Watch for memory leaks in listeners and static collections, especially when opening and closing multiple dump files.

### 3. Parsing Logic (The Core)
- **Accuracy:** The parsing logic for thread states (RUNNABLE, BLOCKED, WAITING) must strictly follow JVM specifications.
- **Robustness:** Handle malformed or truncated thread dumps gracefully without crashing the UI. Provide meaningful error messages to the user.

### 4. Swing UI Best Practices
- **Look & Feel:** Maintain consistency with existing UI components.
- **Layouts:** Prefer `MigLayout` or `GridBagLayout` for complex forms to ensure resizability. Avoid absolute positioning.
- **Accessibility:** Ensure components have appropriate tooltips and mnemonic keys where applicable.

## Review Focus Areas
- **Deadlock Detection:** Double-check the logic that identifies circular dependencies in monitor locks.
- **Regex Performance:** Ensure that regular expressions used for log parsing are optimized and protected against Catastrophic Backtracking.
- **Clean Code:** Enforce Java 11+ coding standards (or the specific version TDA uses), focusing on readability and modularity.

## Communication Style
- Be concise and technical.
- If suggesting a change, explain the impact on performance or thread safety.
- Use code snippets for refactoring suggestions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ The MCP server exposes the following tools:
| `find_long_running` | None | Identifies threads that remain in the same state/stack trace across consecutive dumps. |
| `analyze_virtual_threads` | None | Detects virtual threads where the carrier thread is stuck in application code. |
| `get_native_threads` | `dump_index` (int, required) | Returns a list of all threads in a native method for a specific thread dump. |
| `get_zombie_threads` | None | Returns a list of zombie threads (unresolved SMR addresses) with timestamps and dump names. |
| `clear` | None | Resets the server state and clears the internal thread store for a new log file. |

#### Troubleshooting
Expand Down Expand Up @@ -143,7 +144,7 @@ When you encounter a log file that appears to contain Java thread dumps:
1. DO NOT try to read or "cat" the entire file if it's large.
2. Use the `tda-analyzer` MCP toolset.
3. First, call `parse_log(path="...")` to initialize the analysis.
4. Use `get_summary()`, `check_deadlocks()`, `find_long_running()`, `analyze_virtual_threads()`, and `get_native_threads()` to perform the analysis.
4. Use `get_summary()`, `check_deadlocks()`, `find_long_running()`, `analyze_virtual_threads()`, `get_native_threads()`, and `get_zombie_threads()` to perform the analysis.
5. Provide your insights based on the structured data returned by these tools rather than the raw log text.
```
This configuration makes the analysis much faster and significantly reduces token usage.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public List<Map<String, Object>> getDumpsSummary() {
info.put("time", tdi.getStartTime());
info.put("threadCount", tdi.getThreads() != null ? tdi.getThreads().getNodeCount() : 0);
info.put("deadlockCount", tdi.getDeadlocks() != null ? tdi.getDeadlocks().getNodeCount() : 0);
if (tdi.getSmrInfo() != null) {
info.put("smrInfo", tdi.getSmrInfo());
}
summary.add(info);
}
return summary;
Expand Down Expand Up @@ -152,6 +155,24 @@ public List<Map<String, String>> getNativeThreads(int dumpIndex) {
return nativeThreads;
}

public List<Map<String, String>> getZombieThreads() {
List<Map<String, String>> results = new ArrayList<>();
for (DefaultMutableTreeNode node : topNodes) {
ThreadDumpInfo tdi = (ThreadDumpInfo) node.getUserObject();
List<String> unresolved = tdi.getUnresolvedSmrAddresses();
if (unresolved != null && !unresolved.isEmpty()) {
for (String addr : unresolved) {
Map<String, String> entry = new HashMap<>();
entry.put("address", addr);
entry.put("dumpName", tdi.getName());
entry.put("timestamp", tdi.getStartTime() != null ? tdi.getStartTime() : "unknown");
results.add(entry);
}
}
}
return results;
}

private void collectNativeThreads(Category cat, List<Map<String, String>> nativeThreads) {
if (cat != null) {
int threadCount = cat.getNodeCount();
Expand Down
4 changes: 4 additions & 0 deletions tda/src/main/java/de/grimmfrost/tda/mcp/MCPServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ private static void handleListTools(JsonObject request) {
tools.add(createTool("get_native_threads", "Returns a list of all threads currently in a native method for a specific thread dump.",
createProperty("dump_index", "integer", "The index of the thread dump as retrieved from get_summary.")));

tools.add(createTool("get_zombie_threads", "Returns a list of zombie threads (SMR addresses that could not be resolved to any thread).", new JsonObject()));

result.add("tools", gson.toJsonTree(tools));
sendResponse(request.get("id").getAsInt(), result);
}
Expand Down Expand Up @@ -156,6 +158,8 @@ private static Object handleGenericRequest(String method, JsonObject params) thr
case "get_native_threads":
int dumpIndex = params.get("dump_index").getAsInt();
return provider.getNativeThreads(dumpIndex);
case "get_zombie_threads":
return provider.getZombieThreads();
case "clear":
provider.clear();
return "Cleared thread store.";
Expand Down
101 changes: 100 additions & 1 deletion tda/src/main/java/de/grimmfrost/tda/model/ThreadDumpInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class ThreadDumpInfo extends AbstractInfo {

private String startTime;
private String overview;
private String smrInfo;
private java.util.List<String> unresolvedSmrAddresses;
private Analyzer dumpAnalyzer;

private Category waitingThreads;
Expand Down Expand Up @@ -122,21 +124,39 @@ private void createOverview() {
String hintBgColor = "#fff3cd";
String hintBorderColor = "#ffeeba";
String hintTextColor = "#856404";
String warningBgColor = "#f8d7da";
String warningBorderColor = "#f5c6cb";
String warningTextColor = "#721c24";

StringBuilder statData = new StringBuilder();
statData.append("<html><body style=\"background-color: ").append(bgColor).append("; font-family: sans-serif; margin: 20px; color: ").append(textColor).append(";\">");

statData.append("<h2 style=\"color: ").append(headerColor).append("; border-bottom: 2px solid #3498db; padding-bottom: 10px;\">Thread Dump Overview</h2>");

// Thread State Distribution (Visual Chart)
java.util.Map<String, ThreadInfo> tidMap = new java.util.HashMap<>();
if (threadsCount > 0) {
// ... (keep state extraction logic as is)
java.util.Map<String, Integer> stateDistribution = new java.util.HashMap<>();
Category threadsCat = getThreads();
for (int i = 0; i < threadsCount; i++) {
javax.swing.tree.DefaultMutableTreeNode node = (javax.swing.tree.DefaultMutableTreeNode) threadsCat.getNodeAt(i);
ThreadInfo ti = (ThreadInfo) node.getUserObject();

// Build tid map for SMR resolution
String[] tokens = ti.getTokens();
if (tokens != null && tokens.length > 3 && tokens[3] != null && tokens[3].length() > 0) {
try {
String hexTid = "0x" + Long.toHexString(Long.parseLong(tokens[3]));
// Pad to 16 chars to match SMR info format often seen
while (hexTid.length() < 18) {
hexTid = "0x0" + hexTid.substring(2);
}
tidMap.put(hexTid, ti);
} catch (NumberFormatException nfe) {
// ignore
}
}

String state = "UNKNOWN";
if (tokens != null) {
if (tokens.length >= 7) {
Expand Down Expand Up @@ -215,7 +235,59 @@ private void createOverview() {
statData.append("<td width=\"50%\" style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append("; background-color: ").append(tableAltRowColor).append(";\"><b>Overall Monitor Count:</b> ").append(monitorsCount).append("</td>");
statData.append("<td width=\"50%\" style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append("; background-color: ").append(tableAltRowColor).append(";\"><b>Deadlocks:</b> <span style=\"").append(deadlocksCount > 0 ? "color: #dc3545; font-weight: bold;" : "").append("\">").append(deadlocksCount).append("</span></td>");
statData.append("</tr>");

if (getSmrInfo() != null) {
unresolvedSmrAddresses = new java.util.ArrayList<>();
statData.append("<tr>");
statData.append("<td colspan=\"2\" style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append(";\"><b>Threads class SMR (Safe Memory Reclamation) info:</b><br>");

// Format SMR info as table
String smr = getSmrInfo();
java.util.List<String> addresses = new java.util.ArrayList<>();
java.util.regex.Pattern p = java.util.regex.Pattern.compile("0x[0-9a-fA-F]+");
java.util.regex.Matcher m = p.matcher(smr);
while (m.find()) {
String addr = m.group().toLowerCase();
if (!smr.contains("_java_thread_list=" + addr)) {
addresses.add(addr);
}
}

statData.append("<table style=\"margin: 10px 0; border-collapse: collapse; font-size: 11px; width: 100%;\">");
statData.append("<tr style=\"background-color: ").append(chartBgColor).append(";\">");
statData.append("<th style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; text-align: left;\">Address</th>");
statData.append("<th style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; text-align: left;\">Resolved Thread</th>");
statData.append("</tr>");

boolean hasUnresolved = false;
for (String addr : addresses) {
statData.append("<tr>");
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append(";\">").append(addr).append("</td>");

ThreadInfo resolved = tidMap.get(addr);
if (resolved != null) {
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append(";\">").append(resolved.getName()).append("</td>");
} else {
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; color: #dc3545; font-weight: bold;\">NOT FOUND</td>");
hasUnresolved = true;
unresolvedSmrAddresses.add(addr);
}
statData.append("</tr>");
}
statData.append("</table>");

if (hasUnresolved) {
statData.append("<div style=\"padding: 10px; background-color: ").append(warningBgColor)
.append("; border: 1px solid ").append(warningBorderColor)
.append("; color: ").append(warningTextColor)
.append("; border-radius: 4px; margin-top: 10px; font-size: 12px;\">")
.append("<b>Warning:</b> Some SMR addresses could not be resolved to threads. This might indicate issues with properly removing threads from the JVM's internal thread list.")
.append("</div>");
}

statData.append("</tr>");
}

statData.append("<tr>");
statData.append("<td style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append(";\"><b>Threads locking:</b> ").append(lockingCount).append("</td>");
statData.append("<td style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append(";\"><b>Monitors without locking:</b> ").append(monitorsNoLockCount).append("</td>");
Expand Down Expand Up @@ -442,6 +514,33 @@ public void setHeapInfo(HeapInfo value) {
heapInfo = value;
}

/**
* get the SMR info of this thread dump.
* @return SMR info.
*/
public String getSmrInfo() {
return smrInfo;
}

/**
* set the SMR info of this thread dump.
* @param smrInfo the SMR info.
*/
public void setSmrInfo(String smrInfo) {
this.smrInfo = smrInfo;
}

/**
* get the unresolved SMR addresses found in this thread dump.
* @return list of unresolved SMR addresses.
*/
public java.util.List<String> getUnresolvedSmrAddresses() {
if (overview == null) {
createOverview();
}
return unresolvedSmrAddresses;
}

/**
* string representation of this node, is used to displayed the node info
* in the tree.
Expand Down
16 changes: 16 additions & 0 deletions tda/src/main/java/de/grimmfrost/tda/parser/SunJDKParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ public MutableTreeNode parseNext() {
int sleeping = 0;
int virtualThreads = 0;
boolean locked = true;
boolean inSmrInfo = false;
StringBuilder smrInfo = new StringBuilder();
boolean finished = false;
MonitorMap mmap = new MonitorMap();
Stack monitorStack = new Stack();
Expand Down Expand Up @@ -211,6 +213,20 @@ public MutableTreeNode parseNext() {
}
}
} else {
if (line.indexOf("Threads class SMR info:") >= 0) {
inSmrInfo = true;
smrInfo.append(line).append("\n");
continue;
}
if (inSmrInfo) {
if (line.trim().length() == 0 || line.startsWith("\"")) {
inSmrInfo = false;
overallTDI.setSmrInfo(smrInfo.toString().trim());
} else {
smrInfo.append(line).append("\n");
continue;
}
}
if (line.startsWith("\"")) {
// We are starting a group of lines for a different thread
// First, flush state for the previous thread (if any)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,53 @@ public void testNativeThreadAnalysis() throws Exception {
}
assertTrue(foundSpecific, "Should find specific native method with library info");
}

@Test
public void testZombieThreadAnalysis() throws Exception {
HeadlessAnalysisProvider provider = new HeadlessAnalysisProvider();
String logPath = "src/test/resources/jstack_dump.log";
File logFile = new File(logPath);
if (!logFile.exists()) {
System.out.println("[DEBUG_LOG] Skip test, jstack_dump.log not found");
return;
}

provider.parseLogFile(logPath);

// The original jstack_dump.log has no zombies.
List<Map<String, String>> results = (List) provider.getZombieThreads();
assertTrue(results.isEmpty(), "Should report no zombie threads for clean dump");

// Now we need a dump with zombies. We can manually create a temporary file.
File tempFile = File.createTempFile("zombie", ".log");
java.nio.file.Files.write(tempFile.toPath(), ("2026-01-20 17:29:40\n" +
"Full thread dump OpenJDK 64-Bit Server VM (21.0.9+10-LTS mixed mode, sharing):\n" +
"\n" +
"Threads class SMR info:\n" +
"_java_thread_list=0x000000087e826560, length=2, elements={\n" +
"0x000000010328e320, 0x00000001deadbeef\n" +
"}\n" +
"\n" +
"\"Reference Handler\" #9 [30467] daemon prio=10 os_prio=31 cpu=0.44ms elapsed=25574.11s tid=0x000000010328e320 nid=30467 waiting on condition [0x000000016e7c2000]\n" +
" java.lang.Thread.State: RUNNABLE\n").getBytes());

try {
provider.clear();
provider.parseLogFile(tempFile.getAbsolutePath());
results = (List) provider.getZombieThreads();

boolean found = false;
for (Map<String, String> msg : results) {
if ("0x00000001deadbeef".equals(msg.get("address"))) {
found = true;
assertEquals("2026-01-20 17:29:40", msg.get("timestamp"));
assertNotNull(msg.get("dumpName"));
break;
}
}
assertTrue(found, "Should find zombie thread 0x00000001deadbeef with timestamp");
} finally {
tempFile.delete();
}
}
}
Loading