Skip to content

Commit

Permalink
implement java thread dump parser
Browse files Browse the repository at this point in the history
  • Loading branch information
lqs committed Oct 1, 2023
1 parent 5e65502 commit 2923296
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 33 deletions.
4 changes: 3 additions & 1 deletion common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import "time"
type ProcessType int

const (
ProcessTypeGo ProcessType = iota
ProcessTypeGo = ProcessType(1)
ProcessTypeJava = ProcessType(2)
)

type Process struct {
Type ProcessType
PID int
BuildVersion string
Path string
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/google/gops v0.3.28
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98
github.com/rivo/tview v0.0.0-20230928053139-9bc1d28d88a9
github.com/shirou/gopsutil/v3 v3.23.8
github.com/shirou/gopsutil/v3 v3.23.9
golang.org/x/text v0.13.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
github.com/shirou/gopsutil/v3 v3.23.9 h1:ZI5bWVeu2ep4/DIxB4U9okeYJ7zp/QLTO4auRb/ty/E=
github.com/shirou/gopsutil/v3 v3.23.9/go.mod h1:x/NWSb71eMcjFIO0vhyGW5nZ7oSIgVjrCnADckb85GA=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
Expand Down
1 change: 1 addition & 0 deletions gops/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Goroutine struct {
Id int
Name string
State string
Wait string
Frames []Frame
Expand Down
10 changes: 5 additions & 5 deletions jvmhsperf/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,20 @@ func buildRequest(cmd string, args ...string) []byte {
return request.Bytes()
}

func Execute(pid int, cmd string, args ...string) (string, error) {
func Execute(pid int, cmd string, args ...string) ([]byte, error) {
conn, err := connect(pid)
if err != nil {
return "", err
return nil, err
}
request := buildRequest(cmd, args...)
if _, err := conn.Write(request); err != nil {
return "", fmt.Errorf("error writing to socket: %v", err)
return nil, fmt.Errorf("error writing to socket: %v", err)
}

response, err := io.ReadAll(conn)
if err != nil {
return "", fmt.Errorf("error reading from socket: %v", err)
return nil, fmt.Errorf("error reading from socket: %v", err)
}

return string(response), nil
return response, nil
}
1 change: 1 addition & 0 deletions jvmhsperf/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func ListProcesses() []common.Process {
}
createTime, _ := proc.CreateTime()
processes = append(processes, common.Process{
Type: common.ProcessTypeJava,
PID: pid,
BuildVersion: "java",
Path: cmdline,
Expand Down
62 changes: 62 additions & 0 deletions jvmhsperf/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package jvmhsperf

import (
"bufio"
"bytes"
"regexp"
"strconv"
"strings"

"github.com/lqs/pscope/gops"
)

// "http-nio-8080-exec-1" #20 daemon prio=5 os_prio=0 cpu=126.11ms elapsed=144.34s tid=0x00007f9048f3c290 nid=0xc038 waiting on condition [0x00007f8faf7f9000]
var headerRegex = regexp.MustCompile(`^"(.+)" #(\d+) (?:.+ )?cpu=(\d+(?:\.\d+)?m?s) (?:.+ )?nid=(0x[0-9a-f]+) (.+?) +(\[(0x[0-9a-f]+)])?$`)

// java.lang.Thread.State: TIMED_WAITING (sleeping)
var threadStateRegex = regexp.MustCompile(`^java\.lang\.Thread\.State: (.+)$`)

// at some.package.Class$SubClass.method(RequestMappingHandlerAdapter.java:878)
var frameRegex = regexp.MustCompile(`^at (?:([a-zA-Z0-9.]*?)\.)?([a-zA-Z0-9$]+)\.([a-zA-Z0-9]+)\((.+):(\d+)\)$`)

type Thread struct {
Id int
Name string
State string
Elapsed string
Frames []gops.Frame
}

func ParseJavaThreadDump(threadDump []byte) []*Thread {
var threads []*Thread
reader := bufio.NewReader(bytes.NewReader(threadDump))
for {
line, err := reader.ReadString('\n')
if err != nil {
break
}
line = strings.TrimSpace(line)

if matches := headerRegex.FindStringSubmatch(line); matches != nil {
threadId, _ := strconv.Atoi(matches[2])
thread := &Thread{
Id: threadId,
Name: matches[1],
State: matches[5],
}
threads = append(threads, thread)
} else if matches := threadStateRegex.FindStringSubmatch(line); matches != nil {
thread := threads[len(threads)-1]
thread.State = matches[1]
} else if matches := frameRegex.FindStringSubmatch(line); matches != nil {
frame := gops.Frame{
Package: matches[1],
Func: matches[2] + "." + matches[3],
File: matches[4],
}
thread := threads[len(threads)-1]
thread.Frames = append(thread.Frames, frame)
}
}
return threads
}
11 changes: 6 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"github.com/gdamore/tcell/v2"
"github.com/lqs/pscope/common"
"github.com/lqs/pscope/ui"
"github.com/rivo/tview"
)
Expand Down Expand Up @@ -40,28 +41,28 @@ func main() {

processListView := ui.NewProcessListView(ui.ProcessListViewParams{
Application: application,
OnSelect: func(pid int) {
OnSelect: func(process common.Process) {
t.Push(ui.NewProcessDetailView(ui.ProcessDetailViewParams{
PID: pid,
Process: process,
OnShowStack: func() {
t.Push(ui.NewGoroutineStackView(ui.GoroutineStackViewParams{
Application: application,
PID: pid,
Process: process,
OnClose: t.Pop,
}))
},
OnCPUProfile: func() {
t.Push(ui.NewProfileView(ui.ProfileViewParams{
Application: application,
PID: pid,
Process: process,
Type: "cpu",
OnClose: t.Pop,
}))
},
OnHeapProfile: func() {
t.Push(ui.NewProfileView(ui.ProfileViewParams{
Application: application,
PID: pid,
Process: process,
Type: "heap",
OnClose: t.Pop,
}))
Expand Down
59 changes: 46 additions & 13 deletions ui/goroutinelist.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (

"github.com/gdamore/tcell/v2"
"github.com/google/gops/signal"
"github.com/lqs/pscope/common"
"github.com/lqs/pscope/gops"
"github.com/lqs/pscope/jvmhsperf"
"github.com/rivo/tview"
)

Expand All @@ -18,12 +20,13 @@ type GoroutineStackView struct {

type GoroutineStackViewParams struct {
Application *tview.Application
PID int
Process common.Process
OnClose func()
}

type GoroutineListView struct {
*tview.Table
process common.Process
indexToGoroutine map[int]*gops.Goroutine
currentRow int
}
Expand All @@ -40,10 +43,18 @@ func (g *GoroutineListView) add(goroutine *gops.Goroutine, level int, isLastChil
prefix += "├ "
}
}
g.SetCell(g.currentRow, 0, tview.NewTableCell(prefix+strconv.Itoa(goroutine.Id)))
g.SetCell(g.currentRow, 1, tview.NewTableCell(goroutine.State))
g.SetCell(g.currentRow, 2, tview.NewTableCell(goroutine.Frames[0].Func))
g.SetCell(g.currentRow, 3, tview.NewTableCell(goroutine.Wait))

switch g.process.Type {
case common.ProcessTypeGo:
g.SetCell(g.currentRow, 0, tview.NewTableCell(prefix+strconv.Itoa(goroutine.Id)))
g.SetCell(g.currentRow, 1, tview.NewTableCell(goroutine.State))
g.SetCell(g.currentRow, 2, tview.NewTableCell(goroutine.Frames[0].Func))
g.SetCell(g.currentRow, 3, tview.NewTableCell(goroutine.Wait))
case common.ProcessTypeJava:
g.SetCell(g.currentRow, 0, tview.NewTableCell(prefix+goroutine.Name))
g.SetCell(g.currentRow, 1, tview.NewTableCell(goroutine.State))
}

g.indexToGoroutine[g.currentRow] = goroutine
g.currentRow++

Expand Down Expand Up @@ -71,19 +82,27 @@ func (g *GoroutineListView) Apply(goroutines []*gops.Goroutine) {
}
}

func (v *GoroutineStackView) newGoroutineList() *GoroutineListView {
func (v *GoroutineStackView) newGoroutineList(process common.Process) *GoroutineListView {
table := tview.NewTable()
g := &GoroutineListView{
Table: table,
Table: table,
process: process,
}

table.SetBorder(true)
table.SetTitle(" Goroutines ")
table.SetBorderPadding(0, 0, 1, 1)
table.SetFixed(1, 3)
table.SetCell(0, 0, tview.NewTableCell("ID"))
table.SetCell(0, 1, tview.NewTableCell("State"))
table.SetCell(0, 2, tview.NewTableCell("Wait"))
switch process.Type {
case common.ProcessTypeGo:
table.SetCell(0, 0, tview.NewTableCell("ID"))
table.SetCell(0, 1, tview.NewTableCell("State"))
table.SetCell(0, 2, tview.NewTableCell("Function"))
table.SetCell(0, 3, tview.NewTableCell("Wait"))
case common.ProcessTypeJava:
table.SetCell(0, 0, tview.NewTableCell("Name"))
table.SetCell(0, 1, tview.NewTableCell("State"))
}
table.SetSelectionChangedFunc(func(row, column int) {
if row <= 0 {
row = 1
Expand Down Expand Up @@ -133,7 +152,7 @@ func NewGoroutineStackView(params GoroutineStackViewParams) Widget {
cancel: cancel,
}

goroutineListView := v.newGoroutineList()
goroutineListView := v.newGoroutineList(params.Process)
var stackListView tview.Primitive
flex.AddItem(goroutineListView, 0, 1, true)
flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
Expand All @@ -155,8 +174,22 @@ func NewGoroutineStackView(params GoroutineStackViewParams) Widget {
})

NewReloader(ctx, func() {
result, _ := gops.Cmd(ctx, params.PID, signal.StackTrace)
goroutines := gops.ParseGoStack(result)
var goroutines []*gops.Goroutine
switch params.Process.Type {
case common.ProcessTypeGo:
result, _ := gops.Cmd(ctx, params.Process.PID, signal.StackTrace)
goroutines = gops.ParseGoStack(result)
case common.ProcessTypeJava:
result, _ := jvmhsperf.Execute(params.Process.PID, "threaddump")
for _, thread := range jvmhsperf.ParseJavaThreadDump(result) {
goroutines = append(goroutines, &gops.Goroutine{
Id: thread.Id,
Name: thread.Name,
State: thread.State,
Frames: thread.Frames,
})
}
}

params.Application.QueueUpdateDraw(func() {
v.selectGoroutine = func(goroutine *gops.Goroutine) {
Expand Down
7 changes: 4 additions & 3 deletions ui/processdetail.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strconv"

"github.com/lqs/pscope/common"
"github.com/rivo/tview"
"github.com/shirou/gopsutil/v3/process"
)
Expand All @@ -14,7 +15,7 @@ type ProcessDetailView struct {
}

type ProcessDetailViewParams struct {
PID int
Process common.Process
OnClose func()
OnShowStack func()
OnCPUProfile func()
Expand All @@ -24,7 +25,7 @@ type ProcessDetailViewParams struct {
func NewProcessDetailView(params ProcessDetailViewParams) ProcessDetailView {
form := tview.NewForm()

p, err := process.NewProcess(int32(params.PID))
p, err := process.NewProcess(int32(params.Process.PID))
if err != nil {
panic(err)
}
Expand All @@ -37,7 +38,7 @@ func NewProcessDetailView(params ProcessDetailViewParams) ProcessDetailView {
form.SetTitle(" " + name + " ")
form.SetBorderPadding(0, 0, 1, 1)

form.AddTextView("PID", strconv.Itoa(params.PID), 0, 1, true, false)
form.AddTextView("PID", strconv.Itoa(params.Process.PID), 0, 1, true, false)
form.AddTextView("Name", name, 0, 1, true, false)
form.AddTextView("CPU Percent", fmt.Sprintf("%.1f%%", cpuPercent*100), 0, 1, true, false)

Expand Down
6 changes: 3 additions & 3 deletions ui/processlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type ProcessListView struct {

type ProcessListViewParams struct {
Application *tview.Application
OnSelect func(pid int)
OnSelect func(process common.Process)
OnClose func()
}

Expand All @@ -38,6 +38,7 @@ func findProcesses() []common.Process {
}
createTime, _ := proc.CreateTime()
processes = append(processes, common.Process{
Type: common.ProcessTypeGo,
PID: p.PID,
BuildVersion: p.BuildVersion,
Path: p.Path,
Expand Down Expand Up @@ -136,8 +137,7 @@ func NewProcessListView(params ProcessListViewParams) ProcessListView {
params.Application.QueueUpdateDraw(func() {
table.ScrollToBeginning()
table.SetSelectedFunc(func(row int, column int) {
pid := processes[row-1].PID
params.OnSelect(pid)
params.OnSelect(processes[row-1])
})
})
})
Expand Down
5 changes: 3 additions & 2 deletions ui/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/google/gops/signal"
"github.com/google/pprof/profile"
"github.com/lqs/pscope/common"
"github.com/lqs/pscope/gops"
"github.com/rivo/tview"
"golang.org/x/text/cases"
Expand All @@ -25,7 +26,7 @@ type ProfileView struct {

type ProfileViewParams struct {
Application *tview.Application
PID int
Process common.Process
Type string
OnClose func()
}
Expand Down Expand Up @@ -78,7 +79,7 @@ func (v ProfileView) start(ctx context.Context) {

go func() {
defer close(done)
result, err := gops.Cmd(ctx, v.params.PID, p)
result, err := gops.Cmd(ctx, v.params.Process.PID, p)
if err != nil {
// TODO: show error
v.params.Application.QueueUpdateDraw(func() {
Expand Down

0 comments on commit 2923296

Please sign in to comment.