Skip to content

Commit 44a4e6b

Browse files
committed
detect stuck carrier threads in java 21+ thread dumps
1 parent 10ed7cf commit 44a4e6b

File tree

5 files changed

+150
-0
lines changed

5 files changed

+150
-0
lines changed

tda/src/main/java/de/grimmfrost/tda/Analyzer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ public String analyzeDump() {
105105
statData.append("<a style=\"color: ").append(linkColor).append(";\" href=\"http://www.tagtraum.com/gcviewer.html\">GCViewer-Homepage</a> for more<br>");
106106
statData.append(" information on how to do this.</td></tr>");
107107
}
108+
109+
// check for stuck carrier threads
110+
if (threadCount > 0) {
111+
int stuckCarrierThreads = 0;
112+
Category threadsCat = tdi.getThreads();
113+
for (int i = 0; i < threadCount; i++) {
114+
javax.swing.tree.DefaultMutableTreeNode node = (javax.swing.tree.DefaultMutableTreeNode) threadsCat.getNodeAt(i);
115+
ThreadInfo ti = (ThreadInfo) node.getUserObject();
116+
if (ti.getContent().contains("carrier thread seems to be stuck in application code")) {
117+
stuckCarrierThreads++;
118+
}
119+
}
120+
if (stuckCarrierThreads > 0) {
121+
statData.append("<tr bgcolor=\"#ffffff\"<td></td></tr>");
122+
statData.append("<tr bgcolor=\"").append(altRowColor).append("\"><td colspan=2><font face=System color=\"").append(textColor).append("\"><p>")
123+
.append("Detected ").append(stuckCarrierThreads).append(" virtual thread(s) where the carrier thread seems to be stuck in application code.</p><br>");
124+
statData.append("This might indicate \"pinning\" or long-running operations on a virtual thread that prevent it from yielding.<br>");
125+
statData.append("A Carrier Thread is usually named something like 'ForkJoinPool-x-Worker'.<br>");
126+
statData.append("Check the <a style=\"color: ").append(linkColor).append(";\" href=\"dump://\">thread dump</a> for threads with this warning.</td></tr>");
127+
}
128+
}
108129

