Skip to content

Commit

Permalink
Add support for different thread dump (closes #5)
Browse files Browse the repository at this point in the history
  • Loading branch information
igr committed Mar 27, 2024
1 parent 0a37d51 commit 92d854f
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 137 deletions.
12 changes: 7 additions & 5 deletions app/src/main/kotlin/dev/oblac/tdv/app/LocalApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package dev.oblac.tdv.app
import java.nio.file.Path

fun main() {
// generateThreadDumpReport(Path.of("in/Thread.print.20240216120818"))
// generateThreadDumpReport(Path.of("in/Thread.print.20240304090236"))
// generateThreadDumpReport(Path.of("in/Thread.print.20240304090109"))
// generateThreadDumpReport(Path.of("in/Thread.print.20240319144720"))
generateThreadDumpReport(Path.of("in/Thread.print.20240216120818"))
generateThreadDumpReport(Path.of("in/Thread.print.20240304090236"))
generateThreadDumpReport(Path.of("in/Thread.print.20240304090109"))
generateThreadDumpReport(Path.of("in/Thread.print.20240319144720"))
generateThreadDumpReport(Path.of("in/preprod-Thread-20240320.dump"))

// generateThreadDumpReport(Path.of("issues/0001-threaddump.txt.gz"))
generateThreadDumpReport(Path.of("issues/0001-threaddump.txt.gz"))
generateThreadDumpReport(Path.of("issues/0005-tdump.txt.gz"))

}
1 change: 1 addition & 0 deletions app/src/test/kotlin/dev/oblac/tdv/app/AppTest0001.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AppTest0001 {
tda = process("../issues/0001-threaddump.txt.gz")
}
}

