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 ".???" +}