Index: src/cmd/pprof/internal/report/report.go |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/src/cmd/pprof/internal/report/report.go |
@@ -0,0 +1,1718 @@ |
+// Copyright 2014 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 report summarizes a performance profile into a |
+// human-readable report. |
+package report |
+ |
+import ( |
+ "fmt" |
+ "io" |
+ "math" |
+ "os" |
+ "path/filepath" |
+ "regexp" |
+ "sort" |
+ "strconv" |
+ "strings" |
+ "time" |
+ |
+ "cmd/pprof/internal/plugin" |
+ "cmd/pprof/internal/profile" |
+) |
+ |
+// Generate generates a report as directed by the Report. |
+func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error { |
+ o := rpt.options |
+ |
+ switch o.OutputFormat { |
+ case Dot: |
+ return printDOT(w, rpt) |
+ case Tree: |
+ return printTree(w, rpt) |
+ case Text: |
+ return printText(w, rpt) |
+ case Raw: |
+ fmt.Fprint(w, rpt.prof.String()) |
+ return nil |
+ case Tags: |
+ return printTags(w, rpt) |
+ case Proto: |
+ return rpt.prof.Write(w) |
+ case Dis: |
+ return printAssembly(w, rpt, obj) |
+ case List: |
+ return printSource(w, rpt) |
+ case WebList: |
+ return printWebSource(w, rpt, obj) |
+ case Callgrind: |
+ return printCallgrind(w, rpt) |
+ } |
+ return fmt.Errorf("unexpected output format") |
+} |
+ |
+// printAssembly prints an annotated assembly listing. |
+func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error { |
+ g, err := newGraph(rpt) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ o := rpt.options |
+ prof := rpt.prof |
+ |
+ // If the regexp source can be parsed as an address, also match |
+ // functions that land on that address. |
+ var address *uint64 |
+ if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { |
+ address = &hex |
+ } |
+ |
+ fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total)) |
+ symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj) |
+ symNodes := nodesPerSymbol(g.ns, symbols) |
+ // Sort function names for printing. |
+ var syms objSymbols |
+ for s := range symNodes { |
+ syms = append(syms, s) |
+ } |
+ sort.Sort(syms) |
+ |
+ // Correlate the symbols from the binary with the profile samples. |
+ for _, s := range syms { |
+ sns := symNodes[s] |
+ |
+ // Gather samples for this symbol. |
+ flatSum, cumSum := sumNodes(sns) |
+ |
+ // Get the function assembly. |
+ insns, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ ns := annotateAssembly(insns, sns, s.base) |
+ |
+ fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0]) |
+ for _, name := range s.sym.Name[1:] { |
+ fmt.Fprintf(w, " AKA ======================== %s\n", name) |
+ } |
+ fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", |
+ rpt.formatValue(flatSum), rpt.formatValue(cumSum), |
+ percentage(cumSum, rpt.total)) |
+ |
+ for _, n := range ns { |
+ fmt.Fprintf(w, "%10s %10s %10x: %s\n", valueOrDot(n.flat, rpt), valueOrDot(n.cum, rpt), n.info.address, n.info.name) |
+ } |
+ } |
+ return nil |
+} |
+ |
+// symbolsFromBinaries examines the binaries listed on the profile |
+// that have associated samples, and identifies symbols matching rx. |
+func symbolsFromBinaries(prof *profile.Profile, g graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol { |
+ hasSamples := make(map[string]bool) |
+ // Only examine mappings that have samples that match the |
+ // regexp. This is an optimization to speed up pprof. |
+ for _, n := range g.ns { |
+ if name := n.info.prettyName(); rx.MatchString(name) && n.info.objfile != "" { |
+ hasSamples[n.info.objfile] = true |
+ } |
+ } |
+ |
+ // Walk all mappings looking for matching functions with samples. |
+ var objSyms []*objSymbol |
+ for _, m := range prof.Mapping { |
+ if !hasSamples[filepath.Base(m.File)] { |
+ if address == nil || !(m.Start <= *address && *address <= m.Limit) { |
+ continue |
+ } |
+ } |
+ |
+ f, err := obj.Open(m.File, m.Start) |
+ if err != nil { |
+ fmt.Printf("%v\n", err) |
+ continue |
+ } |
+ |
+ // Find symbols in this binary matching the user regexp. |
+ var addr uint64 |
+ if address != nil { |
+ addr = *address |
+ } |
+ msyms, err := f.Symbols(rx, addr) |
+ base := f.Base() |
+ f.Close() |
+ if err != nil { |
+ continue |
+ } |
+ for _, ms := range msyms { |
+ objSyms = append(objSyms, |
+ &objSymbol{ |
+ sym: ms, |
+ base: base, |
+ }, |
+ ) |
+ } |
+ } |
+ |
+ return objSyms |
+} |
+ |
+// objSym represents a symbol identified from a binary. It includes |
+// the SymbolInfo from the disasm package and the base that must be |
+// added to correspond to sample addresses |
+type objSymbol struct { |
+ sym *plugin.Sym |
+ base uint64 |
+} |
+ |
+// objSymbols is a wrapper type to enable sorting of []*objSymbol. |
+type objSymbols []*objSymbol |
+ |
+func (o objSymbols) Len() int { |
+ return len(o) |
+} |
+ |
+func (o objSymbols) Less(i, j int) bool { |
+ if namei, namej := o[i].sym.Name[0], o[j].sym.Name[0]; namei != namej { |
+ return namei < namej |
+ } |
+ return o[i].sym.Start < o[j].sym.Start |
+} |
+ |
+func (o objSymbols) Swap(i, j int) { |
+ o[i], o[j] = o[j], o[i] |
+} |
+ |
+// nodesPerSymbol classifies nodes into a group of symbols. |
+func nodesPerSymbol(ns nodes, symbols []*objSymbol) map[*objSymbol]nodes { |
+ symNodes := make(map[*objSymbol]nodes) |
+ for _, s := range symbols { |
+ // Gather samples for this symbol. |
+ for _, n := range ns { |
+ address := n.info.address - s.base |
+ if address >= s.sym.Start && address < s.sym.End { |
+ symNodes[s] = append(symNodes[s], n) |
+ } |
+ } |
+ } |
+ return symNodes |
+} |
+ |
+// annotateAssembly annotates a set of assembly instructions with a |
+// set of samples. It returns a set of nodes to display. base is an |
+// offset to adjust the sample addresses. |
+func annotateAssembly(insns []plugin.Inst, samples nodes, base uint64) nodes { |
+ // Add end marker to simplify printing loop. |
+ insns = append(insns, plugin.Inst{^uint64(0), "", "", 0}) |
+ |
+ // Ensure samples are sorted by address. |
+ samples.sort(addressOrder) |
+ |
+ var s int |
+ var asm nodes |
+ for ix, in := range insns[:len(insns)-1] { |
+ n := node{ |
+ info: nodeInfo{ |
+ address: in.Addr, |
+ name: in.Text, |
+ file: trimPath(in.File), |
+ lineno: in.Line, |
+ }, |
+ } |
+ |
+ // Sum all the samples until the next instruction (to account |
+ // for samples attributed to the middle of an instruction). |
+ for next := insns[ix+1].Addr; s < len(samples) && samples[s].info.address-base < next; s++ { |
+ n.flat += samples[s].flat |
+ n.cum += samples[s].cum |
+ if samples[s].info.file != "" { |
+ n.info.file = trimPath(samples[s].info.file) |
+ n.info.lineno = samples[s].info.lineno |
+ } |
+ } |
+ asm = append(asm, &n) |
+ } |
+ |
+ return asm |
+} |
+ |
+// valueOrDot formats a value according to a report, intercepting zero |
+// values. |
+func valueOrDot(value int64, rpt *Report) string { |
+ if value == 0 { |
+ return "." |
+ } |
+ return rpt.formatValue(value) |
+} |
+ |
+// canAccessFile determines if the filename can be opened for reading. |
+func canAccessFile(path string) bool { |
+ if fi, err := os.Stat(path); err == nil { |
+ return fi.Mode().Perm()&0400 != 0 |
+ } |
+ return false |
+} |
+ |
+// printTags collects all tags referenced in the profile and prints |
+// them in a sorted table. |
+func printTags(w io.Writer, rpt *Report) error { |
+ p := rpt.prof |
+ |
+ // Hashtable to keep accumulate tags as key,value,count. |
+ tagMap := make(map[string]map[string]int64) |
+ for _, s := range p.Sample { |
+ for key, vals := range s.Label { |
+ for _, val := range vals { |
+ if valueMap, ok := tagMap[key]; ok { |
+ valueMap[val] = valueMap[val] + s.Value[0] |
+ continue |
+ } |
+ valueMap := make(map[string]int64) |
+ valueMap[val] = s.Value[0] |
+ tagMap[key] = valueMap |
+ } |
+ } |
+ for key, vals := range s.NumLabel { |
+ for _, nval := range vals { |
+ val := scaledValueLabel(nval, key, "auto") |
+ if valueMap, ok := tagMap[key]; ok { |
+ valueMap[val] = valueMap[val] + s.Value[0] |
+ continue |
+ } |
+ valueMap := make(map[string]int64) |
+ valueMap[val] = s.Value[0] |
+ tagMap[key] = valueMap |
+ } |
+ } |
+ } |
+ |
+ tagKeys := make(tags, 0, len(tagMap)) |
+ for key := range tagMap { |
+ tagKeys = append(tagKeys, &tag{name: key}) |
+ } |
+ sort.Sort(tagKeys) |
+ |
+ for _, tagKey := range tagKeys { |
+ var total int64 |
+ key := tagKey.name |
+ tags := make(tags, 0, len(tagMap[key])) |
+ for t, c := range tagMap[key] { |
+ total += c |
+ tags = append(tags, &tag{name: t, weight: c}) |
+ } |
+ |
+ sort.Sort(tags) |
+ fmt.Fprintf(w, "%s: Total %d\n", key, total) |
+ for _, t := range tags { |
+ if total > 0 { |
+ fmt.Fprintf(w, " %8d (%s): %s\n", t.weight, |
+ percentage(t.weight, total), t.name) |
+ } else { |
+ fmt.Fprintf(w, " %8d: %s\n", t.weight, t.name) |
+ } |
+ } |
+ fmt.Fprintln(w) |
+ } |
+ return nil |
+} |
+ |
+// printText prints a flat text report for a profile. |
+func printText(w io.Writer, rpt *Report) error { |
+ g, err := newGraph(rpt) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ origCount, droppedNodes, _ := g.preprocess(rpt) |
+ fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n")) |
+ |
+ fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n", |
+ "flat", "flat", "sum", "cum", "cum") |
+ |
+ var flatSum int64 |
+ for _, n := range g.ns { |
+ name, flat, cum := n.info.prettyName(), n.flat, n.cum |
+ |
+ flatSum += flat |
+ fmt.Fprintf(w, "%10s %s %s %10s %s %s\n", |
+ rpt.formatValue(flat), |
+ percentage(flat, rpt.total), |
+ percentage(flatSum, rpt.total), |
+ rpt.formatValue(cum), |
+ percentage(cum, rpt.total), |
+ name) |
+ } |
+ return nil |
+} |
+ |
+// printCallgrind prints a graph for a profile on callgrind format. |
+func printCallgrind(w io.Writer, rpt *Report) error { |
+ g, err := newGraph(rpt) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ o := rpt.options |
+ rpt.options.NodeFraction = 0 |
+ rpt.options.EdgeFraction = 0 |
+ rpt.options.NodeCount = 0 |
+ |
+ g.preprocess(rpt) |
+ |
+ fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")") |
+ |
+ files := make(map[string]int) |
+ names := make(map[string]int) |
+ for _, n := range g.ns { |
+ fmt.Fprintln(w, "fl="+callgrindName(files, n.info.file)) |
+ fmt.Fprintln(w, "fn="+callgrindName(names, n.info.name)) |
+ sv, _ := ScaleValue(n.flat, o.SampleUnit, o.OutputUnit) |
+ fmt.Fprintf(w, "%d %d\n", n.info.lineno, int(sv)) |
+ |
+ // Print outgoing edges. |
+ for _, out := range sortedEdges(n.out) { |
+ c, _ := ScaleValue(out.weight, o.SampleUnit, o.OutputUnit) |
+ count := fmt.Sprintf("%d", int(c)) |
+ callee := out.dest |
+ fmt.Fprintln(w, "cfl="+callgrindName(files, callee.info.file)) |
+ fmt.Fprintln(w, "cfn="+callgrindName(names, callee.info.name)) |
+ fmt.Fprintln(w, "calls="+count, callee.info.lineno) |
+ fmt.Fprintln(w, n.info.lineno, count) |
+ } |
+ fmt.Fprintln(w) |
+ } |
+ |
+ return nil |
+} |
+ |
+// callgrindName implements the callgrind naming compression scheme. |
+// For names not previously seen returns "(N) name", where N is a |
+// unique index. For names previously seen returns "(N)" where N is |
+// the index returned the first time. |
+func callgrindName(names map[string]int, name string) string { |
+ if name == "" { |
+ return "" |
+ } |
+ if id, ok := names[name]; ok { |
+ return fmt.Sprintf("(%d)", id) |
+ } |
+ id := len(names) + 1 |
+ names[name] = id |
+ return fmt.Sprintf("(%d) %s", id, name) |
+} |
+ |
+// printTree prints a tree-based report in text form. |
+func printTree(w io.Writer, rpt *Report) error { |
+ const separator = "----------------------------------------------------------+-------------" |
+ const legend = " flat flat% sum% cum cum% calls calls% + context " |
+ |
+ g, err := newGraph(rpt) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ origCount, droppedNodes, _ := g.preprocess(rpt) |
+ fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n")) |
+ |
+ fmt.Fprintln(w, separator) |
+ fmt.Fprintln(w, legend) |
+ var flatSum int64 |
+ |
+ rx := rpt.options.Symbol |
+ for _, n := range g.ns { |
+ name, flat, cum := n.info.prettyName(), n.flat, n.cum |
+ |
+ // Skip any entries that do not match the regexp (for the "peek" command). |
+ if rx != nil && !rx.MatchString(name) { |
+ continue |
+ } |
+ |
+ fmt.Fprintln(w, separator) |
+ // Print incoming edges. |
+ inEdges := sortedEdges(n.in) |
+ inSum := inEdges.sum() |
+ for _, in := range inEdges { |
+ fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(in.weight), |
+ percentage(in.weight, inSum), in.src.info.prettyName()) |
+ } |
+ |
+ // Print current node. |
+ flatSum += flat |
+ fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", |
+ rpt.formatValue(flat), |
+ percentage(flat, rpt.total), |
+ percentage(flatSum, rpt.total), |
+ rpt.formatValue(cum), |
+ percentage(cum, rpt.total), |
+ name) |
+ |
+ // Print outgoing edges. |
+ outEdges := sortedEdges(n.out) |
+ outSum := outEdges.sum() |
+ for _, out := range outEdges { |
+ fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(out.weight), |
+ percentage(out.weight, outSum), out.dest.info.prettyName()) |
+ } |
+ } |
+ if len(g.ns) > 0 { |
+ fmt.Fprintln(w, separator) |
+ } |
+ return nil |
+} |
+ |
+// printDOT prints an annotated callgraph in DOT format. |
+func printDOT(w io.Writer, rpt *Report) error { |
+ g, err := newGraph(rpt) |
+ if err != nil { |
+ return err |
+ } |
+ |
+ origCount, droppedNodes, droppedEdges := g.preprocess(rpt) |
+ |
+ prof := rpt.prof |
+ graphname := "unnamed" |
+ if len(prof.Mapping) > 0 { |
+ graphname = filepath.Base(prof.Mapping[0].File) |
+ } |
+ fmt.Fprintln(w, `digraph "`+graphname+`" {`) |
+ fmt.Fprintln(w, `node [style=filled fillcolor="#f8f8f8"]`) |
+ fmt.Fprintln(w, dotLegend(rpt, g, origCount, droppedNodes, droppedEdges)) |
+ |
+ if len(g.ns) == 0 { |
+ fmt.Fprintln(w, "}") |
+ return nil |
+ } |
+ |
+ // Make sure nodes have a unique consistent id. |
+ nodeIndex := make(map[*node]int) |
+ maxFlat := float64(g.ns[0].flat) |
+ for i, n := range g.ns { |
+ nodeIndex[n] = i + 1 |
+ if float64(n.flat) > maxFlat { |
+ maxFlat = float64(n.flat) |
+ } |
+ } |
+ var edges edgeList |
+ for _, n := range g.ns { |
+ node := dotNode(rpt, maxFlat, nodeIndex[n], n) |
+ fmt.Fprintln(w, node) |
+ if nodelets := dotNodelets(rpt, nodeIndex[n], n); nodelets != "" { |
+ fmt.Fprint(w, nodelets) |
+ } |
+ |
+ // Collect outgoing edges. |
+ for _, e := range n.out { |
+ edges = append(edges, e) |
+ } |
+ } |
+ // Sort edges by frequency as a hint to the graph layout engine. |
+ sort.Sort(edges) |
+ for _, e := range edges { |
+ fmt.Fprintln(w, dotEdge(rpt, nodeIndex[e.src], nodeIndex[e.dest], e)) |
+ } |
+ fmt.Fprintln(w, "}") |
+ return nil |
+} |
+ |
+// percentage computes the percentage of total of a value, and encodes |
+// it as a string. At least two digits of precision are printed. |
+func percentage(value, total int64) string { |
+ var ratio float64 |
+ if total != 0 { |
+ ratio = float64(value) / float64(total) * 100 |
+ } |
+ switch { |
+ case ratio >= 99.95: |
+ return " 100%" |
+ case ratio >= 1.0: |
+ return fmt.Sprintf("%5.2f%%", ratio) |
+ default: |
+ return fmt.Sprintf("%5.2g%%", ratio) |
+ } |
+} |
+ |
+// dotLegend generates the overall graph label for a report in DOT format. |
+func dotLegend(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) string { |
+ label := legendLabels(rpt) |
+ label = append(label, legendDetailLabels(rpt, g, origCount, droppedNodes, droppedEdges)...) |
+ return fmt.Sprintf(`subgraph cluster_L { L [shape=box fontsize=32 label="%s\l"] }`, strings.Join(label, `\l`)) |
+} |
+ |
+// legendLabels generates labels exclusive to graph visualization. |
+func legendLabels(rpt *Report) []string { |
+ prof := rpt.prof |
+ o := rpt.options |
+ var label []string |
+ if len(prof.Mapping) > 0 { |
+ if prof.Mapping[0].File != "" { |
+ label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) |
+ } |
+ if prof.Mapping[0].BuildID != "" { |
+ label = append(label, "Build ID: "+prof.Mapping[0].BuildID) |
+ } |
+ } |
+ if o.SampleType != "" { |
+ label = append(label, "Type: "+o.SampleType) |
+ } |
+ if prof.TimeNanos != 0 { |
+ const layout = "Jan 2, 2006 at 3:04pm (MST)" |
+ label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) |
+ } |
+ if prof.DurationNanos != 0 { |
+ label = append(label, fmt.Sprintf("Duration: %v", time.Duration(prof.DurationNanos))) |
+ } |
+ return label |
+} |
+ |
+// legendDetailLabels generates labels common to graph and text visualization. |
+func legendDetailLabels(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) []string { |
+ nodeFraction := rpt.options.NodeFraction |
+ edgeFraction := rpt.options.EdgeFraction |
+ nodeCount := rpt.options.NodeCount |
+ |
+ label := []string{} |
+ |
+ var flatSum int64 |
+ for _, n := range g.ns { |
+ flatSum = flatSum + n.flat |
+ } |
+ |
+ label = append(label, fmt.Sprintf("%s of %s total (%s)", rpt.formatValue(flatSum), rpt.formatValue(rpt.total), percentage(flatSum, rpt.total))) |
+ |
+ if rpt.total > 0 { |
+ if droppedNodes > 0 { |
+ label = append(label, genLabel(droppedNodes, "node", "cum", |
+ rpt.formatValue(int64(float64(rpt.total)*nodeFraction)))) |
+ } |
+ if droppedEdges > 0 { |
+ label = append(label, genLabel(droppedEdges, "edge", "freq", |
+ rpt.formatValue(int64(float64(rpt.total)*edgeFraction)))) |
+ } |
+ if nodeCount > 0 && nodeCount < origCount { |
+ label = append(label, fmt.Sprintf("Showing top %d nodes out of %d (cum >= %s)", |
+ nodeCount, origCount, |
+ rpt.formatValue(g.ns[len(g.ns)-1].cum))) |
+ } |
+ } |
+ return label |
+} |
+ |
+func genLabel(d int, n, l, f string) string { |
+ if d > 1 { |
+ n = n + "s" |
+ } |
+ return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) |
+} |
+ |
+// dotNode generates a graph node in DOT format. |
+func dotNode(rpt *Report, maxFlat float64, rIndex int, n *node) string { |
+ flat, cum := n.flat, n.cum |
+ |
+ labels := strings.Split(n.info.prettyName(), "::") |
+ label := strings.Join(labels, `\n`) + `\n` |
+ |
+ flatValue := rpt.formatValue(flat) |
+ if flat > 0 { |
+ label = label + fmt.Sprintf(`%s(%s)`, |
+ flatValue, |
+ strings.TrimSpace(percentage(flat, rpt.total))) |
+ } else { |
+ label = label + "0" |
+ } |
+ cumValue := flatValue |
+ if cum != flat { |
+ if flat > 0 { |
+ label = label + `\n` |
+ } else { |
+ label = label + " " |
+ } |
+ cumValue = rpt.formatValue(cum) |
+ label = label + fmt.Sprintf(`of %s(%s)`, |
+ cumValue, |
+ strings.TrimSpace(percentage(cum, rpt.total))) |
+ } |
+ |
+ // Scale font sizes from 8 to 24 based on percentage of flat frequency. |
+ // Use non linear growth to emphasize the size difference. |
+ baseFontSize, maxFontGrowth := 8, 16.0 |
+ fontSize := baseFontSize |
+ if maxFlat > 0 && flat > 0 && float64(flat) <= maxFlat { |
+ fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(flat)/maxFlat))) |
+ } |
+ return fmt.Sprintf(`N%d [label="%s" fontsize=%d shape=box tooltip="%s (%s)"]`, |
+ rIndex, |
+ label, |
+ fontSize, n.info.prettyName(), cumValue) |
+} |
+ |
+// dotEdge generates a graph edge in DOT format. |
+func dotEdge(rpt *Report, from, to int, e *edgeInfo) string { |
+ w := rpt.formatValue(e.weight) |
+ attr := fmt.Sprintf(`label=" %s"`, w) |
+ if rpt.total > 0 { |
+ if weight := 1 + int(e.weight*100/rpt.total); weight > 1 { |
+ attr = fmt.Sprintf(`%s weight=%d`, attr, weight) |
+ } |
+ if width := 1 + int(e.weight*5/rpt.total); width > 1 { |
+ attr = fmt.Sprintf(`%s penwidth=%d`, attr, width) |
+ } |
+ } |
+ arrow := "->" |
+ if e.residual { |
+ arrow = "..." |
+ } |
+ tooltip := fmt.Sprintf(`"%s %s %s (%s)"`, |
+ e.src.info.prettyName(), arrow, e.dest.info.prettyName(), w) |
+ attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, |
+ attr, tooltip, tooltip) |
+ |
+ if e.residual { |
+ attr = attr + ` style="dotted"` |
+ } |
+ |
+ if len(e.src.tags) > 0 { |
+ // Separate children further if source has tags. |
+ attr = attr + " minlen=2" |
+ } |
+ return fmt.Sprintf("N%d -> N%d [%s]", from, to, attr) |
+} |
+ |
+// dotNodelets generates the DOT boxes for the node tags. |
+func dotNodelets(rpt *Report, rIndex int, n *node) (dot string) { |
+ const maxNodelets = 4 // Number of nodelets for alphanumeric labels |
+ const maxNumNodelets = 4 // Number of nodelets for numeric labels |
+ |
+ var ts, nts tags |
+ for _, t := range n.tags { |
+ if t.unit == "" { |
+ ts = append(ts, t) |
+ } else { |
+ nts = append(nts, t) |
+ } |
+ } |
+ |
+ // Select the top maxNodelets alphanumeric labels by weight |
+ sort.Sort(ts) |
+ if len(ts) > maxNodelets { |
+ ts = ts[:maxNodelets] |
+ } |
+ for i, t := range ts { |
+ weight := rpt.formatValue(t.weight) |
+ dot += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) |
+ dot += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) |
+ } |
+ |
+ // Collapse numeric labels into maxNumNodelets buckets, of the form: |
+ // 1MB..2MB, 3MB..5MB, ... |
+ nts = collapseTags(nts, maxNumNodelets) |
+ sort.Sort(nts) |
+ for i, t := range nts { |
+ weight := rpt.formatValue(t.weight) |
+ dot += fmt.Sprintf(`NN%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) |
+ dot += fmt.Sprintf(`N%d -> NN%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) |
+ } |
+ |
+ return dot |
+} |
+ |
+// graph summarizes a performance profile into a format that is |
+// suitable for visualization. |
+type graph struct { |
+ ns nodes |
+} |
+ |
+// nodes is an ordered collection of graph nodes. |
+type nodes []*node |
+ |
+// tags represent sample annotations |
+type tags []*tag |
+type tagMap map[string]*tag |
+ |
+type tag struct { |
+ name string |
+ unit string // Describe the value, "" for non-numeric tags |
+ value int64 |
+ weight int64 |
+} |
+ |
+func (t tags) Len() int { return len(t) } |
+func (t tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] } |
+func (t tags) Less(i, j int) bool { |
+ if t[i].weight == t[j].weight { |
+ return t[i].name < t[j].name |
+ } |
+ return t[i].weight > t[j].weight |
+} |
+ |
+// node is an entry on a profiling report. It represents a unique |
+// program location. It can include multiple names to represent |
+// inlined functions. |
+type node struct { |
+ info nodeInfo // Information associated to this entry. |
+ |
+ // values associated to this node. |
+ // flat is exclusive to this node, cum includes all descendents. |
+ flat, cum int64 |
+ |
+ // in and out contains the nodes immediately reaching or reached by this nodes. |
+ in, out edgeMap |
+ |
+ // tags provide additional information about subsets of a sample. |
+ tags tagMap |
+} |
+ |
+func (ts tags) string() string { |
+ var ret string |
+ for _, s := range ts { |
+ ret = ret + fmt.Sprintf("%s %s %d %d\n", s.name, s.unit, s.value, s.weight) |
+ } |
+ return ret |
+} |
+ |
+type nodeInfo struct { |
+ name string |
+ origName string |
+ address uint64 |
+ file string |
+ startLine, lineno int |
+ inline bool |
+ lowPriority bool |
+ objfile string |
+ parent *node // Used only if creating a calltree |
+} |
+ |
+func (n *node) addTags(s *profile.Sample, weight int64) { |
+ // Add a tag with all string labels |
+ var labels []string |
+ for key, vals := range s.Label { |
+ for _, v := range vals { |
+ labels = append(labels, key+":"+v) |
+ } |
+ } |
+ if len(labels) > 0 { |
+ sort.Strings(labels) |
+ l := n.tags.findOrAddTag(strings.Join(labels, `\n`), "", 0) |
+ l.weight += weight |
+ } |
+ |
+ for key, nvals := range s.NumLabel { |
+ for _, v := range nvals { |
+ label := scaledValueLabel(v, key, "auto") |
+ l := n.tags.findOrAddTag(label, key, v) |
+ l.weight += weight |
+ } |
+ } |
+} |
+ |
+func (m tagMap) findOrAddTag(label, unit string, value int64) *tag { |
+ if l := m[label]; l != nil { |
+ return l |
+ } |
+ l := &tag{ |
+ name: label, |
+ unit: unit, |
+ value: value, |
+ } |
+ m[label] = l |
+ return l |
+} |
+ |
+// collapseTags reduces the number of entries in a tagMap by merging |
+// adjacent nodes into ranges. It uses a greedy approach to merge |
+// starting with the entries with the lowest weight. |
+func collapseTags(ts tags, count int) tags { |
+ if len(ts) <= count { |
+ return ts |
+ } |
+ |
+ sort.Sort(ts) |
+ tagGroups := make([]tags, count) |
+ for i, t := range ts[:count] { |
+ tagGroups[i] = tags{t} |
+ } |
+ for _, t := range ts[count:] { |
+ g, d := 0, tagDistance(t, tagGroups[0][0]) |
+ for i := 1; i < count; i++ { |
+ if nd := tagDistance(t, tagGroups[i][0]); nd < d { |
+ g, d = i, nd |
+ } |
+ } |
+ tagGroups[g] = append(tagGroups[g], t) |
+ } |
+ |
+ var nts tags |
+ for _, g := range tagGroups { |
+ l, w := tagGroupLabel(g) |
+ nts = append(nts, &tag{ |
+ name: l, |
+ weight: w, |
+ }) |
+ } |
+ return nts |
+} |
+ |
+func tagDistance(t, u *tag) float64 { |
+ v, _ := ScaleValue(u.value, u.unit, t.unit) |
+ if v < float64(t.value) { |
+ return float64(t.value) - v |
+ } |
+ return v - float64(t.value) |
+} |
+ |
+func tagGroupLabel(g tags) (string, int64) { |
+ if len(g) == 1 { |
+ t := g[0] |
+ return scaledValueLabel(t.value, t.unit, "auto"), t.weight |
+ } |
+ min := g[0] |
+ max := g[0] |
+ w := min.weight |
+ for _, t := range g[1:] { |
+ if v, _ := ScaleValue(t.value, t.unit, min.unit); int64(v) < min.value { |
+ min = t |
+ } |
+ if v, _ := ScaleValue(t.value, t.unit, max.unit); int64(v) > max.value { |
+ max = t |
+ } |
+ w += t.weight |
+ } |
+ return scaledValueLabel(min.value, min.unit, "auto") + ".." + |
+ scaledValueLabel(max.value, max.unit, "auto"), w |
+} |
+ |
+// sumNodes adds the flat and sum values on a report. |
+func sumNodes(ns nodes) (flat int64, cum int64) { |
+ for _, n := range ns { |
+ flat += n.flat |
+ cum += n.cum |
+ } |
+ return |
+} |
+ |
+type edgeMap map[*node]*edgeInfo |
+ |
+// edgeInfo contains any attributes to be represented about edges in a graph/ |
+type edgeInfo struct { |
+ src, dest *node |
+ // The summary weight of the edge |
+ weight int64 |
+ // residual edges connect nodes that were connected through a |
+ // separate node, which has been removed from the report. |
+ residual bool |
+} |
+ |
+// bumpWeight increases the weight of an edge. If there isn't such an |
+// edge in the map one is created. |
+func bumpWeight(from, to *node, w int64, residual bool) { |
+ if from.out[to] != to.in[from] { |
+ panic(fmt.Errorf("asymmetric edges %v %v", *from, *to)) |
+ } |
+ |
+ if n := from.out[to]; n != nil { |
+ n.weight += w |
+ if n.residual && !residual { |
+ n.residual = false |
+ } |
+ return |
+ } |
+ |
+ info := &edgeInfo{src: from, dest: to, weight: w, residual: residual} |
+ from.out[to] = info |
+ to.in[from] = info |
+} |
+ |
+// Output formats. |
+const ( |
+ Proto = iota |
+ Dot |
+ Tags |
+ Tree |
+ Text |
+ Raw |
+ Dis |
+ List |
+ WebList |
+ Callgrind |
+) |
+ |
+// Options are the formatting and filtering options used to generate a |
+// profile. |
+type Options struct { |
+ OutputFormat int |
+ |
+ CumSort bool |
+ CallTree bool |
+ PrintAddresses bool |
+ DropNegative bool |
+ Ratio float64 |
+ |
+ NodeCount int |
+ NodeFraction float64 |
+ EdgeFraction float64 |
+ |
+ SampleType string |
+ SampleUnit string // Unit for the sample data from the profile. |
+ OutputUnit string // Units for data formatting in report. |
+ |
+ Symbol *regexp.Regexp // Symbols to include on disassembly report. |
+} |
+ |
+// newGraph summarizes performance data from a profile into a graph. |
+func newGraph(rpt *Report) (g graph, err error) { |
+ prof := rpt.prof |
+ o := rpt.options |
+ |
+ // Generate a tree for graphical output if requested. |
+ buildTree := o.CallTree && o.OutputFormat == Dot |
+ |
+ locations := make(map[uint64][]nodeInfo) |
+ for _, l := range prof.Location { |
+ locations[l.ID] = newLocInfo(l) |
+ } |
+ |
+ nm := make(nodeMap) |
+ for _, sample := range prof.Sample { |
+ if sample.Location == nil { |
+ continue |
+ } |
+ |
+ // Construct list of node names for sample. |
+ var stack []nodeInfo |
+ for _, loc := range sample.Location { |
+ id := loc.ID |
+ stack = append(stack, locations[id]...) |
+ } |
+ |
+ // Upfront pass to update the parent chains, to prevent the |
+ // merging of nodes with different parents. |
+ if buildTree { |
+ var nn *node |
+ for i := len(stack); i > 0; i-- { |
+ n := &stack[i-1] |
+ n.parent = nn |
+ nn = nm.findOrInsertNode(*n) |
+ } |
+ } |
+ |
+ leaf := nm.findOrInsertNode(stack[0]) |
+ weight := rpt.sampleValue(sample) |
+ leaf.addTags(sample, weight) |
+ |
+ // Aggregate counter data. |
+ leaf.flat += weight |
+ seen := make(map[*node]bool) |
+ var nn *node |
+ for _, s := range stack { |
+ n := nm.findOrInsertNode(s) |
+ if !seen[n] { |
+ seen[n] = true |
+ n.cum += weight |
+ |
+ if nn != nil { |
+ bumpWeight(n, nn, weight, false) |
+ } |
+ } |
+ nn = n |
+ } |
+ } |
+ |
+ // Collect new nodes into a report. |
+ ns := make(nodes, 0, len(nm)) |
+ for _, n := range nm { |
+ if rpt.options.DropNegative && n.flat < 0 { |
+ continue |
+ } |
+ ns = append(ns, n) |
+ } |
+ |
+ return graph{ns}, nil |
+} |
+ |
+// Create a slice of formatted names for a location. |
+func newLocInfo(l *profile.Location) []nodeInfo { |
+ var objfile string |
+ |
+ if m := l.Mapping; m != nil { |
+ objfile = filepath.Base(m.File) |
+ } |
+ |
+ if len(l.Line) == 0 { |
+ return []nodeInfo{ |
+ { |
+ address: l.Address, |
+ objfile: objfile, |
+ }, |
+ } |
+ } |
+ var info []nodeInfo |
+ numInlineFrames := len(l.Line) - 1 |
+ for li, line := range l.Line { |
+ ni := nodeInfo{ |
+ address: l.Address, |
+ lineno: int(line.Line), |
+ inline: li < numInlineFrames, |
+ objfile: objfile, |
+ } |
+ |
+ if line.Function != nil { |
+ ni.name = line.Function.Name |
+ ni.origName = line.Function.SystemName |
+ ni.file = line.Function.Filename |
+ ni.startLine = int(line.Function.StartLine) |
+ } |
+ |
+ info = append(info, ni) |
+ } |
+ return info |
+} |
+ |
+// nodeMap maps from a node info struct to a node. It is used to merge |
+// report entries with the same info. |
+type nodeMap map[nodeInfo]*node |
+ |
+func (m nodeMap) findOrInsertNode(info nodeInfo) *node { |
+ rr := m[info] |
+ if rr == nil { |
+ rr = &node{ |
+ info: info, |
+ in: make(edgeMap), |
+ out: make(edgeMap), |
+ tags: make(map[string]*tag), |
+ } |
+ m[info] = rr |
+ } |
+ return rr |
+} |
+ |
+// preprocess does any required filtering/sorting according to the |
+// report options. Returns the mapping from each node to any nodes |
+// removed by path compression and statistics on the nodes/edges removed. |
+func (g *graph) preprocess(rpt *Report) (origCount, droppedNodes, droppedEdges int) { |
+ o := rpt.options |
+ |
+ // Compute total weight of current set of nodes. |
+ // This is <= rpt.total because of node filtering. |
+ var totalValue int64 |
+ for _, n := range g.ns { |
+ totalValue += n.flat |
+ } |
+ |
+ // Remove nodes with value <= total*nodeFraction |
+ if nodeFraction := o.NodeFraction; nodeFraction > 0 { |
+ var removed nodes |
+ minValue := int64(float64(totalValue) * nodeFraction) |
+ kept := make(nodes, 0, len(g.ns)) |
+ for _, n := range g.ns { |
+ if n.cum < minValue { |
+ removed = append(removed, n) |
+ } else { |
+ kept = append(kept, n) |
+ tagsKept := make(map[string]*tag) |
+ for s, t := range n.tags { |
+ if t.weight >= minValue { |
+ tagsKept[s] = t |
+ } |
+ } |
+ n.tags = tagsKept |
+ } |
+ } |
+ droppedNodes = len(removed) |
+ removeNodes(removed, false, false) |
+ g.ns = kept |
+ } |
+ |
+ // Remove edges below minimum frequency. |
+ if edgeFraction := o.EdgeFraction; edgeFraction > 0 { |
+ minEdge := int64(float64(totalValue) * edgeFraction) |
+ for _, n := range g.ns { |
+ for src, e := range n.in { |
+ if e.weight < minEdge { |
+ delete(n.in, src) |
+ delete(src.out, n) |
+ droppedEdges++ |
+ } |
+ } |
+ } |
+ } |
+ |
+ sortOrder := flatName |
+ if o.CumSort { |
+ // Force cum sorting for graph output, to preserve connectivity. |
+ sortOrder = cumName |
+ } |
+ |
+ // Nodes that have flat==0 and a single in/out do not provide much |
+ // information. Give them first chance to be removed. Do not consider edges |
+ // from/to nodes that are expected to be removed. |
+ maxNodes := o.NodeCount |
+ if o.OutputFormat == Dot { |
+ if maxNodes > 0 && maxNodes < len(g.ns) { |
+ sortOrder = cumName |
+ g.ns.sort(cumName) |
+ cumCutoff := g.ns[maxNodes].cum |
+ for _, n := range g.ns { |
+ if n.flat == 0 { |
+ if count := countEdges(n.out, cumCutoff); count > 1 { |
+ continue |
+ } |
+ if count := countEdges(n.in, cumCutoff); count != 1 { |
+ continue |
+ } |
+ n.info.lowPriority = true |
+ } |
+ } |
+ } |
+ } |
+ |
+ g.ns.sort(sortOrder) |
+ if maxNodes > 0 { |
+ origCount = len(g.ns) |
+ for index, nodes := 0, 0; index < len(g.ns); index++ { |
+ nodes++ |
+ // For DOT output, count the tags as nodes since we will draw |
+ // boxes for them. |
+ if o.OutputFormat == Dot { |
+ nodes += len(g.ns[index].tags) |
+ } |
+ if nodes > maxNodes { |
+ // Trim to the top n nodes. Create dotted edges to bridge any |
+ // broken connections. |
+ removeNodes(g.ns[index:], true, true) |
+ g.ns = g.ns[:index] |
+ break |
+ } |
+ } |
+ } |
+ removeRedundantEdges(g.ns) |
+ |
+ // Select best unit for profile output. |
+ // Find the appropriate units for the smallest non-zero sample |
+ if o.OutputUnit == "minimum" && len(g.ns) > 0 { |
+ var maxValue, minValue int64 |
+ |
+ for _, n := range g.ns { |
+ if n.flat > 0 && (minValue == 0 || n.flat < minValue) { |
+ minValue = n.flat |
+ } |
+ if n.cum > maxValue { |
+ maxValue = n.cum |
+ } |
+ } |
+ if r := o.Ratio; r > 0 && r != 1 { |
+ minValue = int64(float64(minValue) * r) |
+ maxValue = int64(float64(maxValue) * r) |
+ } |
+ |
+ _, minUnit := ScaleValue(minValue, o.SampleUnit, "minimum") |
+ _, maxUnit := ScaleValue(maxValue, o.SampleUnit, "minimum") |
+ |
+ unit := minUnit |
+ if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind { |
+ // Minimum and maximum values have different units. Scale |
+ // minimum by 100 to use larger units, allowing minimum value to |
+ // be scaled down to 0.01, except for callgrind reports since |
+ // they can only represent integer values. |
+ _, unit = ScaleValue(100*minValue, o.SampleUnit, "minimum") |
+ } |
+ |
+ if unit != "" { |
+ o.OutputUnit = unit |
+ } else { |
+ o.OutputUnit = o.SampleUnit |
+ } |
+ } |
+ return |
+} |
+ |
+// countEdges counts the number of edges below the specified cutoff. |
+func countEdges(el edgeMap, cutoff int64) int { |
+ count := 0 |
+ for _, e := range el { |
+ if e.weight > cutoff { |
+ count++ |
+ } |
+ } |
+ return count |
+} |
+ |
+// removeNodes removes nodes from a report, optionally bridging |
+// connections between in/out edges and spreading out their weights |
+// proportionally. residual marks new bridge edges as residual |
+// (dotted). |
+func removeNodes(toRemove nodes, bridge, residual bool) { |
+ for _, n := range toRemove { |
+ for ei := range n.in { |
+ delete(ei.out, n) |
+ } |
+ if bridge { |
+ for ei, wi := range n.in { |
+ for eo, wo := range n.out { |
+ var weight int64 |
+ if n.cum != 0 { |
+ weight = int64(float64(wo.weight) * (float64(wi.weight) / float64(n.cum))) |
+ } |
+ bumpWeight(ei, eo, weight, residual) |
+ } |
+ } |
+ } |
+ for eo := range n.out { |
+ delete(eo.in, n) |
+ } |
+ } |
+} |
+ |
+// removeRedundantEdges removes residual edges if the destination can |
+// be reached through another path. This is done to simplify the graph |
+// while preserving connectivity. |
+func removeRedundantEdges(ns nodes) { |
+ // Walk the nodes and outgoing edges in reverse order to prefer |
+ // removing edges with the lowest weight. |
+ for i := len(ns); i > 0; i-- { |
+ n := ns[i-1] |
+ in := sortedEdges(n.in) |
+ for j := len(in); j > 0; j-- { |
+ if e := in[j-1]; e.residual && isRedundant(e) { |
+ delete(e.src.out, e.dest) |
+ delete(e.dest.in, e.src) |
+ } |
+ } |
+ } |
+} |
+ |
+// isRedundant determines if an edge can be removed without impacting |
+// connectivity of the whole graph. This is implemented by checking if the |
+// nodes have a common ancestor after removing the edge. |
+func isRedundant(e *edgeInfo) bool { |
+ destPred := predecessors(e, e.dest) |
+ if len(destPred) == 1 { |
+ return false |
+ } |
+ srcPred := predecessors(e, e.src) |
+ |
+ for n := range srcPred { |
+ if destPred[n] && n != e.dest { |
+ return true |
+ } |
+ } |
+ return false |
+} |
+ |
+// predecessors collects all the predecessors to node n, excluding edge e. |
+func predecessors(e *edgeInfo, n *node) map[*node]bool { |
+ seen := map[*node]bool{n: true} |
+ queue := []*node{n} |
+ for len(queue) > 0 { |
+ n := queue[0] |
+ queue = queue[1:] |
+ for _, ie := range n.in { |
+ if e == ie || seen[ie.src] { |
+ continue |
+ } |
+ seen[ie.src] = true |
+ queue = append(queue, ie.src) |
+ } |
+ } |
+ return seen |
+} |
+ |
+// nodeSorter is a mechanism used to allow a report to be sorted |
+// in different ways. |
+type nodeSorter struct { |
+ rs nodes |
+ less func(i, j int) bool |
+} |
+ |
+func (s nodeSorter) Len() int { return len(s.rs) } |
+func (s nodeSorter) Swap(i, j int) { s.rs[i], s.rs[j] = s.rs[j], s.rs[i] } |
+func (s nodeSorter) Less(i, j int) bool { return s.less(i, j) } |
+ |
+type nodeOrder int |
+ |
+const ( |
+ flatName nodeOrder = iota |
+ flatCumName |
+ cumName |
+ nameOrder |
+ fileOrder |
+ addressOrder |
+) |
+ |
+// sort reoders the entries in a report based on the specified |
+// ordering criteria. The result is sorted in decreasing order for |
+// numeric quantities, alphabetically for text, and increasing for |
+// addresses. |
+func (ns nodes) sort(o nodeOrder) error { |
+ var s nodeSorter |
+ |
+ switch o { |
+ case flatName: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ if iv, jv := ns[i].flat, ns[j].flat; iv != jv { |
+ return iv > jv |
+ } |
+ if ns[i].info.prettyName() != ns[j].info.prettyName() { |
+ return ns[i].info.prettyName() < ns[j].info.prettyName() |
+ } |
+ iv, jv := ns[i].cum, ns[j].cum |
+ return iv > jv |
+ }, |
+ } |
+ case flatCumName: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ if iv, jv := ns[i].flat, ns[j].flat; iv != jv { |
+ return iv > jv |
+ } |
+ if iv, jv := ns[i].cum, ns[j].cum; iv != jv { |
+ return iv > jv |
+ } |
+ return ns[i].info.prettyName() < ns[j].info.prettyName() |
+ }, |
+ } |
+ case cumName: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ if ns[i].info.lowPriority != ns[j].info.lowPriority { |
+ return ns[j].info.lowPriority |
+ } |
+ if iv, jv := ns[i].cum, ns[j].cum; iv != jv { |
+ return iv > jv |
+ } |
+ if ns[i].info.prettyName() != ns[j].info.prettyName() { |
+ return ns[i].info.prettyName() < ns[j].info.prettyName() |
+ } |
+ iv, jv := ns[i].flat, ns[j].flat |
+ return iv > jv |
+ }, |
+ } |
+ case nameOrder: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ return ns[i].info.name < ns[j].info.name |
+ }, |
+ } |
+ case fileOrder: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ return ns[i].info.file < ns[j].info.file |
+ }, |
+ } |
+ case addressOrder: |
+ s = nodeSorter{ns, |
+ func(i, j int) bool { |
+ return ns[i].info.address < ns[j].info.address |
+ }, |
+ } |
+ default: |
+ return fmt.Errorf("report: unrecognized sort ordering: %d", o) |
+ } |
+ sort.Sort(s) |
+ return nil |
+} |
+ |
+type edgeList []*edgeInfo |
+ |
+// sortedEdges return a slice of the edges in the map, sorted for |
+// visualization. The sort order is first based on the edge weight |
+// (higher-to-lower) and then by the node names to avoid flakiness. |
+func sortedEdges(edges map[*node]*edgeInfo) edgeList { |
+ el := make(edgeList, 0, len(edges)) |
+ for _, w := range edges { |
+ el = append(el, w) |
+ } |
+ |
+ sort.Sort(el) |
+ return el |
+} |
+ |
+func (el edgeList) Len() int { |
+ return len(el) |
+} |
+ |
+func (el edgeList) Less(i, j int) bool { |
+ if el[i].weight != el[j].weight { |
+ return el[i].weight > el[j].weight |
+ } |
+ |
+ from1 := el[i].src.info.prettyName() |
+ from2 := el[j].src.info.prettyName() |
+ if from1 != from2 { |
+ return from1 < from2 |
+ } |
+ |
+ to1 := el[i].dest.info.prettyName() |
+ to2 := el[j].dest.info.prettyName() |
+ |
+ return to1 < to2 |
+} |
+ |
+func (el edgeList) Swap(i, j int) { |
+ el[i], el[j] = el[j], el[i] |
+} |
+ |
+func (el edgeList) sum() int64 { |
+ var ret int64 |
+ for _, e := range el { |
+ ret += e.weight |
+ } |
+ return ret |
+} |
+ |
+// ScaleValue reformats a value from a unit to a different unit. |
+func ScaleValue(value int64, fromUnit, toUnit string) (sv float64, su string) { |
+ // Avoid infinite recursion on overflow. |
+ if value < 0 && -value > 0 { |
+ v, u := ScaleValue(-value, fromUnit, toUnit) |
+ return -v, u |
+ } |
+ if m, u, ok := memoryLabel(value, fromUnit, toUnit); ok { |
+ return m, u |
+ } |
+ if t, u, ok := timeLabel(value, fromUnit, toUnit); ok { |
+ return t, u |
+ } |
+ // Skip non-interesting units. |
+ switch toUnit { |
+ case "count", "sample", "unit", "minimum": |
+ return float64(value), "" |
+ default: |
+ return float64(value), toUnit |
+ } |
+} |
+ |
+func scaledValueLabel(value int64, fromUnit, toUnit string) string { |
+ v, u := ScaleValue(value, fromUnit, toUnit) |
+ |
+ sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00") |
+ if sv == "0" || sv == "-0" { |
+ return "0" |
+ } |
+ return sv + u |
+} |
+ |
+func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { |
+ fromUnit = strings.TrimSuffix(strings.ToLower(fromUnit), "s") |
+ toUnit = strings.TrimSuffix(strings.ToLower(toUnit), "s") |
+ |
+ switch fromUnit { |
+ case "byte", "b": |
+ case "kilobyte", "kb": |
+ value *= 1024 |
+ case "megabyte", "mb": |
+ value *= 1024 * 1024 |
+ case "gigabyte", "gb": |
+ value *= 1024 * 1024 |
+ default: |
+ return 0, "", false |
+ } |
+ |
+ if toUnit == "minimum" || toUnit == "auto" { |
+ switch { |
+ case value < 1024: |
+ toUnit = "b" |
+ case value < 1024*1024: |
+ toUnit = "kb" |
+ case value < 1024*1024*1024: |
+ toUnit = "mb" |
+ default: |
+ toUnit = "gb" |
+ } |
+ } |
+ |
+ var output float64 |
+ switch toUnit { |
+ default: |
+ output, toUnit = float64(value), "B" |
+ case "kb", "kbyte", "kilobyte": |
+ output, toUnit = float64(value)/1024, "kB" |
+ case "mb", "mbyte", "megabyte": |
+ output, toUnit = float64(value)/(1024*1024), "MB" |
+ case "gb", "gbyte", "giggabyte": |
+ output, toUnit = float64(value)/(1024*1024*1024), "GB" |
+ } |
+ return output, toUnit, true |
+} |
+ |
+func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { |
+ fromUnit = strings.ToLower(fromUnit) |
+ if len(fromUnit) > 2 { |
+ fromUnit = strings.TrimSuffix(fromUnit, "s") |
+ } |
+ |
+ toUnit = strings.ToLower(toUnit) |
+ if len(toUnit) > 2 { |
+ toUnit = strings.TrimSuffix(toUnit, "s") |
+ } |
+ |
+ var d time.Duration |
+ switch fromUnit { |
+ case "nanosecond", "ns": |
+ d = time.Duration(value) * time.Nanosecond |
+ case "microsecond": |
+ d = time.Duration(value) * time.Microsecond |
+ case "millisecond", "ms": |
+ d = time.Duration(value) * time.Millisecond |
+ case "second", "sec": |
+ d = time.Duration(value) * time.Second |
+ case "cycle": |
+ return float64(value), "", true |
+ default: |
+ return 0, "", false |
+ } |
+ |
+ if toUnit == "minimum" || toUnit == "auto" { |
+ switch { |
+ case d < 1*time.Microsecond: |
+ toUnit = "ns" |
+ case d < 1*time.Millisecond: |
+ toUnit = "us" |
+ case d < 1*time.Second: |
+ toUnit = "ms" |
+ case d < 1*time.Minute: |
+ toUnit = "sec" |
+ case d < 1*time.Hour: |
+ toUnit = "min" |
+ case d < 24*time.Hour: |
+ toUnit = "hour" |
+ case d < 15*24*time.Hour: |
+ toUnit = "day" |
+ case d < 120*24*time.Hour: |
+ toUnit = "week" |
+ default: |
+ toUnit = "year" |
+ } |
+ } |
+ |
+ var output float64 |
+ dd := float64(d) |
+ switch toUnit { |
+ case "ns", "nanosecond": |
+ output, toUnit = dd/float64(time.Nanosecond), "ns" |
+ case "us", "microsecond": |
+ output, toUnit = dd/float64(time.Microsecond), "us" |
+ case "ms", "millisecond": |
+ output, toUnit = dd/float64(time.Millisecond), "ms" |
+ case "min", "minute": |
+ output, toUnit = dd/float64(time.Minute), "mins" |
+ case "hour", "hr": |
+ output, toUnit = dd/float64(time.Hour), "hrs" |
+ case "day": |
+ output, toUnit = dd/float64(24*time.Hour), "days" |
+ case "week", "wk": |
+ output, toUnit = dd/float64(7*24*time.Hour), "wks" |
+ case "year", "yr": |
+ output, toUnit = dd/float64(365*7*24*time.Hour), "yrs" |
+ default: |
+ fallthrough |
+ case "sec", "second", "s": |
+ output, toUnit = dd/float64(time.Second), "s" |
+ } |
+ return output, toUnit, true |
+} |
+ |
+// prettyName determines the printable name to be used for a node. |
+func (info *nodeInfo) prettyName() string { |
+ var name string |
+ if info.address != 0 { |
+ name = fmt.Sprintf("%016x", info.address) |
+ } |
+ |
+ if info.name != "" { |
+ name = name + " " + info.name |
+ } |
+ |
+ if info.file != "" { |
+ name += " " + trimPath(info.file) |
+ if info.lineno != 0 { |
+ name += fmt.Sprintf(":%d", info.lineno) |
+ } |
+ } |
+ |
+ if info.inline { |
+ name = name + " (inline)" |
+ } |
+ |
+ if name = strings.TrimSpace(name); name == "" && info.objfile != "" { |
+ name = "[" + info.objfile + "]" |
+ } |
+ return name |
+} |
+ |
+// New builds a new report indexing the sample values interpreting the |
+// samples with the provided function. |
+func New(prof *profile.Profile, options Options, value func(s *profile.Sample) int64, unit string) *Report { |
+ o := &options |
+ if o.SampleUnit == "" { |
+ o.SampleUnit = unit |
+ } |
+ format := func(v int64) string { |
+ if r := o.Ratio; r > 0 && r != 1 { |
+ fv := float64(v) * r |
+ v = int64(fv) |
+ } |
+ return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) |
+ } |
+ return &Report{prof, computeTotal(prof, value), o, value, format} |
+} |
+ |
+// NewDefault builds a new report indexing the sample values with the |
+// last value available. |
+func NewDefault(prof *profile.Profile, options Options) *Report { |
+ index := len(prof.SampleType) - 1 |
+ o := &options |
+ if o.SampleUnit == "" { |
+ o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) |
+ } |
+ value := func(s *profile.Sample) int64 { |
+ return s.Value[index] |
+ } |
+ format := func(v int64) string { |
+ if r := o.Ratio; r > 0 && r != 1 { |
+ fv := float64(v) * r |
+ v = int64(fv) |
+ } |
+ return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) |
+ } |
+ return &Report{prof, computeTotal(prof, value), o, value, format} |
+} |
+ |
+func computeTotal(prof *profile.Profile, value func(s *profile.Sample) int64) int64 { |
+ var ret int64 |
+ for _, sample := range prof.Sample { |
+ ret += value(sample) |
+ } |
+ return ret |
+} |
+ |
+// Report contains the data and associated routines to extract a |
+// report from a profile. |
+type Report struct { |
+ prof *profile.Profile |
+ total int64 |
+ options *Options |
+ sampleValue func(*profile.Sample) int64 |
+ formatValue func(int64) string |
+} |
+ |
+func (rpt *Report) formatTags(s *profile.Sample) (string, bool) { |
+ var labels []string |
+ for key, vals := range s.Label { |
+ for _, v := range vals { |
+ labels = append(labels, key+":"+v) |
+ } |
+ } |
+ for key, nvals := range s.NumLabel { |
+ for _, v := range nvals { |
+ labels = append(labels, scaledValueLabel(v, key, "auto")) |
+ } |
+ } |
+ if len(labels) == 0 { |
+ return "", false |
+ } |
+ sort.Strings(labels) |
+ return strings.Join(labels, `\n`), true |
+} |