github.com/euank/go@v0.0.0-20160829210321-495514729181/src/cmd/internal/pprof/report/report.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package report summarizes a performance profile into a
     6  // human-readable report.
     7  package report
     8  
     9  import (
    10  	"fmt"
    11  	"io"
    12  	"math"
    13  	"path/filepath"
    14  	"regexp"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"cmd/internal/pprof/plugin"
    21  	"cmd/internal/pprof/profile"
    22  )
    23  
    24  // Generate generates a report as directed by the Report.
    25  func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
    26  	o := rpt.options
    27  
    28  	switch o.OutputFormat {
    29  	case Dot:
    30  		return printDOT(w, rpt)
    31  	case Tree:
    32  		return printTree(w, rpt)
    33  	case Text:
    34  		return printText(w, rpt)
    35  	case Raw:
    36  		fmt.Fprint(w, rpt.prof.String())
    37  		return nil
    38  	case Tags:
    39  		return printTags(w, rpt)
    40  	case Proto:
    41  		return rpt.prof.Write(w)
    42  	case Dis:
    43  		return printAssembly(w, rpt, obj)
    44  	case List:
    45  		return printSource(w, rpt)
    46  	case WebList:
    47  		return printWebSource(w, rpt, obj)
    48  	case Callgrind:
    49  		return printCallgrind(w, rpt)
    50  	}
    51  	return fmt.Errorf("unexpected output format")
    52  }
    53  
    54  // printAssembly prints an annotated assembly listing.
    55  func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
    56  	g, err := newGraph(rpt)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	o := rpt.options
    62  	prof := rpt.prof
    63  
    64  	// If the regexp source can be parsed as an address, also match
    65  	// functions that land on that address.
    66  	var address *uint64
    67  	if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil {
    68  		address = &hex
    69  	}
    70  
    71  	fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total))
    72  	symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj)
    73  	symNodes := nodesPerSymbol(g.ns, symbols)
    74  	// Sort function names for printing.
    75  	var syms objSymbols
    76  	for s := range symNodes {
    77  		syms = append(syms, s)
    78  	}
    79  	sort.Sort(syms)
    80  
    81  	// Correlate the symbols from the binary with the profile samples.
    82  	for _, s := range syms {
    83  		sns := symNodes[s]
    84  
    85  		// Gather samples for this symbol.
    86  		flatSum, cumSum := sumNodes(sns)
    87  
    88  		// Get the function assembly.
    89  		insns, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End)
    90  		if err != nil {
    91  			return err
    92  		}
    93  
    94  		ns := annotateAssembly(insns, sns, s.base)
    95  
    96  		fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0])
    97  		for _, name := range s.sym.Name[1:] {
    98  			fmt.Fprintf(w, "    AKA ======================== %s\n", name)
    99  		}
   100  		fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
   101  			rpt.formatValue(flatSum), rpt.formatValue(cumSum),
   102  			percentage(cumSum, rpt.total))
   103  
   104  		for _, n := range ns {
   105  			fmt.Fprintf(w, "%10s %10s %10x: %s\n", valueOrDot(n.flat, rpt), valueOrDot(n.cum, rpt), n.info.address, n.info.name)
   106  		}
   107  	}
   108  	return nil
   109  }
   110  
   111  // symbolsFromBinaries examines the binaries listed on the profile
   112  // that have associated samples, and identifies symbols matching rx.
   113  func symbolsFromBinaries(prof *profile.Profile, g graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol {
   114  	hasSamples := make(map[string]bool)
   115  	// Only examine mappings that have samples that match the
   116  	// regexp. This is an optimization to speed up pprof.
   117  	for _, n := range g.ns {
   118  		if name := n.info.prettyName(); rx.MatchString(name) && n.info.objfile != "" {
   119  			hasSamples[n.info.objfile] = true
   120  		}
   121  	}
   122  
   123  	// Walk all mappings looking for matching functions with samples.
   124  	var objSyms []*objSymbol
   125  	for _, m := range prof.Mapping {
   126  		if !hasSamples[filepath.Base(m.File)] {
   127  			if address == nil || !(m.Start <= *address && *address <= m.Limit) {
   128  				continue
   129  			}
   130  		}
   131  
   132  		f, err := obj.Open(m.File, m.Start)
   133  		if err != nil {
   134  			fmt.Printf("%v\n", err)
   135  			continue
   136  		}
   137  
   138  		// Find symbols in this binary matching the user regexp.
   139  		var addr uint64
   140  		if address != nil {
   141  			addr = *address
   142  		}
   143  		msyms, err := f.Symbols(rx, addr)
   144  		base := f.Base()
   145  		f.Close()
   146  		if err != nil {
   147  			continue
   148  		}
   149  		for _, ms := range msyms {
   150  			objSyms = append(objSyms,
   151  				&objSymbol{
   152  					sym:  ms,
   153  					base: base,
   154  				},
   155  			)
   156  		}
   157  	}
   158  
   159  	return objSyms
   160  }
   161  
   162  // objSym represents a symbol identified from a binary. It includes
   163  // the SymbolInfo from the disasm package and the base that must be
   164  // added to correspond to sample addresses
   165  type objSymbol struct {
   166  	sym  *plugin.Sym
   167  	base uint64
   168  }
   169  
   170  // objSymbols is a wrapper type to enable sorting of []*objSymbol.
   171  type objSymbols []*objSymbol
   172  
   173  func (o objSymbols) Len() int {
   174  	return len(o)
   175  }
   176  
   177  func (o objSymbols) Less(i, j int) bool {
   178  	if namei, namej := o[i].sym.Name[0], o[j].sym.Name[0]; namei != namej {
   179  		return namei < namej
   180  	}
   181  	return o[i].sym.Start < o[j].sym.Start
   182  }
   183  
   184  func (o objSymbols) Swap(i, j int) {
   185  	o[i], o[j] = o[j], o[i]
   186  }
   187  
   188  // nodesPerSymbol classifies nodes into a group of symbols.
   189  func nodesPerSymbol(ns nodes, symbols []*objSymbol) map[*objSymbol]nodes {
   190  	symNodes := make(map[*objSymbol]nodes)
   191  	for _, s := range symbols {
   192  		// Gather samples for this symbol.
   193  		for _, n := range ns {
   194  			address := n.info.address - s.base
   195  			if address >= s.sym.Start && address < s.sym.End {
   196  				symNodes[s] = append(symNodes[s], n)
   197  			}
   198  		}
   199  	}
   200  	return symNodes
   201  }
   202  
   203  // annotateAssembly annotates a set of assembly instructions with a
   204  // set of samples. It returns a set of nodes to display.  base is an
   205  // offset to adjust the sample addresses.
   206  func annotateAssembly(insns []plugin.Inst, samples nodes, base uint64) nodes {
   207  	// Add end marker to simplify printing loop.
   208  	insns = append(insns, plugin.Inst{
   209  		Addr: ^uint64(0),
   210  	})
   211  
   212  	// Ensure samples are sorted by address.
   213  	samples.sort(addressOrder)
   214  
   215  	var s int
   216  	var asm nodes
   217  	for ix, in := range insns[:len(insns)-1] {
   218  		n := node{
   219  			info: nodeInfo{
   220  				address: in.Addr,
   221  				name:    in.Text,
   222  				file:    trimPath(in.File),
   223  				lineno:  in.Line,
   224  			},
   225  		}
   226  
   227  		// Sum all the samples until the next instruction (to account
   228  		// for samples attributed to the middle of an instruction).
   229  		for next := insns[ix+1].Addr; s < len(samples) && samples[s].info.address-base < next; s++ {
   230  			n.flat += samples[s].flat
   231  			n.cum += samples[s].cum
   232  			if samples[s].info.file != "" {
   233  				n.info.file = trimPath(samples[s].info.file)
   234  				n.info.lineno = samples[s].info.lineno
   235  			}
   236  		}
   237  		asm = append(asm, &n)
   238  	}
   239  
   240  	return asm
   241  }
   242  
   243  // valueOrDot formats a value according to a report, intercepting zero
   244  // values.
   245  func valueOrDot(value int64, rpt *Report) string {
   246  	if value == 0 {
   247  		return "."
   248  	}
   249  	return rpt.formatValue(value)
   250  }
   251  
   252  // printTags collects all tags referenced in the profile and prints
   253  // them in a sorted table.
   254  func printTags(w io.Writer, rpt *Report) error {
   255  	p := rpt.prof
   256  
   257  	// Hashtable to keep accumulate tags as key,value,count.
   258  	tagMap := make(map[string]map[string]int64)
   259  	for _, s := range p.Sample {
   260  		for key, vals := range s.Label {
   261  			for _, val := range vals {
   262  				if valueMap, ok := tagMap[key]; ok {
   263  					valueMap[val] = valueMap[val] + s.Value[0]
   264  					continue
   265  				}
   266  				valueMap := make(map[string]int64)
   267  				valueMap[val] = s.Value[0]
   268  				tagMap[key] = valueMap
   269  			}
   270  		}
   271  		for key, vals := range s.NumLabel {
   272  			for _, nval := range vals {
   273  				val := scaledValueLabel(nval, key, "auto")
   274  				if valueMap, ok := tagMap[key]; ok {
   275  					valueMap[val] = valueMap[val] + s.Value[0]
   276  					continue
   277  				}
   278  				valueMap := make(map[string]int64)
   279  				valueMap[val] = s.Value[0]
   280  				tagMap[key] = valueMap
   281  			}
   282  		}
   283  	}
   284  
   285  	tagKeys := make(tags, 0, len(tagMap))
   286  	for key := range tagMap {
   287  		tagKeys = append(tagKeys, &tag{name: key})
   288  	}
   289  	sort.Sort(tagKeys)
   290  
   291  	for _, tagKey := range tagKeys {
   292  		var total int64
   293  		key := tagKey.name
   294  		tags := make(tags, 0, len(tagMap[key]))
   295  		for t, c := range tagMap[key] {
   296  			total += c
   297  			tags = append(tags, &tag{name: t, weight: c})
   298  		}
   299  
   300  		sort.Sort(tags)
   301  		fmt.Fprintf(w, "%s: Total %d\n", key, total)
   302  		for _, t := range tags {
   303  			if total > 0 {
   304  				fmt.Fprintf(w, "  %8d (%s): %s\n", t.weight,
   305  					percentage(t.weight, total), t.name)
   306  			} else {
   307  				fmt.Fprintf(w, "  %8d: %s\n", t.weight, t.name)
   308  			}
   309  		}
   310  		fmt.Fprintln(w)
   311  	}
   312  	return nil
   313  }
   314  
   315  // printText prints a flat text report for a profile.
   316  func printText(w io.Writer, rpt *Report) error {
   317  	g, err := newGraph(rpt)
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	origCount, droppedNodes, _ := g.preprocess(rpt)
   323  	fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n"))
   324  
   325  	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
   326  		"flat", "flat", "sum", "cum", "cum")
   327  
   328  	var flatSum int64
   329  	for _, n := range g.ns {
   330  		name, flat, cum := n.info.prettyName(), n.flat, n.cum
   331  
   332  		flatSum += flat
   333  		fmt.Fprintf(w, "%10s %s %s %10s %s  %s\n",
   334  			rpt.formatValue(flat),
   335  			percentage(flat, rpt.total),
   336  			percentage(flatSum, rpt.total),
   337  			rpt.formatValue(cum),
   338  			percentage(cum, rpt.total),
   339  			name)
   340  	}
   341  	return nil
   342  }
   343  
   344  // printCallgrind prints a graph for a profile on callgrind format.
   345  func printCallgrind(w io.Writer, rpt *Report) error {
   346  	g, err := newGraph(rpt)
   347  	if err != nil {
   348  		return err
   349  	}
   350  
   351  	o := rpt.options
   352  	rpt.options.NodeFraction = 0
   353  	rpt.options.EdgeFraction = 0
   354  	rpt.options.NodeCount = 0
   355  
   356  	g.preprocess(rpt)
   357  
   358  	fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")")
   359  
   360  	files := make(map[string]int)
   361  	names := make(map[string]int)
   362  	for _, n := range g.ns {
   363  		fmt.Fprintln(w, "fl="+callgrindName(files, n.info.file))
   364  		fmt.Fprintln(w, "fn="+callgrindName(names, n.info.name))
   365  		sv, _ := ScaleValue(n.flat, o.SampleUnit, o.OutputUnit)
   366  		fmt.Fprintf(w, "%d %d\n", n.info.lineno, int(sv))
   367  
   368  		// Print outgoing edges.
   369  		for _, out := range sortedEdges(n.out) {
   370  			c, _ := ScaleValue(out.weight, o.SampleUnit, o.OutputUnit)
   371  			count := fmt.Sprintf("%d", int(c))
   372  			callee := out.dest
   373  			fmt.Fprintln(w, "cfl="+callgrindName(files, callee.info.file))
   374  			fmt.Fprintln(w, "cfn="+callgrindName(names, callee.info.name))
   375  			fmt.Fprintln(w, "calls="+count, callee.info.lineno)
   376  			fmt.Fprintln(w, n.info.lineno, count)
   377  		}
   378  		fmt.Fprintln(w)
   379  	}
   380  
   381  	return nil
   382  }
   383  
   384  // callgrindName implements the callgrind naming compression scheme.
   385  // For names not previously seen returns "(N) name", where N is a
   386  // unique index. For names previously seen returns "(N)" where N is
   387  // the index returned the first time.
   388  func callgrindName(names map[string]int, name string) string {
   389  	if name == "" {
   390  		return ""
   391  	}
   392  	if id, ok := names[name]; ok {
   393  		return fmt.Sprintf("(%d)", id)
   394  	}
   395  	id := len(names) + 1
   396  	names[name] = id
   397  	return fmt.Sprintf("(%d) %s", id, name)
   398  }
   399  
   400  // printTree prints a tree-based report in text form.
   401  func printTree(w io.Writer, rpt *Report) error {
   402  	const separator = "----------------------------------------------------------+-------------"
   403  	const legend = "      flat  flat%   sum%        cum   cum%   calls calls% + context 	 	 "
   404  
   405  	g, err := newGraph(rpt)
   406  	if err != nil {
   407  		return err
   408  	}
   409  
   410  	origCount, droppedNodes, _ := g.preprocess(rpt)
   411  	fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n"))
   412  
   413  	fmt.Fprintln(w, separator)
   414  	fmt.Fprintln(w, legend)
   415  	var flatSum int64
   416  
   417  	rx := rpt.options.Symbol
   418  	for _, n := range g.ns {
   419  		name, flat, cum := n.info.prettyName(), n.flat, n.cum
   420  
   421  		// Skip any entries that do not match the regexp (for the "peek" command).
   422  		if rx != nil && !rx.MatchString(name) {
   423  			continue
   424  		}
   425  
   426  		fmt.Fprintln(w, separator)
   427  		// Print incoming edges.
   428  		inEdges := sortedEdges(n.in)
   429  		inSum := inEdges.sum()
   430  		for _, in := range inEdges {
   431  			fmt.Fprintf(w, "%50s %s |   %s\n", rpt.formatValue(in.weight),
   432  				percentage(in.weight, inSum), in.src.info.prettyName())
   433  		}
   434  
   435  		// Print current node.
   436  		flatSum += flat
   437  		fmt.Fprintf(w, "%10s %s %s %10s %s                | %s\n",
   438  			rpt.formatValue(flat),
   439  			percentage(flat, rpt.total),
   440  			percentage(flatSum, rpt.total),
   441  			rpt.formatValue(cum),
   442  			percentage(cum, rpt.total),
   443  			name)
   444  
   445  		// Print outgoing edges.
   446  		outEdges := sortedEdges(n.out)
   447  		outSum := outEdges.sum()
   448  		for _, out := range outEdges {
   449  			fmt.Fprintf(w, "%50s %s |   %s\n", rpt.formatValue(out.weight),
   450  				percentage(out.weight, outSum), out.dest.info.prettyName())
   451  		}
   452  	}
   453  	if len(g.ns) > 0 {
   454  		fmt.Fprintln(w, separator)
   455  	}
   456  	return nil
   457  }
   458  
   459  // printDOT prints an annotated callgraph in DOT format.
   460  func printDOT(w io.Writer, rpt *Report) error {
   461  	g, err := newGraph(rpt)
   462  	if err != nil {
   463  		return err
   464  	}
   465  
   466  	origCount, droppedNodes, droppedEdges := g.preprocess(rpt)
   467  
   468  	prof := rpt.prof
   469  	graphname := "unnamed"
   470  	if len(prof.Mapping) > 0 {
   471  		graphname = filepath.Base(prof.Mapping[0].File)
   472  	}
   473  	fmt.Fprintln(w, `digraph "`+graphname+`" {`)
   474  	fmt.Fprintln(w, `node [style=filled fillcolor="#f8f8f8"]`)
   475  	fmt.Fprintln(w, dotLegend(rpt, g, origCount, droppedNodes, droppedEdges))
   476  
   477  	if len(g.ns) == 0 {
   478  		fmt.Fprintln(w, "}")
   479  		return nil
   480  	}
   481  
   482  	// Make sure nodes have a unique consistent id.
   483  	nodeIndex := make(map[*node]int)
   484  	maxFlat := float64(g.ns[0].flat)
   485  	for i, n := range g.ns {
   486  		nodeIndex[n] = i + 1
   487  		if float64(n.flat) > maxFlat {
   488  			maxFlat = float64(n.flat)
   489  		}
   490  	}
   491  	var edges edgeList
   492  	for _, n := range g.ns {
   493  		node := dotNode(rpt, maxFlat, nodeIndex[n], n)
   494  		fmt.Fprintln(w, node)
   495  		if nodelets := dotNodelets(rpt, nodeIndex[n], n); nodelets != "" {
   496  			fmt.Fprint(w, nodelets)
   497  		}
   498  
   499  		// Collect outgoing edges.
   500  		for _, e := range n.out {
   501  			edges = append(edges, e)
   502  		}
   503  	}
   504  	// Sort edges by frequency as a hint to the graph layout engine.
   505  	sort.Sort(edges)
   506  	for _, e := range edges {
   507  		fmt.Fprintln(w, dotEdge(rpt, nodeIndex[e.src], nodeIndex[e.dest], e))
   508  	}
   509  	fmt.Fprintln(w, "}")
   510  	return nil
   511  }
   512  
   513  // percentage computes the percentage of total of a value, and encodes
   514  // it as a string. At least two digits of precision are printed.
   515  func percentage(value, total int64) string {
   516  	var ratio float64
   517  	if total != 0 {
   518  		ratio = float64(value) / float64(total) * 100
   519  	}
   520  	switch {
   521  	case ratio >= 99.95:
   522  		return "  100%"
   523  	case ratio >= 1.0:
   524  		return fmt.Sprintf("%5.2f%%", ratio)
   525  	default:
   526  		return fmt.Sprintf("%5.2g%%", ratio)
   527  	}
   528  }
   529  
   530  // dotLegend generates the overall graph label for a report in DOT format.
   531  func dotLegend(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) string {
   532  	label := legendLabels(rpt)
   533  	label = append(label, legendDetailLabels(rpt, g, origCount, droppedNodes, droppedEdges)...)
   534  	return fmt.Sprintf(`subgraph cluster_L { L [shape=box fontsize=32 label="%s\l"] }`, strings.Join(label, `\l`))
   535  }
   536  
   537  // legendLabels generates labels exclusive to graph visualization.
   538  func legendLabels(rpt *Report) []string {
   539  	prof := rpt.prof
   540  	o := rpt.options
   541  	var label []string
   542  	if len(prof.Mapping) > 0 {
   543  		if prof.Mapping[0].File != "" {
   544  			label = append(label, "File: "+filepath.Base(prof.Mapping[0].File))
   545  		}
   546  		if prof.Mapping[0].BuildID != "" {
   547  			label = append(label, "Build ID: "+prof.Mapping[0].BuildID)
   548  		}
   549  	}
   550  	if o.SampleType != "" {
   551  		label = append(label, "Type: "+o.SampleType)
   552  	}
   553  	if prof.TimeNanos != 0 {
   554  		const layout = "Jan 2, 2006 at 3:04pm (MST)"
   555  		label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout))
   556  	}
   557  	if prof.DurationNanos != 0 {
   558  		label = append(label, fmt.Sprintf("Duration: %v", time.Duration(prof.DurationNanos)))
   559  	}
   560  	return label
   561  }
   562  
   563  // legendDetailLabels generates labels common to graph and text visualization.
   564  func legendDetailLabels(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) []string {
   565  	nodeFraction := rpt.options.NodeFraction
   566  	edgeFraction := rpt.options.EdgeFraction
   567  	nodeCount := rpt.options.NodeCount
   568  
   569  	label := []string{}
   570  
   571  	var flatSum int64
   572  	for _, n := range g.ns {
   573  		flatSum = flatSum + n.flat
   574  	}
   575  
   576  	label = append(label, fmt.Sprintf("%s of %s total (%s)", rpt.formatValue(flatSum), rpt.formatValue(rpt.total), percentage(flatSum, rpt.total)))
   577  
   578  	if rpt.total > 0 {
   579  		if droppedNodes > 0 {
   580  			label = append(label, genLabel(droppedNodes, "node", "cum",
   581  				rpt.formatValue(int64(float64(rpt.total)*nodeFraction))))
   582  		}
   583  		if droppedEdges > 0 {
   584  			label = append(label, genLabel(droppedEdges, "edge", "freq",
   585  				rpt.formatValue(int64(float64(rpt.total)*edgeFraction))))
   586  		}
   587  		if nodeCount > 0 && nodeCount < origCount {
   588  			label = append(label, fmt.Sprintf("Showing top %d nodes out of %d (cum >= %s)",
   589  				nodeCount, origCount,
   590  				rpt.formatValue(g.ns[len(g.ns)-1].cum)))
   591  		}
   592  	}
   593  	return label
   594  }
   595  
   596  func genLabel(d int, n, l, f string) string {
   597  	if d > 1 {
   598  		n = n + "s"
   599  	}
   600  	return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f)
   601  }
   602  
   603  // dotNode generates a graph node in DOT format.
   604  func dotNode(rpt *Report, maxFlat float64, rIndex int, n *node) string {
   605  	flat, cum := n.flat, n.cum
   606  
   607  	labels := strings.Split(n.info.prettyName(), "::")
   608  	label := strings.Join(labels, `\n`) + `\n`
   609  
   610  	flatValue := rpt.formatValue(flat)
   611  	if flat > 0 {
   612  		label = label + fmt.Sprintf(`%s(%s)`,
   613  			flatValue,
   614  			strings.TrimSpace(percentage(flat, rpt.total)))
   615  	} else {
   616  		label = label + "0"
   617  	}
   618  	cumValue := flatValue
   619  	if cum != flat {
   620  		if flat > 0 {
   621  			label = label + `\n`
   622  		} else {
   623  			label = label + " "
   624  		}
   625  		cumValue = rpt.formatValue(cum)
   626  		label = label + fmt.Sprintf(`of %s(%s)`,
   627  			cumValue,
   628  			strings.TrimSpace(percentage(cum, rpt.total)))
   629  	}
   630  
   631  	// Scale font sizes from 8 to 24 based on percentage of flat frequency.
   632  	// Use non linear growth to emphasize the size difference.
   633  	baseFontSize, maxFontGrowth := 8, 16.0
   634  	fontSize := baseFontSize
   635  	if maxFlat > 0 && flat > 0 && float64(flat) <= maxFlat {
   636  		fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(flat)/maxFlat)))
   637  	}
   638  	return fmt.Sprintf(`N%d [label="%s" fontsize=%d shape=box tooltip="%s (%s)"]`,
   639  		rIndex,
   640  		label,
   641  		fontSize, n.info.prettyName(), cumValue)
   642  }
   643  
   644  // dotEdge generates a graph edge in DOT format.
   645  func dotEdge(rpt *Report, from, to int, e *edgeInfo) string {
   646  	w := rpt.formatValue(e.weight)
   647  	attr := fmt.Sprintf(`label=" %s"`, w)
   648  	if rpt.total > 0 {
   649  		if weight := 1 + int(e.weight*100/rpt.total); weight > 1 {
   650  			attr = fmt.Sprintf(`%s weight=%d`, attr, weight)
   651  		}
   652  		if width := 1 + int(e.weight*5/rpt.total); width > 1 {
   653  			attr = fmt.Sprintf(`%s penwidth=%d`, attr, width)
   654  		}
   655  	}
   656  	arrow := "->"
   657  	if e.residual {
   658  		arrow = "..."
   659  	}
   660  	tooltip := fmt.Sprintf(`"%s %s %s (%s)"`,
   661  		e.src.info.prettyName(), arrow, e.dest.info.prettyName(), w)
   662  	attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`,
   663  		attr, tooltip, tooltip)
   664  
   665  	if e.residual {
   666  		attr = attr + ` style="dotted"`
   667  	}
   668  
   669  	if len(e.src.tags) > 0 {
   670  		// Separate children further if source has tags.
   671  		attr = attr + " minlen=2"
   672  	}
   673  	return fmt.Sprintf("N%d -> N%d [%s]", from, to, attr)
   674  }
   675  
   676  // dotNodelets generates the DOT boxes for the node tags.
   677  func dotNodelets(rpt *Report, rIndex int, n *node) (dot string) {
   678  	const maxNodelets = 4    // Number of nodelets for alphanumeric labels
   679  	const maxNumNodelets = 4 // Number of nodelets for numeric labels
   680  
   681  	var ts, nts tags
   682  	for _, t := range n.tags {
   683  		if t.unit == "" {
   684  			ts = append(ts, t)
   685  		} else {
   686  			nts = append(nts, t)
   687  		}
   688  	}
   689  
   690  	// Select the top maxNodelets alphanumeric labels by weight
   691  	sort.Sort(ts)
   692  	if len(ts) > maxNodelets {
   693  		ts = ts[:maxNodelets]
   694  	}
   695  	for i, t := range ts {
   696  		weight := rpt.formatValue(t.weight)
   697  		dot += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight)
   698  		dot += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight)
   699  	}
   700  
   701  	// Collapse numeric labels into maxNumNodelets buckets, of the form:
   702  	// 1MB..2MB, 3MB..5MB, ...
   703  	nts = collapseTags(nts, maxNumNodelets)
   704  	sort.Sort(nts)
   705  	for i, t := range nts {
   706  		weight := rpt.formatValue(t.weight)
   707  		dot += fmt.Sprintf(`NN%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight)
   708  		dot += fmt.Sprintf(`N%d -> NN%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight)
   709  	}
   710  
   711  	return dot
   712  }
   713  
   714  // graph summarizes a performance profile into a format that is
   715  // suitable for visualization.
   716  type graph struct {
   717  	ns nodes
   718  }
   719  
   720  // nodes is an ordered collection of graph nodes.
   721  type nodes []*node
   722  
   723  // tags represent sample annotations
   724  type tags []*tag
   725  type tagMap map[string]*tag
   726  
   727  type tag struct {
   728  	name   string
   729  	unit   string // Describe the value, "" for non-numeric tags
   730  	value  int64
   731  	weight int64
   732  }
   733  
   734  func (t tags) Len() int      { return len(t) }
   735  func (t tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
   736  func (t tags) Less(i, j int) bool {
   737  	if t[i].weight == t[j].weight {
   738  		return t[i].name < t[j].name
   739  	}
   740  	return t[i].weight > t[j].weight
   741  }
   742  
   743  // node is an entry on a profiling report. It represents a unique
   744  // program location. It can include multiple names to represent
   745  // inlined functions.
   746  type node struct {
   747  	info nodeInfo // Information associated to this entry.
   748  
   749  	// values associated to this node.
   750  	// flat is exclusive to this node, cum includes all descendents.
   751  	flat, cum int64
   752  
   753  	// in and out contains the nodes immediately reaching or reached by this nodes.
   754  	in, out edgeMap
   755  
   756  	// tags provide additional information about subsets of a sample.
   757  	tags tagMap
   758  }
   759  
   760  type nodeInfo struct {
   761  	name              string
   762  	origName          string
   763  	address           uint64
   764  	file              string
   765  	startLine, lineno int
   766  	inline            bool
   767  	lowPriority       bool
   768  	objfile           string
   769  	parent            *node // Used only if creating a calltree
   770  }
   771  
   772  func (n *node) addTags(s *profile.Sample, weight int64) {
   773  	// Add a tag with all string labels
   774  	var labels []string
   775  	for key, vals := range s.Label {
   776  		for _, v := range vals {
   777  			labels = append(labels, key+":"+v)
   778  		}
   779  	}
   780  	if len(labels) > 0 {
   781  		sort.Strings(labels)
   782  		l := n.tags.findOrAddTag(strings.Join(labels, `\n`), "", 0)
   783  		l.weight += weight
   784  	}
   785  
   786  	for key, nvals := range s.NumLabel {
   787  		for _, v := range nvals {
   788  			label := scaledValueLabel(v, key, "auto")
   789  			l := n.tags.findOrAddTag(label, key, v)
   790  			l.weight += weight
   791  		}
   792  	}
   793  }
   794  
   795  func (m tagMap) findOrAddTag(label, unit string, value int64) *tag {
   796  	if l := m[label]; l != nil {
   797  		return l
   798  	}
   799  	l := &tag{
   800  		name:  label,
   801  		unit:  unit,
   802  		value: value,
   803  	}
   804  	m[label] = l
   805  	return l
   806  }
   807  
   808  // collapseTags reduces the number of entries in a tagMap by merging
   809  // adjacent nodes into ranges. It uses a greedy approach to merge
   810  // starting with the entries with the lowest weight.
   811  func collapseTags(ts tags, count int) tags {
   812  	if len(ts) <= count {
   813  		return ts
   814  	}
   815  
   816  	sort.Sort(ts)
   817  	tagGroups := make([]tags, count)
   818  	for i, t := range ts[:count] {
   819  		tagGroups[i] = tags{t}
   820  	}
   821  	for _, t := range ts[count:] {
   822  		g, d := 0, tagDistance(t, tagGroups[0][0])
   823  		for i := 1; i < count; i++ {
   824  			if nd := tagDistance(t, tagGroups[i][0]); nd < d {
   825  				g, d = i, nd
   826  			}
   827  		}
   828  		tagGroups[g] = append(tagGroups[g], t)
   829  	}
   830  
   831  	var nts tags
   832  	for _, g := range tagGroups {
   833  		l, w := tagGroupLabel(g)
   834  		nts = append(nts, &tag{
   835  			name:   l,
   836  			weight: w,
   837  		})
   838  	}
   839  	return nts
   840  }
   841  
   842  func tagDistance(t, u *tag) float64 {
   843  	v, _ := ScaleValue(u.value, u.unit, t.unit)
   844  	if v < float64(t.value) {
   845  		return float64(t.value) - v
   846  	}
   847  	return v - float64(t.value)
   848  }
   849  
   850  func tagGroupLabel(g tags) (string, int64) {
   851  	if len(g) == 1 {
   852  		t := g[0]
   853  		return scaledValueLabel(t.value, t.unit, "auto"), t.weight
   854  	}
   855  	min := g[0]
   856  	max := g[0]
   857  	w := min.weight
   858  	for _, t := range g[1:] {
   859  		if v, _ := ScaleValue(t.value, t.unit, min.unit); int64(v) < min.value {
   860  			min = t
   861  		}
   862  		if v, _ := ScaleValue(t.value, t.unit, max.unit); int64(v) > max.value {
   863  			max = t
   864  		}
   865  		w += t.weight
   866  	}
   867  	return scaledValueLabel(min.value, min.unit, "auto") + ".." +
   868  		scaledValueLabel(max.value, max.unit, "auto"), w
   869  }
   870  
   871  // sumNodes adds the flat and sum values on a report.
   872  func sumNodes(ns nodes) (flat int64, cum int64) {
   873  	for _, n := range ns {
   874  		flat += n.flat
   875  		cum += n.cum
   876  	}
   877  	return
   878  }
   879  
   880  type edgeMap map[*node]*edgeInfo
   881  
   882  // edgeInfo contains any attributes to be represented about edges in a graph/
   883  type edgeInfo struct {
   884  	src, dest *node
   885  	// The summary weight of the edge
   886  	weight int64
   887  	// residual edges connect nodes that were connected through a
   888  	// separate node, which has been removed from the report.
   889  	residual bool
   890  }
   891  
   892  // bumpWeight increases the weight of an edge. If there isn't such an
   893  // edge in the map one is created.
   894  func bumpWeight(from, to *node, w int64, residual bool) {
   895  	if from.out[to] != to.in[from] {
   896  		panic(fmt.Errorf("asymmetric edges %v %v", *from, *to))
   897  	}
   898  
   899  	if n := from.out[to]; n != nil {
   900  		n.weight += w
   901  		if n.residual && !residual {
   902  			n.residual = false
   903  		}
   904  		return
   905  	}
   906  
   907  	info := &edgeInfo{src: from, dest: to, weight: w, residual: residual}
   908  	from.out[to] = info
   909  	to.in[from] = info
   910  }
   911  
   912  // Output formats.
   913  const (
   914  	Proto = iota
   915  	Dot
   916  	Tags
   917  	Tree
   918  	Text
   919  	Raw
   920  	Dis
   921  	List
   922  	WebList
   923  	Callgrind
   924  )
   925  
   926  // Options are the formatting and filtering options used to generate a
   927  // profile.
   928  type Options struct {
   929  	OutputFormat int
   930  
   931  	CumSort        bool
   932  	CallTree       bool
   933  	PrintAddresses bool
   934  	DropNegative   bool
   935  	Ratio          float64
   936  
   937  	NodeCount    int
   938  	NodeFraction float64
   939  	EdgeFraction float64
   940  
   941  	SampleType string
   942  	SampleUnit string // Unit for the sample data from the profile.
   943  	OutputUnit string // Units for data formatting in report.
   944  
   945  	Symbol *regexp.Regexp // Symbols to include on disassembly report.
   946  }
   947  
   948  // newGraph summarizes performance data from a profile into a graph.
   949  func newGraph(rpt *Report) (g graph, err error) {
   950  	prof := rpt.prof
   951  	o := rpt.options
   952  
   953  	// Generate a tree for graphical output if requested.
   954  	buildTree := o.CallTree && o.OutputFormat == Dot
   955  
   956  	locations := make(map[uint64][]nodeInfo)
   957  	for _, l := range prof.Location {
   958  		locations[l.ID] = newLocInfo(l)
   959  	}
   960  
   961  	nm := make(nodeMap)
   962  	for _, sample := range prof.Sample {
   963  		if sample.Location == nil {
   964  			continue
   965  		}
   966  
   967  		// Construct list of node names for sample.
   968  		var stack []nodeInfo
   969  		for _, loc := range sample.Location {
   970  			id := loc.ID
   971  			stack = append(stack, locations[id]...)
   972  		}
   973  
   974  		// Upfront pass to update the parent chains, to prevent the
   975  		// merging of nodes with different parents.
   976  		if buildTree {
   977  			var nn *node
   978  			for i := len(stack); i > 0; i-- {
   979  				n := &stack[i-1]
   980  				n.parent = nn
   981  				nn = nm.findOrInsertNode(*n)
   982  			}
   983  		}
   984  
   985  		leaf := nm.findOrInsertNode(stack[0])
   986  		weight := rpt.sampleValue(sample)
   987  		leaf.addTags(sample, weight)
   988  
   989  		// Aggregate counter data.
   990  		leaf.flat += weight
   991  		seen := make(map[*node]bool)
   992  		var nn *node
   993  		for _, s := range stack {
   994  			n := nm.findOrInsertNode(s)
   995  			if !seen[n] {
   996  				seen[n] = true
   997  				n.cum += weight
   998  
   999  				if nn != nil {
  1000  					bumpWeight(n, nn, weight, false)
  1001  				}
  1002  			}
  1003  			nn = n
  1004  		}
  1005  	}
  1006  
  1007  	// Collect new nodes into a report.
  1008  	ns := make(nodes, 0, len(nm))
  1009  	for _, n := range nm {
  1010  		if rpt.options.DropNegative && n.flat < 0 {
  1011  			continue
  1012  		}
  1013  		ns = append(ns, n)
  1014  	}
  1015  
  1016  	return graph{ns}, nil
  1017  }
  1018  
  1019  // Create a slice of formatted names for a location.
  1020  func newLocInfo(l *profile.Location) []nodeInfo {
  1021  	var objfile string
  1022  
  1023  	if m := l.Mapping; m != nil {
  1024  		objfile = filepath.Base(m.File)
  1025  	}
  1026  
  1027  	if len(l.Line) == 0 {
  1028  		return []nodeInfo{
  1029  			{
  1030  				address: l.Address,
  1031  				objfile: objfile,
  1032  			},
  1033  		}
  1034  	}
  1035  	var info []nodeInfo
  1036  	numInlineFrames := len(l.Line) - 1
  1037  	for li, line := range l.Line {
  1038  		ni := nodeInfo{
  1039  			address: l.Address,
  1040  			lineno:  int(line.Line),
  1041  			inline:  li < numInlineFrames,
  1042  			objfile: objfile,
  1043  		}
  1044  
  1045  		if line.Function != nil {
  1046  			ni.name = line.Function.Name
  1047  			ni.origName = line.Function.SystemName
  1048  			ni.file = line.Function.Filename
  1049  			ni.startLine = int(line.Function.StartLine)
  1050  		}
  1051  
  1052  		info = append(info, ni)
  1053  	}
  1054  	return info
  1055  }
  1056  
  1057  // nodeMap maps from a node info struct to a node. It is used to merge
  1058  // report entries with the same info.
  1059  type nodeMap map[nodeInfo]*node
  1060  
  1061  func (m nodeMap) findOrInsertNode(info nodeInfo) *node {
  1062  	rr := m[info]
  1063  	if rr == nil {
  1064  		rr = &node{
  1065  			info: info,
  1066  			in:   make(edgeMap),
  1067  			out:  make(edgeMap),
  1068  			tags: make(map[string]*tag),
  1069  		}
  1070  		m[info] = rr
  1071  	}
  1072  	return rr
  1073  }
  1074  
  1075  // preprocess does any required filtering/sorting according to the
  1076  // report options. Returns the mapping from each node to any nodes
  1077  // removed by path compression and statistics on the nodes/edges removed.
  1078  func (g *graph) preprocess(rpt *Report) (origCount, droppedNodes, droppedEdges int) {
  1079  	o := rpt.options
  1080  
  1081  	// Compute total weight of current set of nodes.
  1082  	// This is <= rpt.total because of node filtering.
  1083  	var totalValue int64
  1084  	for _, n := range g.ns {
  1085  		totalValue += n.flat
  1086  	}
  1087  
  1088  	// Remove nodes with value <= total*nodeFraction
  1089  	if nodeFraction := o.NodeFraction; nodeFraction > 0 {
  1090  		var removed nodes
  1091  		minValue := int64(float64(totalValue) * nodeFraction)
  1092  		kept := make(nodes, 0, len(g.ns))
  1093  		for _, n := range g.ns {
  1094  			if n.cum < minValue {
  1095  				removed = append(removed, n)
  1096  			} else {
  1097  				kept = append(kept, n)
  1098  				tagsKept := make(map[string]*tag)
  1099  				for s, t := range n.tags {
  1100  					if t.weight >= minValue {
  1101  						tagsKept[s] = t
  1102  					}
  1103  				}
  1104  				n.tags = tagsKept
  1105  			}
  1106  		}
  1107  		droppedNodes = len(removed)
  1108  		removeNodes(removed, false, false)
  1109  		g.ns = kept
  1110  	}
  1111  
  1112  	// Remove edges below minimum frequency.
  1113  	if edgeFraction := o.EdgeFraction; edgeFraction > 0 {
  1114  		minEdge := int64(float64(totalValue) * edgeFraction)
  1115  		for _, n := range g.ns {
  1116  			for src, e := range n.in {
  1117  				if e.weight < minEdge {
  1118  					delete(n.in, src)
  1119  					delete(src.out, n)
  1120  					droppedEdges++
  1121  				}
  1122  			}
  1123  		}
  1124  	}
  1125  
  1126  	sortOrder := flatName
  1127  	if o.CumSort {
  1128  		// Force cum sorting for graph output, to preserve connectivity.
  1129  		sortOrder = cumName
  1130  	}
  1131  
  1132  	// Nodes that have flat==0 and a single in/out do not provide much
  1133  	// information. Give them first chance to be removed. Do not consider edges
  1134  	// from/to nodes that are expected to be removed.
  1135  	maxNodes := o.NodeCount
  1136  	if o.OutputFormat == Dot {
  1137  		if maxNodes > 0 && maxNodes < len(g.ns) {
  1138  			sortOrder = cumName
  1139  			g.ns.sort(cumName)
  1140  			cumCutoff := g.ns[maxNodes].cum
  1141  			for _, n := range g.ns {
  1142  				if n.flat == 0 {
  1143  					if count := countEdges(n.out, cumCutoff); count > 1 {
  1144  						continue
  1145  					}
  1146  					if count := countEdges(n.in, cumCutoff); count != 1 {
  1147  						continue
  1148  					}
  1149  					n.info.lowPriority = true
  1150  				}
  1151  			}
  1152  		}
  1153  	}
  1154  
  1155  	g.ns.sort(sortOrder)
  1156  	if maxNodes > 0 {
  1157  		origCount = len(g.ns)
  1158  		for index, nodes := 0, 0; index < len(g.ns); index++ {
  1159  			nodes++
  1160  			// For DOT output, count the tags as nodes since we will draw
  1161  			// boxes for them.
  1162  			if o.OutputFormat == Dot {
  1163  				nodes += len(g.ns[index].tags)
  1164  			}
  1165  			if nodes > maxNodes {
  1166  				// Trim to the top n nodes. Create dotted edges to bridge any
  1167  				// broken connections.
  1168  				removeNodes(g.ns[index:], true, true)
  1169  				g.ns = g.ns[:index]
  1170  				break
  1171  			}
  1172  		}
  1173  	}
  1174  	removeRedundantEdges(g.ns)
  1175  
  1176  	// Select best unit for profile output.
  1177  	// Find the appropriate units for the smallest non-zero sample
  1178  	if o.OutputUnit == "minimum" && len(g.ns) > 0 {
  1179  		var maxValue, minValue int64
  1180  
  1181  		for _, n := range g.ns {
  1182  			if n.flat > 0 && (minValue == 0 || n.flat < minValue) {
  1183  				minValue = n.flat
  1184  			}
  1185  			if n.cum > maxValue {
  1186  				maxValue = n.cum
  1187  			}
  1188  		}
  1189  		if r := o.Ratio; r > 0 && r != 1 {
  1190  			minValue = int64(float64(minValue) * r)
  1191  			maxValue = int64(float64(maxValue) * r)
  1192  		}
  1193  
  1194  		_, minUnit := ScaleValue(minValue, o.SampleUnit, "minimum")
  1195  		_, maxUnit := ScaleValue(maxValue, o.SampleUnit, "minimum")
  1196  
  1197  		unit := minUnit
  1198  		if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind {
  1199  			// Minimum and maximum values have different units. Scale
  1200  			// minimum by 100 to use larger units, allowing minimum value to
  1201  			// be scaled down to 0.01, except for callgrind reports since
  1202  			// they can only represent integer values.
  1203  			_, unit = ScaleValue(100*minValue, o.SampleUnit, "minimum")
  1204  		}
  1205  
  1206  		if unit != "" {
  1207  			o.OutputUnit = unit
  1208  		} else {
  1209  			o.OutputUnit = o.SampleUnit
  1210  		}
  1211  	}
  1212  	return
  1213  }
  1214  
  1215  // countEdges counts the number of edges below the specified cutoff.
  1216  func countEdges(el edgeMap, cutoff int64) int {
  1217  	count := 0
  1218  	for _, e := range el {
  1219  		if e.weight > cutoff {
  1220  			count++
  1221  		}
  1222  	}
  1223  	return count
  1224  }
  1225  
  1226  // removeNodes removes nodes from a report, optionally bridging
  1227  // connections between in/out edges and spreading out their weights
  1228  // proportionally. residual marks new bridge edges as residual
  1229  // (dotted).
  1230  func removeNodes(toRemove nodes, bridge, residual bool) {
  1231  	for _, n := range toRemove {
  1232  		for ei := range n.in {
  1233  			delete(ei.out, n)
  1234  		}
  1235  		if bridge {
  1236  			for ei, wi := range n.in {
  1237  				for eo, wo := range n.out {
  1238  					var weight int64
  1239  					if n.cum != 0 {
  1240  						weight = int64(float64(wo.weight) * (float64(wi.weight) / float64(n.cum)))
  1241  					}
  1242  					bumpWeight(ei, eo, weight, residual)
  1243  				}
  1244  			}
  1245  		}
  1246  		for eo := range n.out {
  1247  			delete(eo.in, n)
  1248  		}
  1249  	}
  1250  }
  1251  
  1252  // removeRedundantEdges removes residual edges if the destination can
  1253  // be reached through another path. This is done to simplify the graph
  1254  // while preserving connectivity.
  1255  func removeRedundantEdges(ns nodes) {
  1256  	// Walk the nodes and outgoing edges in reverse order to prefer
  1257  	// removing edges with the lowest weight.
  1258  	for i := len(ns); i > 0; i-- {
  1259  		n := ns[i-1]
  1260  		in := sortedEdges(n.in)
  1261  		for j := len(in); j > 0; j-- {
  1262  			if e := in[j-1]; e.residual && isRedundant(e) {
  1263  				delete(e.src.out, e.dest)
  1264  				delete(e.dest.in, e.src)
  1265  			}
  1266  		}
  1267  	}
  1268  }
  1269  
  1270  // isRedundant determines if an edge can be removed without impacting
  1271  // connectivity of the whole graph. This is implemented by checking if the
  1272  // nodes have a common ancestor after removing the edge.
  1273  func isRedundant(e *edgeInfo) bool {
  1274  	destPred := predecessors(e, e.dest)
  1275  	if len(destPred) == 1 {
  1276  		return false
  1277  	}
  1278  	srcPred := predecessors(e, e.src)
  1279  
  1280  	for n := range srcPred {
  1281  		if destPred[n] && n != e.dest {
  1282  			return true
  1283  		}
  1284  	}
  1285  	return false
  1286  }
  1287  
  1288  // predecessors collects all the predecessors to node n, excluding edge e.
  1289  func predecessors(e *edgeInfo, n *node) map[*node]bool {
  1290  	seen := map[*node]bool{n: true}
  1291  	queue := []*node{n}
  1292  	for len(queue) > 0 {
  1293  		n := queue[0]
  1294  		queue = queue[1:]
  1295  		for _, ie := range n.in {
  1296  			if e == ie || seen[ie.src] {
  1297  				continue
  1298  			}
  1299  			seen[ie.src] = true
  1300  			queue = append(queue, ie.src)
  1301  		}
  1302  	}
  1303  	return seen
  1304  }
  1305  
  1306  // nodeSorter is a mechanism used to allow a report to be sorted
  1307  // in different ways.
  1308  type nodeSorter struct {
  1309  	rs   nodes
  1310  	less func(i, j int) bool
  1311  }
  1312  
  1313  func (s nodeSorter) Len() int           { return len(s.rs) }
  1314  func (s nodeSorter) Swap(i, j int)      { s.rs[i], s.rs[j] = s.rs[j], s.rs[i] }
  1315  func (s nodeSorter) Less(i, j int) bool { return s.less(i, j) }
  1316  
  1317  type nodeOrder int
  1318  
  1319  const (
  1320  	flatName nodeOrder = iota
  1321  	flatCumName
  1322  	cumName
  1323  	nameOrder
  1324  	fileOrder
  1325  	addressOrder
  1326  )
  1327  
  1328  // sort reorders the entries in a report based on the specified
  1329  // ordering criteria. The result is sorted in decreasing order for
  1330  // numeric quantities, alphabetically for text, and increasing for
  1331  // addresses.
  1332  func (ns nodes) sort(o nodeOrder) error {
  1333  	var s nodeSorter
  1334  
  1335  	switch o {
  1336  	case flatName:
  1337  		s = nodeSorter{ns,
  1338  			func(i, j int) bool {
  1339  				if iv, jv := ns[i].flat, ns[j].flat; iv != jv {
  1340  					return iv > jv
  1341  				}
  1342  				if ns[i].info.prettyName() != ns[j].info.prettyName() {
  1343  					return ns[i].info.prettyName() < ns[j].info.prettyName()
  1344  				}
  1345  				iv, jv := ns[i].cum, ns[j].cum
  1346  				return iv > jv
  1347  			},
  1348  		}
  1349  	case flatCumName:
  1350  		s = nodeSorter{ns,
  1351  			func(i, j int) bool {
  1352  				if iv, jv := ns[i].flat, ns[j].flat; iv != jv {
  1353  					return iv > jv
  1354  				}
  1355  				if iv, jv := ns[i].cum, ns[j].cum; iv != jv {
  1356  					return iv > jv
  1357  				}
  1358  				return ns[i].info.prettyName() < ns[j].info.prettyName()
  1359  			},
  1360  		}
  1361  	case cumName:
  1362  		s = nodeSorter{ns,
  1363  			func(i, j int) bool {
  1364  				if ns[i].info.lowPriority != ns[j].info.lowPriority {
  1365  					return ns[j].info.lowPriority
  1366  				}
  1367  				if iv, jv := ns[i].cum, ns[j].cum; iv != jv {
  1368  					return iv > jv
  1369  				}
  1370  				if ns[i].info.prettyName() != ns[j].info.prettyName() {
  1371  					return ns[i].info.prettyName() < ns[j].info.prettyName()
  1372  				}
  1373  				iv, jv := ns[i].flat, ns[j].flat
  1374  				return iv > jv
  1375  			},
  1376  		}
  1377  	case nameOrder:
  1378  		s = nodeSorter{ns,
  1379  			func(i, j int) bool {
  1380  				return ns[i].info.name < ns[j].info.name
  1381  			},
  1382  		}
  1383  	case fileOrder:
  1384  		s = nodeSorter{ns,
  1385  			func(i, j int) bool {
  1386  				return ns[i].info.file < ns[j].info.file
  1387  			},
  1388  		}
  1389  	case addressOrder:
  1390  		s = nodeSorter{ns,
  1391  			func(i, j int) bool {
  1392  				return ns[i].info.address < ns[j].info.address
  1393  			},
  1394  		}
  1395  	default:
  1396  		return fmt.Errorf("report: unrecognized sort ordering: %d", o)
  1397  	}
  1398  	sort.Sort(s)
  1399  	return nil
  1400  }
  1401  
  1402  type edgeList []*edgeInfo
  1403  
  1404  // sortedEdges return a slice of the edges in the map, sorted for
  1405  // visualization. The sort order is first based on the edge weight
  1406  // (higher-to-lower) and then by the node names to avoid flakiness.
  1407  func sortedEdges(edges map[*node]*edgeInfo) edgeList {
  1408  	el := make(edgeList, 0, len(edges))
  1409  	for _, w := range edges {
  1410  		el = append(el, w)
  1411  	}
  1412  
  1413  	sort.Sort(el)
  1414  	return el
  1415  }
  1416  
  1417  func (el edgeList) Len() int {
  1418  	return len(el)
  1419  }
  1420  
  1421  func (el edgeList) Less(i, j int) bool {
  1422  	if el[i].weight != el[j].weight {
  1423  		return el[i].weight > el[j].weight
  1424  	}
  1425  
  1426  	from1 := el[i].src.info.prettyName()
  1427  	from2 := el[j].src.info.prettyName()
  1428  	if from1 != from2 {
  1429  		return from1 < from2
  1430  	}
  1431  
  1432  	to1 := el[i].dest.info.prettyName()
  1433  	to2 := el[j].dest.info.prettyName()
  1434  
  1435  	return to1 < to2
  1436  }
  1437  
  1438  func (el edgeList) Swap(i, j int) {
  1439  	el[i], el[j] = el[j], el[i]
  1440  }
  1441  
  1442  func (el edgeList) sum() int64 {
  1443  	var ret int64
  1444  	for _, e := range el {
  1445  		ret += e.weight
  1446  	}
  1447  	return ret
  1448  }
  1449  
  1450  // ScaleValue reformats a value from a unit to a different unit.
  1451  func ScaleValue(value int64, fromUnit, toUnit string) (sv float64, su string) {
  1452  	// Avoid infinite recursion on overflow.
  1453  	if value < 0 && -value > 0 {
  1454  		v, u := ScaleValue(-value, fromUnit, toUnit)
  1455  		return -v, u
  1456  	}
  1457  	if m, u, ok := memoryLabel(value, fromUnit, toUnit); ok {
  1458  		return m, u
  1459  	}
  1460  	if t, u, ok := timeLabel(value, fromUnit, toUnit); ok {
  1461  		return t, u
  1462  	}
  1463  	// Skip non-interesting units.
  1464  	switch toUnit {
  1465  	case "count", "sample", "unit", "minimum":
  1466  		return float64(value), ""
  1467  	default:
  1468  		return float64(value), toUnit
  1469  	}
  1470  }
  1471  
  1472  func scaledValueLabel(value int64, fromUnit, toUnit string) string {
  1473  	v, u := ScaleValue(value, fromUnit, toUnit)
  1474  
  1475  	sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00")
  1476  	if sv == "0" || sv == "-0" {
  1477  		return "0"
  1478  	}
  1479  	return sv + u
  1480  }
  1481  
  1482  func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) {
  1483  	fromUnit = strings.TrimSuffix(strings.ToLower(fromUnit), "s")
  1484  	toUnit = strings.TrimSuffix(strings.ToLower(toUnit), "s")
  1485  
  1486  	switch fromUnit {
  1487  	case "byte", "b":
  1488  	case "kilobyte", "kb":
  1489  		value *= 1024
  1490  	case "megabyte", "mb":
  1491  		value *= 1024 * 1024
  1492  	case "gigabyte", "gb":
  1493  		value *= 1024 * 1024 * 1024
  1494  	default:
  1495  		return 0, "", false
  1496  	}
  1497  
  1498  	if toUnit == "minimum" || toUnit == "auto" {
  1499  		switch {
  1500  		case value < 1024:
  1501  			toUnit = "b"
  1502  		case value < 1024*1024:
  1503  			toUnit = "kb"
  1504  		case value < 1024*1024*1024:
  1505  			toUnit = "mb"
  1506  		default:
  1507  			toUnit = "gb"
  1508  		}
  1509  	}
  1510  
  1511  	var output float64
  1512  	switch toUnit {
  1513  	default:
  1514  		output, toUnit = float64(value), "B"
  1515  	case "kb", "kbyte", "kilobyte":
  1516  		output, toUnit = float64(value)/1024, "kB"
  1517  	case "mb", "mbyte", "megabyte":
  1518  		output, toUnit = float64(value)/(1024*1024), "MB"
  1519  	case "gb", "gbyte", "gigabyte":
  1520  		output, toUnit = float64(value)/(1024*1024*1024), "GB"
  1521  	}
  1522  	return output, toUnit, true
  1523  }
  1524  
  1525  func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) {
  1526  	fromUnit = strings.ToLower(fromUnit)
  1527  	if len(fromUnit) > 2 {
  1528  		fromUnit = strings.TrimSuffix(fromUnit, "s")
  1529  	}
  1530  
  1531  	toUnit = strings.ToLower(toUnit)
  1532  	if len(toUnit) > 2 {
  1533  		toUnit = strings.TrimSuffix(toUnit, "s")
  1534  	}
  1535  
  1536  	var d time.Duration
  1537  	switch fromUnit {
  1538  	case "nanosecond", "ns":
  1539  		d = time.Duration(value) * time.Nanosecond
  1540  	case "microsecond":
  1541  		d = time.Duration(value) * time.Microsecond
  1542  	case "millisecond", "ms":
  1543  		d = time.Duration(value) * time.Millisecond
  1544  	case "second", "sec":
  1545  		d = time.Duration(value) * time.Second
  1546  	case "cycle":
  1547  		return float64(value), "", true
  1548  	default:
  1549  		return 0, "", false
  1550  	}
  1551  
  1552  	if toUnit == "minimum" || toUnit == "auto" {
  1553  		switch {
  1554  		case d < 1*time.Microsecond:
  1555  			toUnit = "ns"
  1556  		case d < 1*time.Millisecond:
  1557  			toUnit = "us"
  1558  		case d < 1*time.Second:
  1559  			toUnit = "ms"
  1560  		case d < 1*time.Minute:
  1561  			toUnit = "sec"
  1562  		case d < 1*time.Hour:
  1563  			toUnit = "min"
  1564  		case d < 24*time.Hour:
  1565  			toUnit = "hour"
  1566  		case d < 15*24*time.Hour:
  1567  			toUnit = "day"
  1568  		case d < 120*24*time.Hour:
  1569  			toUnit = "week"
  1570  		default:
  1571  			toUnit = "year"
  1572  		}
  1573  	}
  1574  
  1575  	var output float64
  1576  	dd := float64(d)
  1577  	switch toUnit {
  1578  	case "ns", "nanosecond":
  1579  		output, toUnit = dd/float64(time.Nanosecond), "ns"
  1580  	case "us", "microsecond":
  1581  		output, toUnit = dd/float64(time.Microsecond), "us"
  1582  	case "ms", "millisecond":
  1583  		output, toUnit = dd/float64(time.Millisecond), "ms"
  1584  	case "min", "minute":
  1585  		output, toUnit = dd/float64(time.Minute), "mins"
  1586  	case "hour", "hr":
  1587  		output, toUnit = dd/float64(time.Hour), "hrs"
  1588  	case "day":
  1589  		output, toUnit = dd/float64(24*time.Hour), "days"
  1590  	case "week", "wk":
  1591  		output, toUnit = dd/float64(7*24*time.Hour), "wks"
  1592  	case "year", "yr":
  1593  		output, toUnit = dd/float64(365*7*24*time.Hour), "yrs"
  1594  	default:
  1595  		fallthrough
  1596  	case "sec", "second", "s":
  1597  		output, toUnit = dd/float64(time.Second), "s"
  1598  	}
  1599  	return output, toUnit, true
  1600  }
  1601  
  1602  // prettyName determines the printable name to be used for a node.
  1603  func (info *nodeInfo) prettyName() string {
  1604  	var name string
  1605  	if info.address != 0 {
  1606  		name = fmt.Sprintf("%016x", info.address)
  1607  	}
  1608  
  1609  	if info.name != "" {
  1610  		name = name + " " + info.name
  1611  	}
  1612  
  1613  	if info.file != "" {
  1614  		name += " " + trimPath(info.file)
  1615  		if info.lineno != 0 {
  1616  			name += fmt.Sprintf(":%d", info.lineno)
  1617  		}
  1618  	}
  1619  
  1620  	if info.inline {
  1621  		name = name + " (inline)"
  1622  	}
  1623  
  1624  	if name = strings.TrimSpace(name); name == "" && info.objfile != "" {
  1625  		name = "[" + info.objfile + "]"
  1626  	}
  1627  	return name
  1628  }
  1629  
  1630  // New builds a new report indexing the sample values interpreting the
  1631  // samples with the provided function.
  1632  func New(prof *profile.Profile, options Options, value func(s *profile.Sample) int64, unit string) *Report {
  1633  	o := &options
  1634  	if o.SampleUnit == "" {
  1635  		o.SampleUnit = unit
  1636  	}
  1637  	format := func(v int64) string {
  1638  		if r := o.Ratio; r > 0 && r != 1 {
  1639  			fv := float64(v) * r
  1640  			v = int64(fv)
  1641  		}
  1642  		return scaledValueLabel(v, o.SampleUnit, o.OutputUnit)
  1643  	}
  1644  	return &Report{prof, computeTotal(prof, value), o, value, format}
  1645  }
  1646  
  1647  // NewDefault builds a new report indexing the sample values with the
  1648  // last value available.
  1649  func NewDefault(prof *profile.Profile, options Options) *Report {
  1650  	index := len(prof.SampleType) - 1
  1651  	o := &options
  1652  	if o.SampleUnit == "" {
  1653  		o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit)
  1654  	}
  1655  	value := func(s *profile.Sample) int64 {
  1656  		return s.Value[index]
  1657  	}
  1658  	format := func(v int64) string {
  1659  		if r := o.Ratio; r > 0 && r != 1 {
  1660  			fv := float64(v) * r
  1661  			v = int64(fv)
  1662  		}
  1663  		return scaledValueLabel(v, o.SampleUnit, o.OutputUnit)
  1664  	}
  1665  	return &Report{prof, computeTotal(prof, value), o, value, format}
  1666  }
  1667  
  1668  func computeTotal(prof *profile.Profile, value func(s *profile.Sample) int64) int64 {
  1669  	var ret int64
  1670  	for _, sample := range prof.Sample {
  1671  		ret += value(sample)
  1672  	}
  1673  	return ret
  1674  }
  1675  
  1676  // Report contains the data and associated routines to extract a
  1677  // report from a profile.
  1678  type Report struct {
  1679  	prof        *profile.Profile
  1680  	total       int64
  1681  	options     *Options
  1682  	sampleValue func(*profile.Sample) int64
  1683  	formatValue func(int64) string
  1684  }