Skip to content

Commit 9d00c9a

Browse files
authored
Merge pull request #29 from irockel/feat/SMRInfo
Add SMR Info to Dumps where available
2 parents e4e6da5 + ba925ed commit 9d00c9a

File tree

8 files changed

+284
-2
lines changed

8 files changed

+284
-2
lines changed

.github/copilot-instructions.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copilot Review Instructions: TDA - Thread Dump Analyzer
2+
3+
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.
4+
5+
## Core Technical Principles
6+
7+
### 1. Concurrency & Swing (EDT)
8+
- **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.
9+
- **UI Updates:** Ensure all updates to Swing components are wrapped in `SwingUtilities.invokeLater` if triggered from background threads.
10+
11+
### 2. Memory & Performance
12+
- **Large Files:** Thread dumps can be massive (hundreds of MBs). Prefer streaming and incremental parsing over loading entire files into memory.
13+
- **Object Lifecycle:** Watch for memory leaks in listeners and static collections, especially when opening and closing multiple dump files.
14+
15+
### 3. Parsing Logic (The Core)
16+
- **Accuracy:** The parsing logic for thread states (RUNNABLE, BLOCKED, WAITING) must strictly follow JVM specifications.
17+
- **Robustness:** Handle malformed or truncated thread dumps gracefully without crashing the UI. Provide meaningful error messages to the user.
18+
19+
### 4. Swing UI Best Practices
20+
- **Look & Feel:** Maintain consistency with existing UI components.
21+
- **Layouts:** Prefer `MigLayout` or `GridBagLayout` for complex forms to ensure resizability. Avoid absolute positioning.
22+
- **Accessibility:** Ensure components have appropriate tooltips and mnemonic keys where applicable.
23+
24+
## Review Focus Areas
25+
- **Deadlock Detection:** Double-check the logic that identifies circular dependencies in monitor locks.
26+
- **Regex Performance:** Ensure that regular expressions used for log parsing are optimized and protected against Catastrophic Backtracking.
27+
- **Clean Code:** Enforce Java 11+ coding standards (or the specific version TDA uses), focusing on readability and modularity.
28+
29+
## Communication Style
30+
- Be concise and technical.
31+
- If suggesting a change, explain the impact on performance or thread safety.
32+
- Use code snippets for refactoring suggestions.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ The MCP server exposes the following tools:
112112
| `find_long_running` | None | Identifies threads that remain in the same state/stack trace across consecutive dumps. |
113113
| `analyze_virtual_threads` | None | Detects virtual threads where the carrier thread is stuck in application code. |
114114
| `get_native_threads` | `dump_index` (int, required) | Returns a list of all threads in a native method for a specific thread dump. |
115+
| `get_zombie_threads` | None | Returns a list of zombie threads (unresolved SMR addresses) with timestamps and dump names. |
115116
| `clear` | None | Resets the server state and clears the internal thread store for a new log file. |
116117

