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