109130
return statData.toString();
110131
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ public MutableTreeNode parseNext() {
244244
}
245245
if (isVirtualThread) {
246246
virtualThreads++;
247+
// Reset isVirtualThread here as we are moving to next thread
247248
isVirtualThread = false;
248249
}
249250
}
@@ -307,6 +308,29 @@ public MutableTreeNode parseNext() {
307308
content.append("</b></font>");
308309
content.append("\n");
309310

311+
// Check if carrier thread has issues (like being stuck in application code)
312+
String contentStr = content.toString();
313+
int atIdx = contentStr.indexOf("at ");
314+
if (atIdx > 0) {
315+
String stackTrace = contentStr.substring(atIdx);
316+
String[] lines = stackTrace.split("\n");
317+
boolean foundAppCode = false;
318+
for (String stackLine : lines) {
319+
if (stackLine.contains("at ")
320+
&& !stackLine.contains("java.lang.VirtualThread.run")
321+
&& !stackLine.contains("java.util.concurrent.ForkJoinPool")
322+
&& !stackLine.contains("java.util.concurrent.ForkJoinWorkerThread")
323+
&& !stackLine.contains("java.base@")
324+
&& !stackLine.contains("jdk.internal")) {
325+
foundAppCode = true;
326+
break;
327+
}
328+
}
329+
if (foundAppCode) {
330+
content.append("<font color=\"#ff0000\"><b>Note: carrier thread seems to be stuck in application code.</b></font>\n");
331+
}
332+
}
333+
310334
// Mark this platform thread as carrying a virtual thread
311335
isVirtualThread = true;
312336

@@ -438,6 +462,12 @@ public MutableTreeNode parseNext() {
438462
((Category) catMonitors.getUserObject()).setName(((Category) catMonitors.getUserObject()) + " (" + monitorCount + " Monitors)");
439463
((Category) catMonitorsLocks.getUserObject()).setName(((Category) catMonitorsLocks.getUserObject()) + " (" + monitorsWithoutLocksCount
440464
+ " Monitors)");
465+
466+
if (virtualThreads > 0) {
467+
((Category) catVirtualThreads.getUserObject()).setName(((Category) catVirtualThreads.getUserObject()) + " (" + virtualThreads + " Virtual Threads)");
468+
threadDump.add(catVirtualThreads);
469+
}
470+
441471
// add thread dump to passed dump store.
442472
if ((threadCount > 0) && (dumpKey != null)) {
443473
threadStore.put(dumpKey.trim(), threads);

tda/src/main/resources/de/grimmfrost/tda/doc/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ TDA - Thread Dump Analyzer -- Version 2.7
66
Note: This is the 2.7 release of the software, for an usage overview see Help/Overview.
77

88
Changes from 2.6 are:
9+
* Detect stuck Carrier Threads in Java 21+ thread dumps.
910
* Reworked the thread dump summary to provide information in a more compact way.
1011
* MacOS Binary is now provided.
1112
* Use flatplaf for a modern look and feel.

tda/src/test/java/de/grimmfrost/tda/SunJDKParserTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,77 @@ public void testVirtualThreadDumps() throws FileNotFoundException, IOException {
321321
}
322322
}
323323

324+
@Test
325+
public void testCarrierThreadIssuesDetection() throws FileNotFoundException, IOException {
326+
System.out.println("testCarrierThreadIssuesDetection");
327+
FileInputStream fis = null;
328+
DumpParser instance = null;
329+
330+
try {
331+
fis = new FileInputStream("src/test/resources/carrier_stuck.log");
332+
Map dumpMap = new HashMap();
333+
Vector topNodes = new Vector();
334+
instance = DumpParserFactory.get().getDumpParserForLogfile(fis, dumpMap, false, 0);
335+
336+
assertTrue(instance instanceof SunJDKParser);
337+
338+
while (instance.hasMoreDumps()) {
339+
topNodes.add(instance.parseNext());
340+
}
341+
342+
assertEquals(1, topNodes.size());
343+
DefaultMutableTreeNode dumpNode = (DefaultMutableTreeNode) topNodes.get(0);
344+
345+
// Navigate to virtual threads category
346+
DefaultMutableTreeNode vtCat = null;
347+
for (int i = 0; i < dumpNode.getChildCount(); i++) {
348+
DefaultMutableTreeNode child = (DefaultMutableTreeNode) dumpNode.getChildAt(i);
349+
Object userObject = child.getUserObject();
350+
if (userObject instanceof Category) {
351+
Category cat = (Category) userObject;
352+
if (cat.getName().contains("Virtual Threads")) {
353+
vtCat = child;
354+
break;
355+
}
356+
}
357+
}
358+
359+
assertNotNull(vtCat, "Virtual Threads category should exist");
360+
361+
boolean foundWarning = false;
362+
for (int i = 0; i < vtCat.getChildCount(); i++) {
363+
DefaultMutableTreeNode threadNode = (DefaultMutableTreeNode) vtCat.getChildAt(i);
364+
Object userObject = threadNode.getUserObject();
365+
String threadInfo = userObject.toString();
366+
if (userObject instanceof ThreadInfo) {
367+
String content = ((ThreadInfo)userObject).getContent();
368+
if (threadInfo.contains("ForkJoinPool-1-worker-1") && content.contains("Note:")) {
369+
foundWarning = true;
370+
assertTrue(content.contains("carrier thread seems to be stuck in application code"), "Warning message should be correct");
371+
}
372+
}
373+
}
374+
375+
assertTrue(foundWarning, "Should have found a warning note for the stuck carrier thread");
376+
377+
// Now test the analyzer output
378+
ThreadDumpInfo tdi = (ThreadDumpInfo) dumpNode.getUserObject();
379+
Analyzer analyzer = new Analyzer(tdi);
380+
String hints = analyzer.analyzeDump();
381+
assertNotNull(hints, "Analysis hints should not be null");
382+
assertTrue(hints.contains("carrier thread seems to be stuck in application code"), "Analysis hints should contain warning about stuck carrier thread");
383+
assertTrue(hints.contains("Detected 1 virtual thread(s)"), "Analysis hints should report correct number of stuck carrier threads");
384+
385+
} finally {
386+
if(instance != null) {
387+
instance.close();
388+
}
389+
if(fis != null) {
390+
fis.close();
391+
}
392+
}
393+
}
394+
324395
@Test
325396
public void testLongRunningDetectionWithVariableFields() throws FileNotFoundException, IOException {
326397
System.out.println("testLongRunningDetectionWithVariableFields");
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2026-01-23 21:50:00
2+
Full thread dump OpenJDK 64-Bit Server VM (21.0.2+13-LTS mixed mode, sharing):
3+
4+
"ForkJoinPool-1-worker-1" #11 daemon [11] prio=5 os_prio=0 cpu=5678.90ms elapsed=58230.14s tid=0x00007f8b2c158000 nid=0x1ac7 runnable [0x00007f8b234f5000]
5+
java.lang.Thread.State: RUNNABLE
6+
at com.example.app.StuckService.heavyProcessing(StuckService.java:45)
7+
at com.example.app.StuckService.process(StuckService.java:25)
8+
at java.lang.VirtualThread.run(java.base@21.0.2/VirtualThread.java:309)
9+
at java.util.concurrent.ForkJoinPool.runWorker(java.base@21.0.2/ForkJoinPool.java:1519)
10+
at java.util.concurrent.ForkJoinWorkerThread.run(java.base@21.0.2/ForkJoinWorkerThread.java:165)
11+
Carrying virtual thread #21
12+
13+
"VirtualThread[#21]" #21 virtual [21] prio=5 os_prio=0 cpu=45.23ms elapsed=12345.67s tid=0x00007f8b2c200000 nid=0x1ac8 runnable [0x0000000000000000]
14+
java.lang.Thread.State: RUNNABLE
15+
at com.example.app.StuckService.heavyProcessing(StuckService.java:45)
16+
at com.example.app.StuckService.process(StuckService.java:25)
17+
at java.lang.VirtualThread.run(java.base@21.0.2/VirtualThread.java:309)
18+
19+
"ForkJoinPool-1-worker-2" #12 daemon [12] prio=5 os_prio=0 cpu=100.00ms elapsed=58230.14s tid=0x00007f8b2c159000 nid=0x1ac8 waiting on condition [0x00007f8b233f4000]
20+
java.lang.Thread.State: WAITING (parking)
21+
at jdk.internal.misc.Unsafe.park(java.base@21.0.2/Native Method)
22+
at java.util.concurrent.locks.LockSupport.park(java.base@21.0.2/LockSupport.java:211)
23+
at java.util.concurrent.ForkJoinPool.awaitWork(java.base@21.0.2/ForkJoinPool.java:1565)
24+
at java.util.concurrent.ForkJoinPool.runWorker(java.base@21.0.2/ForkJoinPool.java:1519)
25+
at java.util.concurrent.ForkJoinWorkerThread.run(java.base@21.0.2/ForkJoinWorkerThread.java:165)
26+
27+
"VM Periodic Task Thread" os_prio=0 cpu=67892.34ms elapsed=58234.15s tid=0x00007f8b2c18f000 nid=0x1aca waiting on condition

0 commit comments

Comments
 (0)