117118
#### Troubleshooting
@@ -143,7 +144,7 @@ When you encounter a log file that appears to contain Java thread dumps:
143144
1. DO NOT try to read or "cat" the entire file if it's large.
144145
2. Use the `tda-analyzer` MCP toolset.
145146
3. First, call `parse_log(path="...")` to initialize the analysis.
146-
4. Use `get_summary()`, `check_deadlocks()`, `find_long_running()`, `analyze_virtual_threads()`, and `get_native_threads()` to perform the analysis.
147+
4. Use `get_summary()`, `check_deadlocks()`, `find_long_running()`, `analyze_virtual_threads()`, `get_native_threads()`, and `get_zombie_threads()` to perform the analysis.
147148
5. Provide your insights based on the structured data returned by these tools rather than the raw log text.
148149
```
149150
This configuration makes the analysis much faster and significantly reduces token usage.

tda/src/main/java/de/grimmfrost/tda/mcp/HeadlessAnalysisProvider.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public List<Map<String, Object>> getDumpsSummary() {
5252
info.put("time", tdi.getStartTime());
5353
info.put("threadCount", tdi.getThreads() != null ? tdi.getThreads().getNodeCount() : 0);
5454
info.put("deadlockCount", tdi.getDeadlocks() != null ? tdi.getDeadlocks().getNodeCount() : 0);
55+
if (tdi.getSmrInfo() != null) {
56+
info.put("smrInfo", tdi.getSmrInfo());
57+
}
5558
summary.add(info);
5659
}
5760
return summary;
@@ -152,6 +155,24 @@ public List<Map<String, String>> getNativeThreads(int dumpIndex) {
152155
return nativeThreads;
153156
}
154157

158+
public List<Map<String, String>> getZombieThreads() {
159+
List<Map<String, String>> results = new ArrayList<>();
160+
for (DefaultMutableTreeNode node : topNodes) {
161+
ThreadDumpInfo tdi = (ThreadDumpInfo) node.getUserObject();
162+
List<String> unresolved = tdi.getUnresolvedSmrAddresses();
163+
if (unresolved != null && !unresolved.isEmpty()) {
164+
for (String addr : unresolved) {
165+
Map<String, String> entry = new HashMap<>();
166+
entry.put("address", addr);
167+
entry.put("dumpName", tdi.getName());
168+
entry.put("timestamp", tdi.getStartTime() != null ? tdi.getStartTime() : "unknown");
169+
results.add(entry);
170+
}
171+
}
172+
}
173+
return results;
174+
}
175+
155176
private void collectNativeThreads(Category cat, List<Map<String, String>> nativeThreads) {
156177
if (cat != null) {
157178
int threadCount = cat.getNodeCount();

tda/src/main/java/de/grimmfrost/tda/mcp/MCPServer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ private static void handleListTools(JsonObject request) {
9696
tools.add(createTool("get_native_threads", "Returns a list of all threads currently in a native method for a specific thread dump.",
9797
createProperty("dump_index", "integer", "The index of the thread dump as retrieved from get_summary.")));
9898

99+
tools.add(createTool("get_zombie_threads", "Returns a list of zombie threads (SMR addresses that could not be resolved to any thread).", new JsonObject()));
100+
99101
result.add("tools", gson.toJsonTree(tools));
100102
sendResponse(request.get("id").getAsInt(), result);
101103
}
@@ -156,6 +158,8 @@ private static Object handleGenericRequest(String method, JsonObject params) thr
156158
case "get_native_threads":
157159
int dumpIndex = params.get("dump_index").getAsInt();
158160
return provider.getNativeThreads(dumpIndex);
161+
case "get_zombie_threads":
162+
return provider.getZombieThreads();
159163
case "clear":
160164
provider.clear();
161165
return "Cleared thread store.";

tda/src/main/java/de/grimmfrost/tda/model/ThreadDumpInfo.java

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class ThreadDumpInfo extends AbstractInfo {
3636

3737
private String startTime;
3838
private String overview;
39+
private String smrInfo;
40+
private java.util.List<String> unresolvedSmrAddresses;
3941
private Analyzer dumpAnalyzer;
4042

4143
private Category waitingThreads;
@@ -122,21 +124,39 @@ private void createOverview() {
122124
String hintBgColor = "#fff3cd";
123125
String hintBorderColor = "#ffeeba";
124126
String hintTextColor = "#856404";
127+
String warningBgColor = "#f8d7da";
128+
String warningBorderColor = "#f5c6cb";
129+
String warningTextColor = "#721c24";
125130

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

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

131136
// Thread State Distribution (Visual Chart)
137+
java.util.Map<String, ThreadInfo> tidMap = new java.util.HashMap<>();
132138
if (threadsCount > 0) {
133-
// ... (keep state extraction logic as is)
134139
java.util.Map<String, Integer> stateDistribution = new java.util.HashMap<>();
135140
Category threadsCat = getThreads();
136141
for (int i = 0; i < threadsCount; i++) {
137142
javax.swing.tree.DefaultMutableTreeNode node = (javax.swing.tree.DefaultMutableTreeNode) threadsCat.getNodeAt(i);
138143
ThreadInfo ti = (ThreadInfo) node.getUserObject();
144+
145+
// Build tid map for SMR resolution
139146
String[] tokens = ti.getTokens();
147+
if (tokens != null && tokens.length > 3 && tokens[3] != null && tokens[3].length() > 0) {
148+
try {
149+
String hexTid = "0x" + Long.toHexString(Long.parseLong(tokens[3]));
150+
// Pad to 18 chars ("0x" + 16 hex digits) to match SMR info format often seen
151+
while (hexTid.length() < 18) {
152+
hexTid = "0x0" + hexTid.substring(2);
153+
}
154+
tidMap.put(hexTid, ti);
155+
} catch (NumberFormatException nfe) {
156+
// Invalid tid format; skip this thread when building the SMR tid mapping
157+
}
158+
}
159+
140160
String state = "UNKNOWN";
141161
if (tokens != null) {
142162
if (tokens.length >= 7) {
@@ -215,7 +235,59 @@ private void createOverview() {
215235
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>");
216236
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>");
217237
statData.append("</tr>");
238+
239+
if (getSmrInfo() != null) {
240+
unresolvedSmrAddresses = new java.util.ArrayList<>();
241+
statData.append("<tr>");
242+
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>");
243+
244+
// Format SMR info as table
245+
String smr = getSmrInfo();
246+
java.util.List<String> addresses = new java.util.ArrayList<>();
247+
java.util.regex.Pattern p = java.util.regex.Pattern.compile("0x[0-9a-fA-F]+");
248+
java.util.regex.Matcher m = p.matcher(smr);
249+
while (m.find()) {
250+
String addr = m.group().toLowerCase();
251+
if (!smr.contains("_java_thread_list=" + addr)) {
252+
addresses.add(addr);
253+
}
254+
}
255+
256+
statData.append("<table style=\"margin: 10px 0; border-collapse: collapse; font-size: 11px; width: 100%;\">");
257+
statData.append("<tr style=\"background-color: ").append(chartBgColor).append(";\">");
258+
statData.append("<th style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; text-align: left;\">Address</th>");
259+
statData.append("<th style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; text-align: left;\">Resolved Thread</th>");
260+
statData.append("</tr>");
261+
262+
boolean hasUnresolved = false;
263+
for (String addr : addresses) {
264+
statData.append("<tr>");
265+
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append(";\">").append(addr).append("</td>");
266+
267+
ThreadInfo resolved = tidMap.get(addr);
268+
if (resolved != null) {
269+
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append(";\">").append(resolved.getName()).append("</td>");
270+
} else {
271+
statData.append("<td style=\"padding: 5px; border: 1px solid ").append(borderColor).append("; color: #dc3545; font-weight: bold;\">NOT FOUND</td>");
272+
hasUnresolved = true;
273+
unresolvedSmrAddresses.add(addr);
274+
}
275+
statData.append("</tr>");
276+
}
277+
statData.append("</table>");
278+
279+
if (hasUnresolved) {
280+
statData.append("<div style=\"padding: 10px; background-color: ").append(warningBgColor)
281+
.append("; border: 1px solid ").append(warningBorderColor)
282+
.append("; color: ").append(warningTextColor)
283+
.append("; border-radius: 4px; margin-top: 10px; font-size: 12px;\">")
284+
.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.")
285+
.append("</div>");
286+
}
218287

288+
statData.append("</tr>");
289+
}
290+
219291
statData.append("<tr>");
220292
statData.append("<td style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append(";\"><b>Threads locking:</b> ").append(lockingCount).append("</td>");
221293
statData.append("<td style=\"padding: 8px; border-bottom: 1px solid ").append(borderColor).append(";\"><b>Monitors without locking:</b> ").append(monitorsNoLockCount).append("</td>");
@@ -442,6 +514,33 @@ public void setHeapInfo(HeapInfo value) {
442514
heapInfo = value;
443515
}
444516

517+
/**
518+
* get the SMR info of this thread dump.
519+
* @return SMR info.
520+
*/
521+
public String getSmrInfo() {
522+
return smrInfo;
523+
}
524+
525+
/**
526+
* set the SMR info of this thread dump.
527+
* @param smrInfo the SMR info.
528+
*/
529+
public void setSmrInfo(String smrInfo) {
530+
this.smrInfo = smrInfo;
531+
}
532+
533+
/**
534+
* get the unresolved SMR addresses found in this thread dump.
535+
* @return list of unresolved SMR addresses.
536+
*/
537+
public java.util.List<String> getUnresolvedSmrAddresses() {
538+
if (overview == null) {
539+
createOverview();
540+
}
541+
return unresolvedSmrAddresses;
542+
}
543+
445544
/**
446545
* string representation of this node, is used to displayed the node info
447546
* in the tree.

tda/src/main/java/de/grimmfrost/tda/parser/SunJDKParser.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ public MutableTreeNode parseNext() {
158158
int sleeping = 0;
159159
int virtualThreads = 0;
160160
boolean locked = true;
161+
boolean inSmrInfo = false;
162+
StringBuilder smrInfo = new StringBuilder();
161163
boolean finished = false;
162164
MonitorMap mmap = new MonitorMap();
163165
Stack monitorStack = new Stack();
@@ -211,6 +213,20 @@ public MutableTreeNode parseNext() {
211213
}
212214
}
213215
} else {
216+
if (line.indexOf("Threads class SMR info:") >= 0) {
217+
inSmrInfo = true;
218+
smrInfo.append(line).append("\n");
219+
continue;
220+
}
221+
if (inSmrInfo) {
222+
if (line.trim().length() == 0 || line.startsWith("\"")) {
223+
inSmrInfo = false;
224+
overallTDI.setSmrInfo(smrInfo.toString().trim());
225+
} else {
226+
smrInfo.append(line).append("\n");
227+
continue;
228+
}
229+
}
214230
if (line.startsWith("\"")) {
215231
// We are starting a group of lines for a different thread
216232
// First, flush state for the previous thread (if any)

tda/src/test/java/de/grimmfrost/tda/mcp/HeadlessAnalysisProviderTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,53 @@ public void testNativeThreadAnalysis() throws Exception {
9797
}
9898
assertTrue(foundSpecific, "Should find specific native method with library info");
9999
}
100+
101+
@Test
102+
public void testZombieThreadAnalysis() throws Exception {
103+
HeadlessAnalysisProvider provider = new HeadlessAnalysisProvider();
104+
String logPath = "src/test/resources/jstack_dump.log";
105+
File logFile = new File(logPath);
106+
if (!logFile.exists()) {
107+
System.out.println("[DEBUG_LOG] Skip test, jstack_dump.log not found");
108+
return;
109+
}
110+
111+
provider.parseLogFile(logPath);
112+
113+
// The original jstack_dump.log has no zombies.
114+
List<Map<String, String>> results = (List) provider.getZombieThreads();
115+
assertTrue(results.isEmpty(), "Should report no zombie threads for clean dump");
116+
117+
// Now we need a dump with zombies. We can manually create a temporary file.
118+
File tempFile = File.createTempFile("zombie", ".log");
119+
java.nio.file.Files.write(tempFile.toPath(), ("2026-01-20 17:29:40\n" +
120+
"Full thread dump OpenJDK 64-Bit Server VM (21.0.9+10-LTS mixed mode, sharing):\n" +
121+
"\n" +
122+
"Threads class SMR info:\n" +
123+
"_java_thread_list=0x000000087e826560, length=2, elements={\n" +
124+
"0x000000010328e320, 0x00000001deadbeef\n" +
125+
"}\n" +
126+
"\n" +
127+
"\"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" +
128+
" java.lang.Thread.State: RUNNABLE\n").getBytes());
129+
130+
try {
131+
provider.clear();
132+
provider.parseLogFile(tempFile.getAbsolutePath());
133+
results = (List) provider.getZombieThreads();
134+
135+
boolean found = false;
136+
for (Map<String, String> msg : results) {
137+
if ("0x00000001deadbeef".equals(msg.get("address"))) {
138+
found = true;
139+
assertEquals("2026-01-20 17:29:40", msg.get("timestamp"));
140+
assertNotNull(msg.get("dumpName"));
141+
break;
142+
}
143+
}
144+
assertTrue(found, "Should find zombie thread 0x00000001deadbeef with timestamp");
145+
} finally {
146+
tempFile.delete();
147+
}
148+
}
100149
}

0 commit comments

Comments
 (0)