Skip to content

Commit

Permalink
add 'xgo tool trace' to view stack trace
Browse files Browse the repository at this point in the history
  • Loading branch information
xhd2015 committed Mar 20, 2024
1 parent 1030fb3 commit 1b6e63e
Show file tree
Hide file tree
Showing 32 changed files with 1,782 additions and 70 deletions.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ See [Usage](#usage) and [Documentation](#xgo) for more details.
curl -fsSL https://github.com/xhd2015/xgo/raw/master/install.sh | bash
```

If you've already installed `xgo`, you can upgrade it with:

```sh
xgo upgrade
```

# Requirement
`xgo` requires at least `go1.17` to compile.

There is no specific limitation on OS and Architecture.

**All OS and Architectures** are supported by `xgo` as long as they are supported by `go`.

# Usage
The following code demonstrates how to setup mock on a given function:
- The function `add(a,b)` normally adds `a` and `b` up, resulting in `a+b`,
Expand Down Expand Up @@ -182,7 +195,7 @@ import (
)

func init() {
trace.Use()
trace.Enable()
}

func main() {
Expand Down Expand Up @@ -229,6 +242,11 @@ XGO_TRACE_OUTPUT=stdout xgo run ./
# NOTE: other fields are ommited for displaying key information.
```

You can view the trace with:`xgo tool trace TestExample.json`

Output:
![trace html](cmd/trace/testdata/stack_trace.jpg "Trace")

By default, Trace will write traces to a temp directory under current working directory. This behavior can be overridden by setting `XGO_TRACE_OUTPUT` to different values:
- `XGO_TRACE_OUTPUT=stdout`: traces will be written to stdout, for debugging purepose,
- `XGO_TRACE_OUTPUT=<dir>`: traces will be written to `<dir>`,
Expand All @@ -242,6 +260,3 @@ The strategy employed by `go-mock` works well but causes much longer build time
However, `go-mock` is remarkable for it's discovery of Trap, Trace besides Mock, and additional abilities like trapping variable and disabling map randomness.

It is the shoulder which `xgo` stands on.

# Go Version Requirement
`xgo` requires at least `go1.17` to compile.
18 changes: 18 additions & 0 deletions cmd/trace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Trace
Trace allows visualize generated trace with a crafted UI.

# Usage
```sh
xgo tool trace path/to/Trace.json
```

for example:
```sh
# assuming starting at project root
cd runtime
# run test, generate TestUpdateUserInfo.json
xgo test ./stack_trace

cd ..
xgo tool trace ./runtime/test/stack_trace/TestUpdateUserInfo.json
```
303 changes: 303 additions & 0 deletions cmd/trace/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
package main

import (
_ "embed"
"encoding/json"
"fmt"
"html"
"io"
"net/http"
"os"
"runtime/debug"
"strings"
"time"

"github.com/xhd2015/xgo/support/cmd"
)

func main() {
args := os.Args[1:]
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "requires file\n")
os.Exit(1)
}
file := args[0]
serveFile(file)
}

func serveFile(file string) {
server := http.NewServeMux()
server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
defer func() {
if e := recover(); e != nil {
stack := debug.Stack()
io.WriteString(w, fmt.Sprintf("<pre>panic: %v\n%s</pre>", e, stack))
}
}()
record, err := parseRecord(file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, fmt.Sprintf("%v", err))
return
}
w.Header().Set("Content-Type", "text/html")
renderRecordHTML(record, file, w)
})
port := 7070
url := fmt.Sprintf("http://localhost:%d", port)
fmt.Printf("Server listen at %s\n", url)

go func() {
time.Sleep(500 * time.Millisecond)
cmd.Run("open", url)
}()

err := http.ListenAndServe(":7070", server)
if err != nil {
panic(err)
}
}

//go:embed style.css
var styles string

//go:embed script.js
var script string

func parseRecord(file string) (*RootExport, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var root *RootExport
err = json.Unmarshal(data, &root)
if err != nil {
return nil, err
}
return root, nil
}

