diff options
Diffstat (limited to 'src/cmd/pprof/internal/report')
-rw-r--r-- | src/cmd/pprof/internal/report/report.go | 1718 | ||||
-rw-r--r-- | src/cmd/pprof/internal/report/source.go | 450 | ||||
-rw-r--r-- | src/cmd/pprof/internal/report/source_html.go | 77 |
3 files changed, 2245 insertions, 0 deletions
diff --git a/src/cmd/pprof/internal/report/report.go b/src/cmd/pprof/internal/report/report.go new file mode 100644 index 000000000..e5977fd03 --- /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 +} diff --git a/src/cmd/pprof/internal/report/source.go b/src/cmd/pprof/internal/report/source.go new file mode 100644 index 000000000..57300dd91 --- /dev/null +++ b/src/cmd/pprof/internal/report/source.go @@ -0,0 +1,450 @@ +// 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 + +// This file contains routines related to the generation of annotated +// source listings. + +import ( + "bufio" + "fmt" + "html/template" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "cmd/pprof/internal/plugin" +) + +// printSource prints an annotated source listing, include all +// functions with samples that match the regexp rpt.options.symbol. +// The sources are sorted by function name and then by filename to +// eliminate potential nondeterminism. +func printSource(w io.Writer, rpt *Report) error { + o := rpt.options + g, err := newGraph(rpt) + if err != nil { + return err + } + + // Identify all the functions that match the regexp provided. + // Group nodes for each matching function. + var functions nodes + functionNodes := make(map[string]nodes) + for _, n := range g.ns { + if !o.Symbol.MatchString(n.info.name) { + continue + } + if functionNodes[n.info.name] == nil { + functions = append(functions, n) + } + functionNodes[n.info.name] = append(functionNodes[n.info.name], n) + } + functions.sort(nameOrder) + + fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) + for _, fn := range functions { + name := fn.info.name + + // Identify all the source files associated to this function. + // Group nodes for each source file. + var sourceFiles nodes + fileNodes := make(map[string]nodes) + for _, n := range functionNodes[name] { + if n.info.file == "" { + continue + } + if fileNodes[n.info.file] == nil { + sourceFiles = append(sourceFiles, n) + } + fileNodes[n.info.file] = append(fileNodes[n.info.file], n) + } + + if len(sourceFiles) == 0 { + fmt.Printf("No source information for %s\n", name) + continue + } + + sourceFiles.sort(fileOrder) + + // Print each file associated with this function. + for _, fl := range sourceFiles { + filename := fl.info.file + fns := fileNodes[filename] + flatSum, cumSum := sumNodes(fns) + + fnodes, path, err := getFunctionSource(name, filename, fns, 0, 0) + fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, path) + fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", + rpt.formatValue(flatSum), rpt.formatValue(cumSum), + percentage(cumSum, rpt.total)) + + if err != nil { + fmt.Fprintf(w, " Error: %v\n", err) + continue + } + + for _, fn := range fnodes { + fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), fn.info.lineno, fn.info.name) + } + } + } + return nil +} + +// printWebSource prints an annotated source listing, include all +// functions with samples that match the regexp rpt.options.symbol. +func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error { + o := rpt.options + g, err := newGraph(rpt) + if err != nil { + return err + } + + // 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 + } + + // Extract interesting symbols from binary files in the profile and + // classify samples per symbol. + symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj) + symNodes := nodesPerSymbol(g.ns, symbols) + + // Sort symbols for printing. + var syms objSymbols + for s := range symNodes { + syms = append(syms, s) + } + sort.Sort(syms) + + if len(syms) == 0 { + return fmt.Errorf("no samples found on routines matching: %s", o.Symbol.String()) + } + + printHeader(w, rpt) + for _, s := range syms { + name := s.sym.Name[0] + // Identify sources associated to a symbol by examining + // symbol samples. Classify samples per source file. + var sourceFiles nodes + fileNodes := make(map[string]nodes) + for _, n := range symNodes[s] { + if n.info.file == "" { + continue + } + if fileNodes[n.info.file] == nil { + sourceFiles = append(sourceFiles, n) + } + fileNodes[n.info.file] = append(fileNodes[n.info.file], n) + } + + if len(sourceFiles) == 0 { + fmt.Printf("No source information for %s\n", name) + continue + } + + sourceFiles.sort(fileOrder) + + // Print each file associated with this function. + for _, fl := range sourceFiles { + filename := fl.info.file + fns := fileNodes[filename] + + asm := assemblyPerSourceLine(symbols, fns, filename, obj) + start, end := sourceCoordinates(asm) + + fnodes, path, err := getFunctionSource(name, filename, fns, start, end) + if err != nil { + fnodes, path = getMissingFunctionSource(filename, asm, start, end) + } + + flatSum, cumSum := sumNodes(fnodes) + printFunctionHeader(w, name, path, flatSum, cumSum, rpt) + for _, fn := range fnodes { + printFunctionSourceLine(w, fn, asm[fn.info.lineno], rpt) + } + printFunctionClosing(w) + } + } + printPageClosing(w) + return nil +} + +// sourceCoordinates returns the lowest and highest line numbers from +// a set of assembly statements. +func sourceCoordinates(asm map[int]nodes) (start, end int) { + for l := range asm { + if start == 0 || l < start { + start = l + } + if end == 0 || l > end { + end = l + } + } + return start, end +} + +// assemblyPerSourceLine disassembles the binary containing a symbol +// and classifies the assembly instructions according to its +// corresponding source line, annotating them with a set of samples. +func assemblyPerSourceLine(objSyms []*objSymbol, rs nodes, src string, obj plugin.ObjTool) map[int]nodes { + assembly := make(map[int]nodes) + // Identify symbol to use for this collection of samples. + o := findMatchingSymbol(objSyms, rs) + if o == nil { + return assembly + } + + // Extract assembly for matched symbol + insns, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End) + if err != nil { + return assembly + } + + srcBase := filepath.Base(src) + anodes := annotateAssembly(insns, rs, o.base) + var lineno = 0 + for _, an := range anodes { + if filepath.Base(an.info.file) == srcBase { + lineno = an.info.lineno + } + if lineno != 0 { + assembly[lineno] = append(assembly[lineno], an) + } + } + + return assembly +} + +// findMatchingSymbol looks for the symbol that corresponds to a set +// of samples, by comparing their addresses. +func findMatchingSymbol(objSyms []*objSymbol, ns nodes) *objSymbol { + for _, n := range ns { + for _, o := range objSyms { + if filepath.Base(o.sym.File) == n.info.objfile && + o.sym.Start <= n.info.address-o.base && + n.info.address-o.base <= o.sym.End { + return o + } + } + } + return nil +} + +// printHeader prints the page header for a weblist report. +func printHeader(w io.Writer, rpt *Report) { + fmt.Fprintln(w, weblistPageHeader) + + var labels []string + for _, l := range legendLabels(rpt) { + labels = append(labels, template.HTMLEscapeString(l)) + } + + fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`, + strings.Join(labels, "<br>\n"), + rpt.formatValue(rpt.total), + ) +} + +// printFunctionHeader prints a function header for a weblist report. +func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) { + fmt.Fprintf(w, `<h1>%s</h1>%s +<pre onClick="pprof_toggle_asm()"> + Total: %10s %10s (flat, cum) %s +`, + template.HTMLEscapeString(name), template.HTMLEscapeString(path), + rpt.formatValue(flatSum), rpt.formatValue(cumSum), + percentage(cumSum, rpt.total)) +} + +// printFunctionSourceLine prints a source line and the corresponding assembly. +func printFunctionSourceLine(w io.Writer, fn *node, assembly nodes, rpt *Report) { + if len(assembly) == 0 { + fmt.Fprintf(w, + "<span class=line> %6d</span> <span class=nop> %10s %10s %s </span>\n", + fn.info.lineno, + valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), + template.HTMLEscapeString(fn.info.name)) + return + } + + fmt.Fprintf(w, + "<span class=line> %6d</span> <span class=deadsrc> %10s %10s %s </span>", + fn.info.lineno, + valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), + template.HTMLEscapeString(fn.info.name)) + fmt.Fprint(w, "<span class=asm>") + for _, an := range assembly { + var fileline string + class := "disasmloc" + if an.info.file != "" { + fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.info.file), an.info.lineno) + if an.info.lineno != fn.info.lineno { + class = "unimportant" + } + } + fmt.Fprintf(w, " %8s %10s %10s %8x: %-48s <span class=%s>%s</span>\n", "", + valueOrDot(an.flat, rpt), valueOrDot(an.cum, rpt), + an.info.address, + template.HTMLEscapeString(an.info.name), + class, + template.HTMLEscapeString(fileline)) + } + fmt.Fprintln(w, "</span>") +} + +// printFunctionClosing prints the end of a function in a weblist report. +func printFunctionClosing(w io.Writer) { + fmt.Fprintln(w, "</pre>") +} + +// printPageClosing prints the end of the page in a weblist report. +func printPageClosing(w io.Writer) { + fmt.Fprintln(w, weblistPageClosing) +} + +// getFunctionSource collects the sources of a function from a source +// file and annotates it with the samples in fns. Returns the sources +// as nodes, using the info.name field to hold the source code. +func getFunctionSource(fun, file string, fns nodes, start, end int) (nodes, string, error) { + f, file, err := adjustSourcePath(file) + if err != nil { + return nil, file, err + } + + lineNodes := make(map[int]nodes) + + // Collect source coordinates from profile. + const margin = 5 // Lines before first/after last sample. + if start == 0 { + if fns[0].info.startLine != 0 { + start = fns[0].info.startLine + } else { + start = fns[0].info.lineno - margin + } + } else { + start -= margin + } + if end == 0 { + end = fns[0].info.lineno + } + end += margin + for _, n := range fns { + lineno := n.info.lineno + nodeStart := n.info.startLine + if nodeStart == 0 { + nodeStart = lineno - margin + } + nodeEnd := lineno + margin + if nodeStart < start { + start = nodeStart + } else if nodeEnd > end { + end = nodeEnd + } + lineNodes[lineno] = append(lineNodes[lineno], n) + } + + var src nodes + buf := bufio.NewReader(f) + lineno := 1 + for { + line, err := buf.ReadString('\n') + if err != nil { + if line == "" || err != io.EOF { + return nil, file, err + } + } + if lineno >= start { + flat, cum := sumNodes(lineNodes[lineno]) + + src = append(src, &node{ + info: nodeInfo{ + name: strings.TrimRight(line, "\n"), + lineno: lineno, + }, + flat: flat, + cum: cum, + }) + } + lineno++ + if lineno > end { + break + } + } + return src, file, nil +} + +// getMissingFunctionSource creates a dummy function body to point to +// the source file and annotates it with the samples in asm. +func getMissingFunctionSource(filename string, asm map[int]nodes, start, end int) (nodes, string) { + var fnodes nodes + for i := start; i <= end; i++ { + lrs := asm[i] + if len(lrs) == 0 { + continue + } + flat, cum := sumNodes(lrs) + fnodes = append(fnodes, &node{ + info: nodeInfo{ + name: "???", + lineno: i, + }, + flat: flat, + cum: cum, + }) + } + return fnodes, filename +} + +// adjustSourcePath adjusts the pathe for a source file by trimmming +// known prefixes and searching for the file on all parents of the +// current working dir. +func adjustSourcePath(path string) (*os.File, string, error) { + path = trimPath(path) + f, err := os.Open(path) + if err == nil { + return f, path, nil + } + + if dir, wderr := os.Getwd(); wderr == nil { + for { + parent := filepath.Dir(dir) + if parent == dir { + break + } + if f, err := os.Open(filepath.Join(parent, path)); err == nil { + return f, filepath.Join(parent, path), nil + } + + dir = parent + } + } + + return nil, path, err +} + +// trimPath cleans up a path by removing prefixes that are commonly +// found on profiles. +func trimPath(path string) string { + basePaths := []string{ + "/proc/self/cwd/./", + "/proc/self/cwd/", + } + + sPath := filepath.ToSlash(path) + + for _, base := range basePaths { + if strings.HasPrefix(sPath, base) { + return filepath.FromSlash(sPath[len(base):]) + } + } + return path +} diff --git a/src/cmd/pprof/internal/report/source_html.go b/src/cmd/pprof/internal/report/source_html.go new file mode 100644 index 000000000..267fabdc4 --- /dev/null +++ b/src/cmd/pprof/internal/report/source_html.go @@ -0,0 +1,77 @@ +// 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 + +const weblistPageHeader = ` +<!DOCTYPE html> +<html> +<head> +<title>Pprof listing</title> +<style type="text/css"> +body { +font-family: sans-serif; +} +h1 { + font-size: 1.5em; + margin-bottom: 4px; +} +.legend { + font-size: 1.25em; +} +.line { +color: #aaaaaa; +} +.nop { +color: #aaaaaa; +} +.unimportant { +color: #cccccc; +} +.disasmloc { +color: #000000; +} +.deadsrc { +cursor: pointer; +} +.deadsrc:hover { +background-color: #eeeeee; +} +.livesrc { +color: #0000ff; +cursor: pointer; +} +.livesrc:hover { +background-color: #eeeeee; +} +.asm { +color: #008800; +display: none; +} +</style> +<script type="text/javascript"> +function pprof_toggle_asm(e) { + var target; + if (!e) e = window.event; + if (e.target) target = e.target; + else if (e.srcElement) target = e.srcElement; + + if (target) { + var asm = target.nextSibling; + if (asm && asm.className == "asm") { + asm.style.display = (asm.style.display == "block" ? "" : "block"); + e.preventDefault(); + return false; + } + } +} +</script> +</head> +<body> +` + +const weblistPageClosing = ` +</body> +</html> +` |