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