cmd: add coreview tool
Add a sample application to use the gocore package.
Change-Id: I717cdd626531a0d9252c5aac5bcd1acdf357c0a1
Reviewed-on: https://go-review.googlesource.com/92418
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/cmd/viewcore/html.go b/cmd/viewcore/html.go
new file mode 100644
index 0000000..f24223d
--- /dev/null
+++ b/cmd/viewcore/html.go
@@ -0,0 +1,408 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "html"
+ "math"
+ "net/http"
+ "strconv"
+
+ "golang.org/x/debug/core"
+ "golang.org/x/debug/gocore"
+)
+
+func serveHTML(c *gocore.Process) {
+ http.HandleFunc("/object", func(w http.ResponseWriter, r *http.Request) {
+ objs, ok := r.URL.Query()["o"]
+ if !ok || len(objs) != 1 {
+ fmt.Fprintf(w, "wrong or missing o= object specification")
+ return
+ }
+ obj, err := strconv.ParseInt(objs[0], 16, 64)
+ if err != nil {
+ fmt.Fprintf(w, "unparseable o= object specification: %s", err)
+ return
+ }
+ a := core.Address(obj)
+ x, _ := c.FindObject(a)
+ if x == 0 {
+ fmt.Fprintf(w, "can't find object at %x", a)
+ return
+ }
+ addr := c.Addr(x)
+ size := c.Size(x)
+ typ, repeat := c.Type(x)
+
+ tableStyle(w)
+ fmt.Fprintf(w, "<h1>object %x</h1>\n", a)
+ fmt.Fprintf(w, "<h3>%s</h3>\n", html.EscapeString(typeName(c, x)))
+ fmt.Fprintf(w, "<h3>%d bytes</h3>\n", size)
+
+ if typ != nil && repeat == 1 && typ.String() == "runtime.g" {
+ found := false
+ for _, g := range c.Goroutines() {
+ if g.Addr() == addr {
+ found = true
+ break
+ }
+ }
+ if found {
+ fmt.Fprintf(w, "<h3><a href=\"goroutine?g=%x\">goroutine stack</a></h3>\n", addr)
+ }
+ }
+
+ fmt.Fprintf(w, "<table>\n")
+ fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n")
+ var end int64
+ if typ != nil {
+ n := size / typ.Size
+ if n > 1 {
+ for i := int64(0); i < n; i++ {
+ htmlObject(w, c, fmt.Sprintf("[%d]", i), addr.Add(i*typ.Size), typ, nil)
+ }
+ } else {
+ htmlObject(w, c, "", addr, typ, nil)
+ }
+ end = n * typ.Size
+ }
+ for i := end; i < size; i += c.Process().PtrSize() {
+ fmt.Fprintf(w, "<tr><td>f%d</td><td colspan=\"2\">?</td>", i)
+ if c.IsPtr(addr.Add(i)) {
+ fmt.Fprintf(w, "<td>%s</td>", htmlPointer(c, c.Process().ReadPtr(addr.Add(i))))
+ } else {
+ fmt.Fprintf(w, "<td><pre>")
+ for j := int64(0); j < c.Process().PtrSize(); j++ {
+ fmt.Fprintf(w, "%02x ", c.Process().ReadUint8(addr.Add(i+j)))
+ }
+ fmt.Fprintf(w, "</pre></td><td><pre>")
+ for j := int64(0); j < c.Process().PtrSize(); j++ {
+ r := c.Process().ReadUint8(addr.Add(i + j))
+ if r >= 32 && r <= 126 {
+ fmt.Fprintf(w, "%s", html.EscapeString(string(rune(r))))
+ } else {
+ fmt.Fprintf(w, ".")
+ }
+ }
+ fmt.Fprintf(w, "</pre></td>")
+ }
+ fmt.Fprintf(w, "</tr>\n")
+ }
+ fmt.Fprintf(w, "</table>\n")
+ fmt.Fprintf(w, "<h3>references to this object</h3>\n")
+ nrev := 0
+ c.ForEachReversePtr(x, func(z gocore.Object, r *gocore.Root, i, j int64) bool {
+ if nrev == 10 {
+ fmt.Fprintf(w, "...additional references elided...<br/>\n")
+ return false
+ }
+ if r != nil {
+ fmt.Fprintf(w, "%s%s", r.Name, typeFieldName(r.Type, i))
+ } else {
+ t, r := c.Type(z)
+ if t == nil {
+ fmt.Fprintf(w, "%s", htmlPointer(c, c.Addr(z).Add(i)))
+ } else {
+ idx := ""
+ if r > 1 {
+ idx = fmt.Sprintf("[%d]", i/t.Size)
+ i %= t.Size
+ }
+ fmt.Fprintf(w, "%s%s%s", htmlPointer(c, c.Addr(z)), idx, typeFieldName(t, i))
+ }
+ }
+ fmt.Fprintf(w, " → %s<br/>\n", htmlPointer(c, a.Add(j)))
+ nrev++
+ return true
+ })
+ })
+ http.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "<h1>goroutines</h1>\n")
+ tableStyle(w)
+ fmt.Fprintf(w, "<table>\n")
+ fmt.Fprintf(w, "<tr><th align=left>goroutine</th><th align=left>top of stack</th></tr>\n")
+ for _, g := range c.Goroutines() {
+ fmt.Fprintf(w, "<tr><td><a href=\"goroutine?g=%x\">%x</a></td><td>%s</td></tr>\n", g.Addr(), g.Addr(), g.Frames()[0].Func().Name())
+ }
+ fmt.Fprintf(w, "</table>\n")
+ // TODO: export goroutine state (runnable, running, syscall, ...) and print it here.
+ })
+ http.HandleFunc("/goroutine", func(w http.ResponseWriter, r *http.Request) {
+ gs, ok := r.URL.Query()["g"]
+ if !ok || len(gs) != 1 {
+ fmt.Fprintf(w, "wrong or missing g= goroutine specification")
+ return
+ }
+ addr, err := strconv.ParseInt(gs[0], 16, 64)
+ if err != nil {
+ fmt.Fprintf(w, "unparseable g= goroutine specification: %s\n", err)
+ return
+ }
+ a := core.Address(addr)
+ var g *gocore.Goroutine
+ for _, x := range c.Goroutines() {
+ if x.Addr() == a {
+ g = x
+ break
+ }
+ }
+ if g == nil {
+ fmt.Fprintf(w, "goroutine %x not found\n", a)
+ return
+ }
+
+ tableStyle(w)
+ fmt.Fprintf(w, "<h1>goroutine %x</h1>\n", g.Addr())
+ fmt.Fprintf(w, "<h3>%s</h3>\n", htmlPointer(c, g.Addr()))
+ fmt.Fprintf(w, "<h3>%d bytes of stack</h3>\n", g.Stack())
+ for _, f := range g.Frames() {
+ fmt.Fprintf(w, "<h3>%s+%d</h3>\n", f.Func().Name(), f.PC().Sub(f.Func().Entry()))
+ // TODO: convert fn+off to file+lineno.
+ fmt.Fprintf(w, "<table>\n")
+ fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n")
+ for _, r := range f.Roots() {
+ htmlObject(w, c, r.Name, r.Addr, r.Type, f.Live)
+ }
+ fmt.Fprintf(w, "</table>\n")
+ }
+ })
+ http.HandleFunc("/globals", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "<h1>globals</h1>\n")
+ tableStyle(w)
+ fmt.Fprintf(w, "<table>\n")
+ fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n")
+ for _, r := range c.Globals() {
+ htmlObject(w, c, r.Name, r.Addr, r.Type, nil)
+ }
+ fmt.Fprintf(w, "</table>\n")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "<h1>core dump viewer</h1>\n")
+ fmt.Fprintf(w, "%s<br/>\n", c.Process().Arch())
+ fmt.Fprintf(w, "%s<br/>\n", c.BuildVersion())
+ fmt.Fprintf(w, "<a href=\"goroutines\">goroutines</a><br/>\n")
+ fmt.Fprintf(w, "<a href=\"globals\">globals</a><br/>\n")
+ tableStyle(w)
+ fmt.Fprintf(w, "<table>\n")
+ fmt.Fprintf(w, "<tr><th align=left>category</th><th align=left>bytes</th><th align=left>percent</th></tr>\n")
+ all := c.Stats().Size
+ var p func(*gocore.Stats, string)
+ p = func(s *gocore.Stats, prefix string) {
+ fmt.Fprintf(w, "<tr><td>%s%s</td><td align=right>%d</td><td align=right>%.2f</td></tr>\n", prefix, s.Name, s.Size, float64(s.Size)/float64(all)*100)
+ for _, c := range s.Children {
+ p(c, prefix+"..")
+ }
+ }
+ p(c.Stats(), "")
+ fmt.Fprintf(w, "</table>\n")
+ })
+ fmt.Println("serving on :8080")
+ http.ListenAndServe(":8080", nil)
+}
+
+func htmlObject(w http.ResponseWriter, c *gocore.Process, name string, a core.Address, t *gocore.Type, live map[core.Address]bool) {
+ switch t.Kind {
+ case gocore.KindBool:
+ v := c.Process().ReadUint8(a) != 0
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%t</td></tr>\n", name, html.EscapeString(t.String()), v)
+ case gocore.KindInt:
+ var v int64
+ switch t.Size {
+ case 1:
+ v = int64(c.Process().ReadInt8(a))
+ case 2:
+ v = int64(c.Process().ReadInt16(a))
+ case 4:
+ v = int64(c.Process().ReadInt32(a))
+ case 8:
+ v = c.Process().ReadInt64(a)
+ }
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%d</td></tr>\n", name, html.EscapeString(t.String()), v)
+ case gocore.KindUint:
+ var v uint64
+ switch t.Size {
+ case 1:
+ v = uint64(c.Process().ReadUint8(a))
+ case 2:
+ v = uint64(c.Process().ReadUint16(a))
+ case 4:
+ v = uint64(c.Process().ReadUint32(a))
+ case 8:
+ v = c.Process().ReadUint64(a)
+ }
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%d</td></tr>\n", name, html.EscapeString(t.String()), v)
+ case gocore.KindFloat:
+ var v float64
+ switch t.Size {
+ case 4:
+ v = float64(math.Float32frombits(c.Process().ReadUint32(a)))
+ case 8:
+ v = math.Float64frombits(c.Process().ReadUint64(a))
+ }
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%f</td></tr>\n", name, html.EscapeString(t.String()), v)
+ case gocore.KindComplex:
+ var v complex128
+ switch t.Size {
+ case 8:
+ v = complex128(complex(
+ math.Float32frombits(c.Process().ReadUint32(a)),
+ math.Float32frombits(c.Process().ReadUint32(a.Add(4)))))
+
+ case 16:
+ v = complex(
+ math.Float64frombits(c.Process().ReadUint64(a)),
+ math.Float64frombits(c.Process().ReadUint64(a.Add(8))))
+ }
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%f</td></tr>\n", name, html.EscapeString(t.String()), v)
+ case gocore.KindEface:
+ fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">interface{}</td><td>*runtime._type</td><td>%s</td>", name, htmlPointerAt(c, a, live))
+ if live == nil || live[a] {
+ dt := c.DynamicType(t, a)
+ if dt != nil {
+ fmt.Fprintf(w, "<td>%s</td>", dt.Name)
+ }
+ }
+ fmt.Fprintf(w, "</tr>\n")
+ fmt.Fprintf(w, "<tr><td>unsafe.Pointer</td><td>%s</td></tr>\n", htmlPointerAt(c, a.Add(c.Process().PtrSize()), live))
+ case gocore.KindIface:
+ fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">interface{...}</td><td>*runtime.itab</td><td>%s</td>", name, htmlPointerAt(c, a, live))
+ if live == nil || live[a] {
+ dt := c.DynamicType(t, a)
+ if dt != nil {
+ fmt.Fprintf(w, "<td>%s</td>", dt.Name)
+ }
+ }
+ fmt.Fprintf(w, "</tr>\n")
+ fmt.Fprintf(w, "<tr><td>unsafe.Pointer</td><td>%s</td></tr>\n", htmlPointerAt(c, a.Add(c.Process().PtrSize()), live))
+ case gocore.KindPtr:
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%s</td></tr>\n", name, html.EscapeString(t.String()), htmlPointerAt(c, a, live))
+ case gocore.KindFunc:
+ fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%s</td>", name, html.EscapeString(t.String()), htmlPointerAt(c, a, live))
+ if fn := c.Process().ReadPtr(a); fn != 0 {
+ pc := c.Process().ReadPtr(fn)
+ if f := c.FindFunc(pc); f != nil && f.Entry() == pc {
+ fmt.Fprintf(w, "<td>%s</td>", f.Name())
+ }
+ }
+ fmt.Fprintf(w, "</tr>\n")
+ case gocore.KindString:
+ n := c.Process().ReadInt(a.Add(c.Process().PtrSize()))
+ fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">string</td><td>*uint8</td><td>%s</td>", name, htmlPointerAt(c, a, live))
+ if live == nil || live[a] {
+ if n > 0 {
+ n2 := n
+ ddd := ""
+ if n > 100 {
+ n2 = 100
+ ddd = "..."
+ }
+ b := make([]byte, n2)
+ c.Process().ReadAt(b, c.Process().ReadPtr(a))
+ fmt.Fprintf(w, "<td rowspan=\"2\">\"%s\"%s</td>", html.EscapeString(string(b)), ddd)
+ } else {
+ fmt.Fprintf(w, "<td rowspan=\"2\">\"\"</td>")
+ }
+ }
+ fmt.Fprintf(w, "</tr>\n")
+ fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", n)
+ case gocore.KindSlice:
+ fmt.Fprintf(w, "<tr><td rowspan=\"3\">%s</td><td rowspan=\"3\">%s</td><td>*%s</td><td>%s</td></tr>\n", name, t, t.Elem, htmlPointerAt(c, a, live))
+ fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", c.Process().ReadInt(a.Add(c.Process().PtrSize())))
+ fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", c.Process().ReadInt(a.Add(c.Process().PtrSize()*2)))
+ case gocore.KindArray:
+ s := t.Elem.Size
+ n := t.Count
+ if n*s > 16384 {
+ n = (16384 + s - 1) / s
+ }
+ for i := int64(0); i < n; i++ {
+ htmlObject(w, c, fmt.Sprintf("%s[%d]", name, i), a.Add(i*s), t.Elem, live)
+ }
+ if n*s != t.Size {
+ fmt.Fprintf(w, "<tr><td>...</td><td>...</td><td>...</td></tr>\n")
+ }
+ case gocore.KindStruct:
+ for _, f := range t.Fields {
+ htmlObject(w, c, name+"."+f.Name, a.Add(f.Off), f.Type, live)
+ }
+ }
+}
+
+func htmlPointer(c *gocore.Process, a core.Address) string {
+ if a == 0 {
+ return "nil"
+ }
+ x, i := c.FindObject(a)
+ if x == 0 {
+ return fmt.Sprintf("%x", a)
+ }
+ s := fmt.Sprintf("<a href=\"/object?o=%x\">object %x</a>", c.Addr(x), c.Addr(x))
+ if i == 0 {
+ return s
+ }
+ t, r := c.Type(x)
+ if t == nil {
+ return fmt.Sprintf("%s+%d", s, i)
+ }
+ idx := ""
+ if r > 1 {
+ idx = fmt.Sprintf("[%d]", i/t.Size)
+ i %= t.Size
+ }
+ return fmt.Sprintf("%s%s%s", s, idx, typeFieldName(t, i))
+}
+
+func htmlPointerAt(c *gocore.Process, a core.Address, live map[core.Address]bool) string {
+ if live != nil && !live[a] {
+ return "<text style=\"color:LightGray\">dead</text>"
+ }
+ return htmlPointer(c, c.Process().ReadPtr(a))
+}
+
+func tableStyle(w http.ResponseWriter) {
+ fmt.Fprintf(w, "<style>\n")
+ fmt.Fprintf(w, "table, th, td {\n")
+ fmt.Fprintf(w, " border: 1px solid black;\n")
+ fmt.Fprintf(w, " border-collapse: collapse;\n")
+ fmt.Fprintf(w, " align: left;\n")
+ fmt.Fprintf(w, "}\n")
+ fmt.Fprintf(w, "table, th, td {\n")
+ fmt.Fprintf(w, " padding: 2px;\n")
+ fmt.Fprintf(w, "}\n")
+ fmt.Fprintf(w, "tr:hover {background-color: #f5f5f5}\n")
+ fmt.Fprintf(w, "</style>\n")
+
+}
+
+// Returns the name of the field at offset off in x.
+func objField(c *gocore.Process, x gocore.Object, off int64) string {
+ t, r := c.Type(x)
+ if t == nil {
+ return fmt.Sprintf("f%d", off)
+ }
+ s := ""
+ if r > 1 {
+ s = fmt.Sprintf("[%d]", off/t.Size)
+ off %= t.Size
+ }
+ return s + typeFieldName(t, off)
+}
+
+func objRegion(c *gocore.Process, x gocore.Object, off int64) string {
+ t, r := c.Type(x)
+ if t == nil {
+ return fmt.Sprintf("f%d", off)
+ }
+ if off == 0 {
+ return ""
+ }
+ s := ""
+ if r > 1 {
+ s = fmt.Sprintf("[%d]", off/t.Size)
+ off %= t.Size
+ }
+ return s + typeFieldName(t, off)
+}
diff --git a/cmd/viewcore/main.go b/cmd/viewcore/main.go
new file mode 100644
index 0000000..1d94673
--- /dev/null
+++ b/cmd/viewcore/main.go
@@ -0,0 +1,477 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The coreview tool is a command-line tool for exploring the state of a Go process
+// that has dumped core.
+// Run "coreview help" for a list of commands.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "runtime/pprof"
+ "sort"
+ "strconv"
+ "text/tabwriter"
+
+ "golang.org/x/debug/core"
+ "golang.org/x/debug/gocore"
+)
+
+func usage() {
+ fmt.Println(`
+Usage:
+
+ coreview command corefile
+
+The commands are:
+
+ help: print this message
+ overview: print a few overall statistics
+ mappings: print virtual memory mappings
+ goroutines: list goroutines
+ histogram: print histogram of heap memory use by Go type
+ breakdown: print memory use by class
+ objects: print a list of all live objects
+ objgraph: dump object graph to the file tmp.dot
+ reachable: find path from root to an object
+ html: start an http server on :8080 for browsing core file data
+
+Flags applicable to all commands:
+`)
+ flag.PrintDefaults()
+}
+
+func main() {
+ base := flag.String("base", "", "root directory to find core dump file references")
+ prof := flag.String("prof", "", "write cpu profile of coreview to this file (for coreview's developers)")
+ flag.Parse()
+
+ // Extract command.
+ args := flag.Args()
+ if len(args) < 1 {
+ fmt.Fprintf(os.Stderr, "%s: no command specified\n", os.Args[0])
+ usage()
+ os.Exit(2)
+ }
+ cmd := args[0]
+ if cmd == "help" {
+ usage()
+ return
+ }
+
+ if *prof != "" {
+ f, err := os.Create(*prof)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "can't open profile file: %s\n", err)
+ os.Exit(2)
+ }
+ pprof.StartCPUProfile(f)
+ defer pprof.StopCPUProfile()
+ }
+
+ var flags gocore.Flags
+ switch cmd {
+ default:
+ fmt.Fprintf(os.Stderr, "%s: unknown command %s\n", os.Args[0], cmd)
+ fmt.Fprintf(os.Stderr, "Run 'coreview help' for usage.\n")
+ os.Exit(2)
+ case "overview":
+ case "mappings":
+ case "goroutines":
+ case "histogram":
+ flags = gocore.FlagTypes
+ case "breakdown":
+ case "objgraph":
+ flags = gocore.FlagTypes
+ case "objects":
+ flags = gocore.FlagTypes
+ case "reachable":
+ flags = gocore.FlagTypes | gocore.FlagReverse
+ case "html":
+ flags = gocore.FlagTypes | gocore.FlagReverse
+ }
+
+ // All commands other than "help" need a core file.
+ if len(args) < 2 {
+ fmt.Fprintf(os.Stderr, "%s: no core dump specified for command %s\n", os.Args[0], cmd)
+ fmt.Fprintf(os.Stderr, "Run 'coreview help' for usage.\n")
+ os.Exit(2)
+ }
+ file := args[1]
+ p, err := core.Core(file, *base)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+ for _, w := range p.Warnings() {
+ fmt.Fprintf(os.Stderr, "WARNING: %s\n", w)
+ }
+ c, err := gocore.Core(p, flags)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+
+ switch cmd {
+ default:
+ fmt.Fprintf(os.Stderr, "%s: unknown command %s\n", os.Args[0], cmd)
+ fmt.Fprintf(os.Stderr, "Run '%s help' for usage.\n", os.Args[0])
+ os.Exit(2)
+
+ case "overview":
+ t := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
+ fmt.Fprintf(t, "arch\t%s\n", p.Arch())
+ fmt.Fprintf(t, "runtime\t%s\n", c.BuildVersion())
+ var total int64
+ for _, m := range p.Mappings() {
+ total += m.Max().Sub(m.Min())
+ }
+ fmt.Fprintf(t, "memory\t%.1f MB\n", float64(total)/(1<<20))
+ t.Flush()
+
+ case "mappings":
+ t := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.AlignRight)
+ fmt.Fprintf(t, "min\tmax\tperm\tsource\toriginal\t\n")
+ for _, m := range p.Mappings() {
+ perm := ""
+ if m.Perm()&core.Read != 0 {
+ perm += "r"
+ } else {
+ perm += "-"
+ }
+ if m.Perm()&core.Write != 0 {
+ perm += "w"
+ } else {
+ perm += "-"
+ }
+ if m.Perm()&core.Exec != 0 {
+ perm += "x"
+ } else {
+ perm += "-"
+ }
+ file, off := m.Source()
+ fmt.Fprintf(t, "%x\t%x\t%s\t%s@%x\t", m.Min(), m.Max(), perm, file, off)
+ if m.CopyOnWrite() {
+ file, off = m.OrigSource()
+ fmt.Fprintf(t, "%s@%x", file, off)
+ }
+ fmt.Fprintf(t, "\t\n")
+ }
+ t.Flush()
+
+ case "goroutines":
+ for _, g := range c.Goroutines() {
+ fmt.Printf("G stacksize=%x\n", g.Stack())
+ for _, f := range g.Frames() {
+ pc := f.PC()
+ entry := f.Func().Entry()
+ var adj string
+ switch {
+ case pc == entry:
+ adj = ""
+ case pc < entry:
+ adj = fmt.Sprintf("-%d", entry.Sub(pc))
+ default:
+ adj = fmt.Sprintf("+%d", pc.Sub(entry))
+ }
+ fmt.Printf(" %016x %016x %s%s\n", f.Min(), f.Max(), f.Func().Name(), adj)
+ }
+ }
+
+ case "histogram":
+ // Produce an object histogram (bytes per type).
+ type bucket struct {
+ name string
+ size int64
+ count int64
+ }
+ var buckets []*bucket
+ m := map[string]*bucket{}
+ c.ForEachObject(func(x gocore.Object) bool {
+ name := typeName(c, x)
+ b := m[name]
+ if b == nil {
+ b = &bucket{name: name, size: c.Size(x)}
+ buckets = append(buckets, b)
+ m[name] = b
+ }
+ b.count++
+ return true
+ })
+ sort.Slice(buckets, func(i, j int) bool {
+ return buckets[i].size*buckets[i].count > buckets[j].size*buckets[j].count
+ })
+ t := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.AlignRight)
+ fmt.Fprintf(t, "%s\t%s\t%s\t %s\n", "count", "size", "bytes", "type")
+ for _, e := range buckets {
+ fmt.Fprintf(t, "%d\t%d\t%d\t %s\n", e.count, e.size, e.count*e.size, e.name)
+ }
+ t.Flush()
+
+ case "breakdown":
+ t := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', tabwriter.AlignRight)
+ all := c.Stats().Size
+ var printStat func(*gocore.Stats, string)
+ printStat = func(s *gocore.Stats, indent string) {
+ comment := ""
+ switch s.Name {
+ case "bss":
+ comment = "(grab bag, includes OS thread stacks, ...)"
+ case "manual spans":
+ comment = "(Go stacks)"
+ case "retained":
+ comment = "(kept for reuse by Go)"
+ case "released":
+ comment = "(given back to the OS)"
+ }
+ fmt.Fprintf(t, "%s\t%d\t%6.2f%%\t %s\n", fmt.Sprintf("%-20s", indent+s.Name), s.Size, float64(s.Size)*100/float64(all), comment)
+ for _, c := range s.Children {
+ printStat(c, indent+" ")
+ }
+ }
+ printStat(c.Stats(), "")
+ t.Flush()
+
+ case "objgraph":
+ // Dump object graph to output file.
+ w, err := os.Create("tmp.dot")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Fprintf(w, "digraph {\n")
+ for k, r := range c.Globals() {
+ printed := false
+ c.ForEachRootPtr(r, func(i int64, y gocore.Object, j int64) bool {
+ if !printed {
+ fmt.Fprintf(w, "r%d [label=\"%s\n%s\",shape=hexagon]\n", k, r.Name, r.Type)
+ printed = true
+ }
+ fmt.Fprintf(w, "r%d -> o%x [label=\"%s\"", k, c.Addr(y), typeFieldName(r.Type, i))
+ if j != 0 {
+ fmt.Fprintf(w, " ,headlabel=\"+%d\"", j)
+ }
+ fmt.Fprintf(w, "]\n")
+ return true
+ })
+ }
+ for _, g := range c.Goroutines() {
+ last := fmt.Sprintf("o%x", g.Addr())
+ for _, f := range g.Frames() {
+ frame := fmt.Sprintf("f%x", f.Max())
+ fmt.Fprintf(w, "%s [label=\"%s\",shape=rectangle]\n", frame, f.Func().Name())
+ fmt.Fprintf(w, "%s -> %s [style=dotted]\n", last, frame)
+ last = frame
+ for _, r := range f.Roots() {
+ c.ForEachRootPtr(r, func(i int64, y gocore.Object, j int64) bool {
+ fmt.Fprintf(w, "%s -> o%x [label=\"%s%s\"", frame, c.Addr(y), r.Name, typeFieldName(r.Type, i))
+ if j != 0 {
+ fmt.Fprintf(w, " ,headlabel=\"+%d\"", j)
+ }
+ fmt.Fprintf(w, "]\n")
+ return true
+ })
+ }
+ }
+ }
+ c.ForEachObject(func(x gocore.Object) bool {
+ addr := c.Addr(x)
+ size := c.Size(x)
+ fmt.Fprintf(w, "o%x [label=\"%s\\n%d\"]\n", addr, typeName(c, x), size)
+ c.ForEachPtr(x, func(i int64, y gocore.Object, j int64) bool {
+ fmt.Fprintf(w, "o%x -> o%x [label=\"%s\"", addr, c.Addr(y), fieldName(c, x, i))
+ if j != 0 {
+ fmt.Fprintf(w, ",headlabel=\"+%d\"", j)
+ }
+ fmt.Fprintf(w, "]\n")
+ return true
+ })
+ return true
+ })
+ fmt.Fprintf(w, "}")
+ w.Close()
+
+ case "objects":
+ c.ForEachObject(func(x gocore.Object) bool {
+ fmt.Printf("%16x %s\n", c.Addr(x), typeName(c, x))
+ return true
+ })
+
+ case "reachable":
+ if len(args) < 3 {
+ fmt.Fprintf(os.Stderr, "no object address provided\n")
+ os.Exit(1)
+ }
+ n, err := strconv.ParseInt(args[2], 16, 64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "can't parse %s as an object address\n", args[2])
+ os.Exit(1)
+ }
+ a := core.Address(n)
+ obj, _ := c.FindObject(a)
+ if obj == 0 {
+ fmt.Fprintf(os.Stderr, "can't find object at address %s\n", args[2])
+ os.Exit(1)
+ }
+
+ // Breadth-first search backwards until we reach a root.
+ type hop struct {
+ i int64 // offset in "from" object (the key in the path map) where the pointer is
+ x gocore.Object // the "to" object
+ j int64 // the offset in the "to" object
+ }
+ depth := map[gocore.Object]int{}
+ depth[obj] = 0
+ q := []gocore.Object{obj}
+ done := false
+ for !done {
+ if len(q) == 0 {
+ panic("can't find a root that can reach the object")
+ }
+ y := q[0]
+ q = q[1:]
+ c.ForEachReversePtr(y, func(x gocore.Object, r *gocore.Root, i, j int64) bool {
+ if r != nil {
+ // found it.
+ if r.Frame == nil {
+ // Print global
+ fmt.Printf("%s", r.Name)
+ } else {
+ // Print stack up to frame in question.
+ var frames []*gocore.Frame
+ for f := r.Frame.Parent(); f != nil; f = f.Parent() {
+ frames = append(frames, f)
+ }
+ for k := len(frames) - 1; k >= 0; k-- {
+ fmt.Printf("%s\n", frames[k].Func().Name())
+ }
+ // Print frame + variable in frame.
+ fmt.Printf("%s.%s", r.Frame.Func().Name(), r.Name)
+ }
+ fmt.Printf("%s → \n", typeFieldName(r.Type, i))
+
+ z := y
+ for {
+ fmt.Printf("%x %s", c.Addr(z), typeName(c, z))
+ if z == obj {
+ fmt.Println()
+ break
+ }
+ // Find an edge out of z which goes to an object
+ // closer to obj.
+ c.ForEachPtr(z, func(i int64, w gocore.Object, j int64) bool {
+ if d, ok := depth[w]; ok && d < depth[z] {
+ fmt.Printf(" %s → %s", objField(c, z, i), objRegion(c, w, j))
+ z = w
+ return false
+ }
+ return true
+ })
+ fmt.Println()
+ }
+ done = true
+ return false
+ }
+ if _, ok := depth[x]; ok {
+ // we already found a shorter path to this object.
+ return true
+ }
+ depth[x] = depth[y] + 1
+ q = append(q, x)
+ return true
+ })
+ }
+ case "html":
+ serveHTML(c)
+ }
+}
+
+// typeName returns a string representing the type of this object.
+func typeName(c *gocore.Process, x gocore.Object) string {
+ size := c.Size(x)
+ typ, repeat := c.Type(x)
+ if typ == nil {
+ return fmt.Sprintf("unk%d", size)
+ }
+ name := typ.String()
+ n := size / typ.Size
+ if n > 1 {
+ if repeat < n {
+ name = fmt.Sprintf("[%d+%d?]%s", repeat, n-repeat, name)
+ } else {
+ name = fmt.Sprintf("[%d]%s", repeat, name)
+ }
+ }
+ return name
+}
+
+// fieldName returns the name of the field at offset off in x.
+func fieldName(c *gocore.Process, x gocore.Object, off int64) string {
+ size := c.Size(x)
+ typ, repeat := c.Type(x)
+ if typ == nil {
+ return fmt.Sprintf("f%d", off)
+ }
+ n := size / typ.Size
+ i := off / typ.Size
+ if i == 0 && repeat == 1 {
+ // Probably a singleton object, no need for array notation.
+ return typeFieldName(typ, off)
+ }
+ if i >= n {
+ // Partial space at the end of the object - the type can't be complete.
+ return fmt.Sprintf("f%d", off)
+ }
+ q := ""
+ if i >= repeat {
+ // Past the known repeat section, add a ? because we're not sure about the type.
+ q = "?"
+ }
+ return fmt.Sprintf("[%d]%s%s", i, typeFieldName(typ, off-i*typ.Size), q)
+}
+
+// typeFieldName returns the name of the field at offset off in t.
+func typeFieldName(t *gocore.Type, off int64) string {
+ switch t.Kind {
+ case gocore.KindBool, gocore.KindInt, gocore.KindUint, gocore.KindFloat:
+ return ""
+ case gocore.KindComplex:
+ if off == 0 {
+ return ".real"
+ }
+ return ".imag"
+ case gocore.KindIface, gocore.KindEface:
+ if off == 0 {
+ return ".type"
+ }
+ return ".data"
+ case gocore.KindPtr, gocore.KindFunc:
+ return ""
+ case gocore.KindString:
+ if off == 0 {
+ return ".ptr"
+ }
+ return ".len"
+ case gocore.KindSlice:
+ if off == 0 {
+ return ".ptr"
+ }
+ if off <= t.Size/2 {
+ return ".len"
+ }
+ return ".cap"
+ case gocore.KindArray:
+ s := t.Elem.Size
+ i := off / s
+ return fmt.Sprintf("[%d]%s", i, typeFieldName(t.Elem, off-i*s))
+ case gocore.KindStruct:
+ for _, f := range t.Fields {
+ if f.Off <= off && off < f.Off+f.Type.Size {
+ return "." + f.Name + typeFieldName(f.Type, off-f.Off)
+ }
+ }
+ }
+ return ".???"
+}