// 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 }