func renderRecordHTML(root *RootExport, file string, w io.Writer) {
h := func(s string) {
_, err := io.WriteString(w, s)
if err != nil {
panic(err)
}
_, err = io.WriteString(w, "\n")
if err != nil {
panic(err)
}
}
h(`<!DOCTYPE html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trace of ` + file + `</title>
</head>
<body style="height: 100%;">
`,
)

h(`<div style="height: 100%;">`)
h(`<style>`)
h(styles)
h(`</style>`)

top := &StackExport{
FuncInfo: &FuncInfoExport{
IdentityName: "<root>",
},
Children: root.Children,
}

h("<script>")
h("window.onload = function(){")
h(" const traces = {}")
h(" const ids = []")
traceIDMapping := make(map[*StackExport]int64)
nextID := int64(1)
var walk func(stack *StackExport)
walk = func(stack *StackExport) {
id := nextID
nextID++
traceIDMapping[stack] = id

stackData, err := marshalStackWithoutChildren(stack)
if err != nil {
stackData = []byte(fmt.Sprintf(`{"error":%q}`, err.Error()))
}
h(fmt.Sprintf(` traces["%d"] = %s`, id, stackData))
h(fmt.Sprintf(` ids.push("%d")`, id))
for _, child := range stack.Children {
walk(child)
}
}
walk(top)

h(script)
h("}")
h("</script>")

h(`<div class="root">`)

h(`<div class="trace-list-root">`)
h(`<div>`)
renderToolbar(h)
h(`</div>`)
// h(fmt.Sprintf(`<ul id="%s" class="trace-list">`, getTraceListID(traceIDMapping[top])))
h(`<ul class="trace-list">`)
add(h, top, traceIDMapping)
h("</ul>")
h(`</div>`)

h(`<div class="detail">`)
h(`<div id="detail-info">
<div class="label-value"> <label>Pkg:</label> <div id="detail-info-pkg"> </div> </div>
<div class="label-value"> <label>Func:</label> <div id="detail-info-func"> </div> </div>
</div>`)
h(`<label>Request</label>`)
h(`<textarea id="detail-request" placeholder="request..."></textarea>`)
h(`<label>Response</label>`)
h(`<textarea id="detail-response" placeholder="response..."></textarea>`)
h("</div>")

h("</div>")

h("</div>")
h(`</body>
</html>`)
}
func renderToolbar(h func(s string)) {
h(fmt.Sprintf(`<div id="toolbar" class="toggle-all-on" onClick="onClickExpandAll(arguments[0])">%s</div>`, svgExpand))
}

func getTraceListID(id int64) string {
return fmt.Sprintf("trace_list_%d", id)
}

const svgIconDown = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"></path></svg>`

const svgIconRight = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"></path></svg>`

const svgExpand = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M9 9H4v1h5V9z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z"></path></svg>`

func makeSvg(svg string, extra string) string {
if extra == "" {
return svg
}
mark := "<svg "
idx := strings.Index(svg, "<svg ")
if idx < 0 {
panic(fmt.Errorf("<svg not found"))
}
return svg[:idx+len(mark)] + extra + " " + svg[idx+len(mark):]
}

func marshalStackWithoutChildren(stack *StackExport) ([]byte, error) {
s := stack.Children
stack.Children = nil
defer func() {
stack.Children = s
}()
return json.Marshal(stack)
}

const allowPkgName = false

func add(h func(string), stack *StackExport, traceIDMapping map[*StackExport]int64) {
var name string
if stack.FuncInfo != nil {
name = stack.FuncInfo.IdentityName
if stack.FuncInfo.Pkg != "" && allowPkgName {
name = lastPart(stack.FuncInfo.Pkg) + "." + name
}
}
if name == "" {
name = "<unknown>"
}
id := traceIDMapping[stack]

var indicator string

if len(stack.Children) > 0 {
// NOTE: onclick on svg does not work, must wrap it with div
makeIcon := func(status string, down string, right string) string {
svgDown := makeSvg(down, `class="toggle-icon-down"`)
svgRight := makeSvg(right, `class="toggle-icon-right"`)
toggleID := fmt.Sprintf("toggle_%d", id)
return fmt.Sprintf(`<div id="%s" class="toggle %s" onclick="onClickToggle(arguments[0],'%d')">%s%s</div>`, toggleID, status, id, svgDown, svgRight)
}
indicator = makeIcon("down", svgIconDown, svgIconRight)
}
headClass := "head-block"
if stack.Panic {
headClass = headClass + " panic"
}
if stack.Error != "" {
headClass = headClass + " error"
}

h(fmt.Sprintf(`<div class="head">
%s
<div class="head-info" id="head_%d" onclick="onClickHead('%d')">
<div class="%s"></div>
<span class="head-name">%s</span>
<span class="head-cost">%s</span>
</div>
</div>
`,
indicator,
id, id,
headClass,
html.EscapeString(name),
formatCost(stack.Begin, stack.End),
))

if len(stack.Children) == 0 {
return
}
h(fmt.Sprintf(`<ul id="%s" class="trace-sub-list">`, getTraceListID(id)))
for _, child := range stack.Children {
h("<li>")
add(h, child, traceIDMapping)
h("</li>")
}
h("</ul>")
}

func lastPart(pkg string) string {
idx := strings.LastIndex(pkg, "/")
if idx < 0 {
return pkg
}
return pkg[idx+1:]
}

func formatCost(begin int64, end int64) string {
if end == 0 && begin == 0 {
return ""
}
cost := end - begin
var sign string
if cost < 0 {
sign = "-"
cost = -cost
}
unit := "μs"
f := float64(cost)
if f >= 1000 {
f = f / 1000
unit = "ms"
if f >= 1000 {
f = f / 1000
unit = "s"
if f >= 60 {
f = f / 60
unit = "m"
}
}
}

return fmt.Sprintf("%s%d%s", sign, int(f), unit)
}
Loading

0 comments on commit 1b6e63e

Please sign in to comment.