Skip to content

Commit 92d854f

Browse files
committed
Add support for different thread dump (closes #5)
1 parent 0a37d51 commit 92d854f

File tree

19 files changed

+475
-137
lines changed

19 files changed

+475
-137
lines changed

app/src/main/kotlin/dev/oblac/tdv/app/LocalApp.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package dev.oblac.tdv.app
33
import java.nio.file.Path
44

55
fun main() {
6-
// generateThreadDumpReport(Path.of("in/Thread.print.20240216120818"))
7-
// generateThreadDumpReport(Path.of("in/Thread.print.20240304090236"))
8-
// generateThreadDumpReport(Path.of("in/Thread.print.20240304090109"))
9-
// generateThreadDumpReport(Path.of("in/Thread.print.20240319144720"))
6+
generateThreadDumpReport(Path.of("in/Thread.print.20240216120818"))
7+
generateThreadDumpReport(Path.of("in/Thread.print.20240304090236"))
8+
generateThreadDumpReport(Path.of("in/Thread.print.20240304090109"))
9+
generateThreadDumpReport(Path.of("in/Thread.print.20240319144720"))
1010
generateThreadDumpReport(Path.of("in/preprod-Thread-20240320.dump"))
1111

12-
// generateThreadDumpReport(Path.of("issues/0001-threaddump.txt.gz"))
12+
generateThreadDumpReport(Path.of("issues/0001-threaddump.txt.gz"))
13+
generateThreadDumpReport(Path.of("issues/0005-tdump.txt.gz"))
14+
1315
}

app/src/test/kotlin/dev/oblac/tdv/app/AppTest0001.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class AppTest0001 {
1515
tda = process("../issues/0001-threaddump.txt.gz")
1616
}
1717
}
18+
1819
@Test
1920
fun testStats() {
2021
assertEquals(34, tda.stats.all.totalThreads)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.oblac.tdv.app
2+
3+
import dev.oblac.tdv.analyzer.ThreadDumpAnalysis
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.BeforeAll
6+
import org.junit.jupiter.api.Test
7+
8+
class AppTest0005 {
9+
10+
companion object {
11+
private lateinit var tda: ThreadDumpAnalysis
12+
@BeforeAll
13+
@JvmStatic
14+
fun setup() {
15+
tda = process("../issues/0005-tdump.txt.gz")
16+
}
17+
}
18+
19+
@Test
20+
fun testStats() {
21+
assertEquals(54, tda.stats.all.totalThreads)
22+
}
23+
}

issues/0005-tdump.txt.gz

5.43 KB
Binary file not shown.

issues/0006-threads_report.txt.gz

3.79 KB
Binary file not shown.

justfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ clean:
99
build:
1010
./gradlew build
1111

12+
test:
13+
./gradlew test
14+
1215
release: clean build
1316
./gradlew shadowJar
1417
ls app/build/libs/*.jar
1518

1619
run target:
17-
java -jar app/build/libs/tdv-0.1.0-all.jar {{target}}
20+
java -jar app/build/libs/tdv-0.5.0-all.jar {{target}}
1821

1922
examples:
2023
just run in/Thread.print.20230908190546

parser/src/main/kotlin/dev/oblac/tdv/parser/ParseStackTrace.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
1010

1111
while (tdi.hasNext()) {
1212
val line = tdi.next().trim()
13-
if (line.isEmpty()) {
14-
tdi.back()
15-
break
16-
}
17-
if (line == "No compile task") {
18-
break
13+
when {
14+
line.isEmpty() -> {
15+
tdi.back()
16+
break
17+
}
18+
line == "No compile task" -> {
19+
break
20+
}
21+
line.startsWith("Compiling:") -> { // todo GC compiler thread
22+
break
23+
}
1924
}
2025

2126
val stackFrame = parseStackFrameLine(line.substring(3), tdi) // "at " prefix
@@ -25,7 +30,7 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
2530
if (!lockLine.startsWith("- ")) {
2631
break
2732
}
28-
val lock = parseLockLine(lockLine.substring(2))
33+
val lock = parseLockLine(lockLine.substring(2), tdi)
2934
stackFrame.locks.add(lock)
3035
tdi.next()
3136
}
@@ -95,12 +100,12 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
95100
FileLine(-1)
96101
)
97102
}
98-
else -> throw IllegalStateException("Could not parse stack frame: $line at $tdi")
103+
else -> throw ParserException("Could not parse stack frame: $line", tdi)
99104
}
100105
return stackFrame
101106
}
102107

103-
private fun parseLockLine(lockLine: String): Lock {
108+
private fun parseLockLine(lockLine: String, tdi: ThreadDumpLocation): Lock {
104109
return when {
105110
lockLine.startsWith("waiting to lock") -> {
106111
val matchResult = HEX_REF_REGEX.find(lockLine)
@@ -138,7 +143,7 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
138143
}
139144
}
140145
else -> {
141-
throw IllegalStateException("Could not parse lock line: $lockLine")}
146+
throw ParserException("Could not parse lock line: $lockLine", tdi)}
142147
}
143148
}
144149
}

parser/src/main/kotlin/dev/oblac/tdv/parser/ParseSystemThreadStatus.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package dev.oblac.tdv.parser
22

33
import dev.oblac.tdv.domain.ThreadState
44

5-
internal object ParseSystemThreadStatus : (String) -> ThreadState {
5+
internal object ParseSystemThreadStatus : (String, ThreadDumpLocation) -> ThreadState {
66

7-
override fun invoke(status: String): ThreadState {
7+
override fun invoke(status: String, tdi: ThreadDumpLocation): ThreadState {
88
return when {
99
status == "runnable" -> ThreadState.RUNNABLE
1010
status == "waiting on condition" -> ThreadState.WAITING
11-
else -> throw IllegalStateException("Unknown thread status: $status")
11+
else -> throw ParserException("Unknown thread status: $status", tdi)
1212
}
1313
}
1414
}

parser/src/main/kotlin/dev/oblac/tdv/parser/ParseThread.kt

Lines changed: 33 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,80 +4,55 @@ import dev.oblac.tdv.domain.*
44

55
internal object ParseThread : (ThreadDumpIterator) -> ThreadInfo {
66

7-
private val JVM_THREAD_HEADER_PARSE_REGEX =
8-
"""^"(?<name>.+?)" #(?<number>\d+)(?: (?<daemon>daemon))? prio=(?<prio>\d+) os_prio=(?<osPrio>\d+) cpu=(?<cpu>[0-9a-z.]+) elapsed=(?<elapsed>[0-9a-z.]+) tid=(?<tid>0x[\da-f]+) nid=(?<nid>0x[\da-f]+) (?<status>.+) \[(?<hexValue>0x[\da-f]+)\]$""".toRegex()
9-
10-
private val SYSTEM_THREAD_HEADER_PARSE_REGEX =
11-
"""^"(?<name>.+?)" os_prio=(?<osPrio>\d+) cpu=(?<cpu>[0-9a-z.]+) elapsed=(?<elapsed>[0-9a-z.]+) tid=(?<tid>0x[\da-f]+) nid=(?<nid>0x[\da-f]+) (?<status>.+)$""".toRegex()
12-
137
override fun invoke(tdi: ThreadDumpIterator): ThreadInfo {
148
val headerLine = tdi.next()
159
val detectLine = headerLine.substringAfter("\"").substringAfter("\"")
16-
return if (detectLine.contains("#")) {
17-
parseAppThread(headerLine, tdi)
18-
} else {
19-
parseSystemThread(headerLine, tdi)
20-
}
10+
11+
return if (detectLine.contains("#")) {
12+
parseAppThread(headerLine, tdi)
13+
} else {
14+
parseSystemThread(headerLine, tdi)
15+
}
2116
}
2217

2318
private fun parseAppThread(
2419
headerLine: String,
2520
tdi: ThreadDumpIterator,
2621
): AppThreadInfo {
27-
val header = JVM_THREAD_HEADER_PARSE_REGEX.find(headerLine)?.groups
28-
?: throw IllegalStateException("Invalid JVM thread header: $headerLine")
29-
30-
val threadName = ThreadName(header["name"]!!.value)
31-
val threadNumber = ThreadNo(header["number"]!!.value.toInt())
32-
val threadDaemon = Daemon.of((header["daemon"] != null))
33-
val threadPriority = ThreadPriority.of(header["prio"]!!.value.toInt())
34-
val osPriority = OsPriority(header["osPrio"]?.value?.toInt() ?: 0)
35-
val cpu = Cpu.of(header["cpu"]!!.value)
36-
val elapsed = Elapsed.of(header["elapsed"]!!.value)
37-
val tid = ThreadId(header["tid"]!!.value)
38-
val nid = NativeId(header["nid"]!!.value)
39-
val threadState = ParseThreadState(tdi)
4022

23+
val header = AppHeader.parse(headerLine, tdi).getOrThrow()
24+
val threadState = ParseThreadState(tdi)
4125
val stackTrace = ParseStackTrace(tdi)
4226

4327
return AppThreadInfo(
44-
name = threadName,
45-
number = threadNumber,
46-
daemon = threadDaemon,
47-
priority = threadPriority,
48-
osPriority = osPriority,
49-
cpu = cpu,
50-
elapsed = elapsed,
51-
tid = tid,
52-
nid = nid,
53-
state = threadState,
54-
stackTrace = stackTrace
55-
)
28+
name = ThreadName(header.name),
29+
number = ThreadNo(header.number.toInt()),
30+
daemon = Daemon.of(header.daemon),
31+
priority = ThreadPriority.of(header.prio),
32+
osPriority = OsPriority(header.osPrio),
33+
cpu = Cpu.of(header.cpu),
34+
elapsed = Elapsed.of(header.elapsed),
35+
tid = ThreadId(header.tid),
36+
nid = NativeId(header.nid),
37+
state = threadState,
38+
stackTrace = stackTrace
39+
)
5640
}
5741

5842
private fun parseSystemThread(
59-
headerLine: String,
60-
tdi: ThreadDumpIterator,
43+
headerLine: String,
44+
tdi: ThreadDumpIterator,
6145
): SystemThreadInfo {
62-
val header = SYSTEM_THREAD_HEADER_PARSE_REGEX.find(headerLine)?.groups
63-
?: throw IllegalStateException("Invalid system thread header: $headerLine at\n$tdi")
64-
65-
val threadName = ThreadName(header["name"]!!.value)
66-
val osPriority = OsPriority(header["osPrio"]?.value?.toInt() ?: 0)
67-
val cpu = Cpu.of(header["cpu"]!!.value)
68-
val elapsed = Elapsed.of(header["elapsed"]!!.value)
69-
val tid = ThreadId(header["tid"]!!.value)
70-
val nid = NativeId(header["nid"]!!.value)
71-
72-
val threadState = ParseSystemThreadStatus(header["status"]!!.value.trim())
73-
return SystemThreadInfo(
74-
name = threadName,
75-
osPriority = osPriority,
76-
cpu = cpu,
77-
elapsed = elapsed,
78-
tid = tid,
79-
nid = nid,
80-
state = threadState
81-
)
46+
val header = JvmHeader.parse(headerLine, tdi).getOrThrow()
47+
48+
return SystemThreadInfo(
49+
name = ThreadName(header.name),
50+
osPriority = OsPriority(header.osPrio),
51+
cpu = Cpu.of(header.cpu),
52+
elapsed = Elapsed.of(header.elapsed),
53+
tid = ThreadId(header.tid),
54+
nid = NativeId(header.nid),
55+
state = ParseSystemThreadStatus(header.state, tdi)
56+
)
8257
}
8358
}

parser/src/main/kotlin/dev/oblac/tdv/parser/ParseThreadDump.kt

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,8 @@ object ParseThreadDump : (String) -> ThreadDump {
88

99
override fun invoke(threadDumpText: String): ThreadDump {
1010
val tdi = ThreadDumpIterator(threadDumpText)
11-
val line0 = tdi.peek()
1211

13-
// header
14-
if (line0.endsWith(':')) {
15-
// Header line with date and name, looks like this:
16-
// 3801084:
17-
// 2024-03-20 17:49:29
18-
// Full thread dump OpenJDK 64-Bit Server VM (17.0.10+7-LTS mixed mode, sharing):
19-
// <empty line>
20-
tdi.skip(4)
21-
}
22-
23-
tdi.peek().let {
24-
if (it == "Threads class SMR info:") {
25-
skipSMR(tdi)
26-
}
27-
}
28-
29-
tdi.skipEmptyLines()
12+
SkipToThreads(tdi)
3013

3114
val threads = mutableListOf<AppThreadInfo>()
3215
val sysThreads = mutableListOf<SystemThreadInfo>()
@@ -45,7 +28,7 @@ object ParseThreadDump : (String) -> ThreadDump {
4528

4629
tdi.skipEmptyLines()
4730
if (tdi.peek().contains("Locked ownable synchronizers:")) { // todo: see what to do whit this
48-
tdi.skip(2)
31+
tdi.skipNonEmptyLines()
4932
}
5033
tdi.skipEmptyLines()
5134
}
@@ -55,35 +38,4 @@ object ParseThreadDump : (String) -> ThreadDump {
5538
sysThreads
5639
)
5740
}
58-
59-
private fun skipSMR(tdi: ThreadDumpIterator) {
60-
fun detectSmr(line: String): Boolean {
61-
return line.startsWith("_java_thread_list=")
62-
|| line.startsWith("_to_delete_list=")
63-
|| line.startsWith("next-> ")
64-
}
65-
tdi.skip()
66-
67-
while (true) {
68-
val nextLine = tdi.peek()
69-
if (!detectSmr(nextLine)) {
70-
break
71-
}
72-
tdi.skipUntilMatch("}")
73-
tdi.skip()
74-
}
75-
}
76-
77-
/**
78-
* Parses the date line if it exists.
79-
*/
80-
private fun isLineWithDate(line: String): Boolean {
81-
return try {
82-
parseDateTime(line)
83-
true
84-
} catch (e: Exception) {
85-
false
86-
}
87-
}
88-
8941
}

0 commit comments

Comments
 (0)