@Test
fun testStats() {
assertEquals(34, tda.stats.all.totalThreads)
Expand Down
23 changes: 23 additions & 0 deletions app/src/test/kotlin/dev/oblac/tdv/app/AppTest0005.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.oblac.tdv.app

import dev.oblac.tdv.analyzer.ThreadDumpAnalysis
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class AppTest0005 {

companion object {
private lateinit var tda: ThreadDumpAnalysis
@BeforeAll
@JvmStatic
fun setup() {
tda = process("../issues/0005-tdump.txt.gz")
}
}

@Test
fun testStats() {
assertEquals(54, tda.stats.all.totalThreads)
}
}
Binary file added issues/0005-tdump.txt.gz
Binary file not shown.
Binary file added issues/0006-threads_report.txt.gz
Binary file not shown.
5 changes: 4 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ clean:
build:
./gradlew build

test:
./gradlew test

release: clean build
./gradlew shadowJar
ls app/build/libs/*.jar

run target:
java -jar app/build/libs/tdv-0.1.0-all.jar {{target}}
java -jar app/build/libs/tdv-0.5.0-all.jar {{target}}

examples:
just run in/Thread.print.20230908190546
Expand Down
25 changes: 15 additions & 10 deletions parser/src/main/kotlin/dev/oblac/tdv/parser/ParseStackTrace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {

while (tdi.hasNext()) {
val line = tdi.next().trim()
if (line.isEmpty()) {
tdi.back()
break
}
if (line == "No compile task") {
break
when {
line.isEmpty() -> {
tdi.back()
break
}
line == "No compile task" -> {
break
}
line.startsWith("Compiling:") -> { // todo GC compiler thread
break
}
}

val stackFrame = parseStackFrameLine(line.substring(3), tdi) // "at " prefix
Expand All @@ -25,7 +30,7 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
if (!lockLine.startsWith("- ")) {
break
}
val lock = parseLockLine(lockLine.substring(2))
val lock = parseLockLine(lockLine.substring(2), tdi)
stackFrame.locks.add(lock)
tdi.next()
}
Expand Down Expand Up @@ -95,12 +100,12 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
FileLine(-1)
)
}
else -> throw IllegalStateException("Could not parse stack frame: $line at $tdi")
else -> throw ParserException("Could not parse stack frame: $line", tdi)
}
return stackFrame
}

private fun parseLockLine(lockLine: String): Lock {
private fun parseLockLine(lockLine: String, tdi: ThreadDumpLocation): Lock {
return when {
lockLine.startsWith("waiting to lock") -> {
val matchResult = HEX_REF_REGEX.find(lockLine)
Expand Down Expand Up @@ -138,7 +143,7 @@ internal object ParseStackTrace : (ThreadDumpIterator) -> List<StackFrame> {
}
}
else -> {
throw IllegalStateException("Could not parse lock line: $lockLine")}
throw ParserException("Could not parse lock line: $lockLine", tdi)}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package dev.oblac.tdv.parser

import dev.oblac.tdv.domain.ThreadState

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

override fun invoke(status: String): ThreadState {
override fun invoke(status: String, tdi: ThreadDumpLocation): ThreadState {
return when {
status == "runnable" -> ThreadState.RUNNABLE
status == "waiting on condition" -> ThreadState.WAITING
else -> throw IllegalStateException("Unknown thread status: $status")
else -> throw ParserException("Unknown thread status: $status", tdi)
}
}
}
91 changes: 33 additions & 58 deletions parser/src/main/kotlin/dev/oblac/tdv/parser/ParseThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,55 @@ import dev.oblac.tdv.domain.*

internal object ParseThread : (ThreadDumpIterator) -> ThreadInfo {

private val JVM_THREAD_HEADER_PARSE_REGEX =
"""^"(?<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()

private val SYSTEM_THREAD_HEADER_PARSE_REGEX =
"""^"(?<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()

override fun invoke(tdi: ThreadDumpIterator): ThreadInfo {
val headerLine = tdi.next()
val detectLine = headerLine.substringAfter("\"").substringAfter("\"")
return if (detectLine.contains("#")) {
parseAppThread(headerLine, tdi)
} else {
parseSystemThread(headerLine, tdi)
}

return if (detectLine.contains("#")) {
parseAppThread(headerLine, tdi)
} else {
parseSystemThread(headerLine, tdi)
}
}

private fun parseAppThread(
headerLine: String,
tdi: ThreadDumpIterator,
): AppThreadInfo {
val header = JVM_THREAD_HEADER_PARSE_REGEX.find(headerLine)?.groups
?: throw IllegalStateException("Invalid JVM thread header: $headerLine")

val threadName = ThreadName(header["name"]!!.value)
val threadNumber = ThreadNo(header["number"]!!.value.toInt())
val threadDaemon = Daemon.of((header["daemon"] != null))
val threadPriority = ThreadPriority.of(header["prio"]!!.value.toInt())
val osPriority = OsPriority(header["osPrio"]?.value?.toInt() ?: 0)
val cpu = Cpu.of(header["cpu"]!!.value)
val elapsed = Elapsed.of(header["elapsed"]!!.value)
val tid = ThreadId(header["tid"]!!.value)
val nid = NativeId(header["nid"]!!.value)
val threadState = ParseThreadState(tdi)

val header = AppHeader.parse(headerLine, tdi).getOrThrow()
val threadState = ParseThreadState(tdi)
val stackTrace = ParseStackTrace(tdi)

return AppThreadInfo(
name = threadName,
number = threadNumber,
daemon = threadDaemon,
priority = threadPriority,
osPriority = osPriority,
cpu = cpu,
elapsed = elapsed,
tid = tid,
nid = nid,
state = threadState,
stackTrace = stackTrace
)
name = ThreadName(header.name),
number = ThreadNo(header.number.toInt()),
daemon = Daemon.of(header.daemon),
priority = ThreadPriority.of(header.prio),
osPriority = OsPriority(header.osPrio),
cpu = Cpu.of(header.cpu),
elapsed = Elapsed.of(header.elapsed),
tid = ThreadId(header.tid),
nid = NativeId(header.nid),
state = threadState,
stackTrace = stackTrace
)
}

private fun parseSystemThread(
headerLine: String,
tdi: ThreadDumpIterator,
headerLine: String,
tdi: ThreadDumpIterator,
): SystemThreadInfo {
val header = SYSTEM_THREAD_HEADER_PARSE_REGEX.find(headerLine)?.groups
?: throw IllegalStateException("Invalid system thread header: $headerLine at\n$tdi")

val threadName = ThreadName(header["name"]!!.value)
val osPriority = OsPriority(header["osPrio"]?.value?.toInt() ?: 0)
val cpu = Cpu.of(header["cpu"]!!.value)
val elapsed = Elapsed.of(header["elapsed"]!!.value)
val tid = ThreadId(header["tid"]!!.value)
val nid = NativeId(header["nid"]!!.value)

val threadState = ParseSystemThreadStatus(header["status"]!!.value.trim())
return SystemThreadInfo(
name = threadName,
osPriority = osPriority,
cpu = cpu,
elapsed = elapsed,
tid = tid,
nid = nid,
state = threadState
)
val header = JvmHeader.parse(headerLine, tdi).getOrThrow()

return SystemThreadInfo(
name = ThreadName(header.name),
osPriority = OsPriority(header.osPrio),
cpu = Cpu.of(header.cpu),
elapsed = Elapsed.of(header.elapsed),
tid = ThreadId(header.tid),
nid = NativeId(header.nid),
state = ParseSystemThreadStatus(header.state, tdi)
)
}
}
52 changes: 2 additions & 50 deletions parser/src/main/kotlin/dev/oblac/tdv/parser/ParseThreadDump.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,8 @@ object ParseThreadDump : (String) -> ThreadDump {

override fun invoke(threadDumpText: String): ThreadDump {
val tdi = ThreadDumpIterator(threadDumpText)
val line0 = tdi.peek()

// header
if (line0.endsWith(':')) {
// Header line with date and name, looks like this:
// 3801084:
// 2024-03-20 17:49:29
// Full thread dump OpenJDK 64-Bit Server VM (17.0.10+7-LTS mixed mode, sharing):
// <empty line>
tdi.skip(4)
}

tdi.peek().let {
if (it == "Threads class SMR info:") {
skipSMR(tdi)
}
}

tdi.skipEmptyLines()
SkipToThreads(tdi)

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

tdi.skipEmptyLines()
if (tdi.peek().contains("Locked ownable synchronizers:")) { // todo: see what to do whit this
tdi.skip(2)
tdi.skipNonEmptyLines()
}
tdi.skipEmptyLines()
}
Expand All @@ -55,35 +38,4 @@ object ParseThreadDump : (String) -> ThreadDump {
sysThreads
)
}

private fun skipSMR(tdi: ThreadDumpIterator) {
fun detectSmr(line: String): Boolean {
return line.startsWith("_java_thread_list=")
|| line.startsWith("_to_delete_list=")
|| line.startsWith("next-> ")
}
tdi.skip()

while (true) {
val nextLine = tdi.peek()
if (!detectSmr(nextLine)) {
break
}
tdi.skipUntilMatch("}")
tdi.skip()
}
}

/**
* Parses the date line if it exists.
*/
private fun isLineWithDate(line: String): Boolean {
return try {
parseDateTime(line)
true
} catch (e: Exception) {
false
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal object ParseThreadState: (ThreadDumpIterator) -> ThreadState {

override fun invoke(tdi: ThreadDumpIterator): ThreadState {
val stateLine = tdi.next().trim()
val state = THREAD_STATUS_PARSE_REGEX.find(stateLine)?.groups?.get("state")?.value ?: throw IllegalStateException("Invalid thread status: $stateLine")
val state = THREAD_STATUS_PARSE_REGEX.find(stateLine)?.groups?.get("state")?.value ?: throw ParserException("Invalid thread status: $stateLine", tdi)
val split = state.split(" ")

val threadState = ThreadState.valueOf(split[0])
Expand Down
48 changes: 48 additions & 0 deletions parser/src/main/kotlin/dev/oblac/tdv/parser/SkipToThreads.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.oblac.tdv.parser

/**
* The whole purpose of this function is to skip
* the bunch of lines that are not related to threads.
*/
internal object SkipToThreads : (ThreadDumpIterator) -> Unit {
override fun invoke(tdi: ThreadDumpIterator) {
tdi.skipEmptyLines()

// Try if the there is no headers
if (AppHeader.parse(tdi.peek(), tdi).isSuccess) {
return
}

// we want to skip all kind of header lines
tdi.skipNonEmptyLines()
tdi.skipEmptyLines()

// skip all SMR info
tdi.peek().let {
if (it == "Threads class SMR info:") {
skipSMR(tdi)
}
}

tdi.skipEmptyLines()
}

private fun skipSMR(tdi: ThreadDumpIterator) {
fun detectSmr(line: String): Boolean {
return line.startsWith("_java_thread_list=")
|| line.startsWith("_to_delete_list=")
|| line.startsWith("next-> ")
}
tdi.skip()

while (true) {
val nextLine = tdi.peek()
if (!detectSmr(nextLine)) {
break
}
tdi.skipUntilMatch("}")
tdi.skip()
}
}

}
Loading

0 comments on commit 92d854f

Please sign in to comment.