1// Copyright 2014 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package report summarizes a performance profile into a
16// human-readable report.
17package report
18
19import (
20	"fmt"
21	"io"
22	"path/filepath"
23	"regexp"
24	"sort"
25	"strconv"
26	"strings"
27	"text/tabwriter"
28	"time"
29
30	"github.com/google/pprof/internal/graph"
31	"github.com/google/pprof/internal/measurement"
32	"github.com/google/pprof/internal/plugin"
33	"github.com/google/pprof/profile"
34)
35
36// Output formats.
37const (
38	Callgrind = iota
39	Comments
40	Dis
41	Dot
42	List
43	Proto
44	Raw
45	Tags
46	Text
47	TopProto
48	Traces
49	Tree
50	WebList
51)
52
53// Options are the formatting and filtering options used to generate a
54// profile.
55type Options struct {
56	OutputFormat int
57
58	CumSort       bool
59	CallTree      bool
60	DropNegative  bool
61	CompactLabels bool
62	Ratio         float64
63	Title         string
64	ProfileLabels []string
65	ActiveFilters []string
66	NumLabelUnits map[string]string
67
68	NodeCount    int
69	NodeFraction float64
70	EdgeFraction float64
71
72	SampleValue       func(s []int64) int64
73	SampleMeanDivisor func(s []int64) int64
74	SampleType        string
75	SampleUnit        string // Unit for the sample data from the profile.
76
77	OutputUnit string // Units for data formatting in report.
78
79	Symbol     *regexp.Regexp // Symbols to include on disassembly report.
80	SourcePath string         // Search path for source files.
81	TrimPath   string         // Paths to trim from source file paths.
82
83	IntelSyntax bool // Whether or not to print assembly in Intel syntax.
84}
85
86// Generate generates a report as directed by the Report.
87func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
88	o := rpt.options
89
90	switch o.OutputFormat {
91	case Comments:
92		return printComments(w, rpt)
93	case Dot:
94		return printDOT(w, rpt)
95	case Tree:
96		return printTree(w, rpt)
97	case Text:
98		return printText(w, rpt)
99	case Traces:
100		return printTraces(w, rpt)
101	case Raw:
102		fmt.Fprint(w, rpt.prof.String())
103		return nil
104	case Tags:
105		return printTags(w, rpt)
106	case Proto:
107		return printProto(w, rpt)
108	case TopProto:
109		return printTopProto(w, rpt)
110	case Dis:
111		return printAssembly(w, rpt, obj)
112	case List:
113		return printSource(w, rpt)
114	case WebList:
115		return printWebSource(w, rpt, obj)
116	case Callgrind:
117		return printCallgrind(w, rpt)
118	}
119	return fmt.Errorf("unexpected output format")
120}
121
122// newTrimmedGraph creates a graph for this report, trimmed according
123// to the report options.
124func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, droppedEdges int) {
125	o := rpt.options
126
127	// Build a graph and refine it. On each refinement step we must rebuild the graph from the samples,
128	// as the graph itself doesn't contain enough information to preserve full precision.
129	visualMode := o.OutputFormat == Dot
130	cumSort := o.CumSort
131
132	// The call_tree option is only honored when generating visual representations of the callgraph.
133	callTree := o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind)
134
135	// First step: Build complete graph to identify low frequency nodes, based on their cum weight.
136	g = rpt.newGraph(nil)
137	totalValue, _ := g.Nodes.Sum()
138	nodeCutoff := abs64(int64(float64(totalValue) * o.NodeFraction))
139	edgeCutoff := abs64(int64(float64(totalValue) * o.EdgeFraction))
140
141	// Filter out nodes with cum value below nodeCutoff.
142	if nodeCutoff > 0 {
143		if callTree {
144			if nodesKept := g.DiscardLowFrequencyNodePtrs(nodeCutoff); len(g.Nodes) != len(nodesKept) {
145				droppedNodes = len(g.Nodes) - len(nodesKept)
146				g.TrimTree(nodesKept)
147			}
148		} else {
149			if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept) {
150				droppedNodes = len(g.Nodes) - len(nodesKept)
151				g = rpt.newGraph(nodesKept)
152			}
153		}
154	}
155	origCount = len(g.Nodes)
156
157	// Second step: Limit the total number of nodes. Apply specialized heuristics to improve
158	// visualization when generating dot output.
159	g.SortNodes(cumSort, visualMode)
160	if nodeCount := o.NodeCount; nodeCount > 0 {
161		// Remove low frequency tags and edges as they affect selection.
162		g.TrimLowFrequencyTags(nodeCutoff)
163		g.TrimLowFrequencyEdges(edgeCutoff)
164		if callTree {
165			if nodesKept := g.SelectTopNodePtrs(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) {
166				g.TrimTree(nodesKept)
167				g.SortNodes(cumSort, visualMode)
168			}
169		} else {
170			if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) {
171				g = rpt.newGraph(nodesKept)
172				g.SortNodes(cumSort, visualMode)
173			}
174		}
175	}
176
177	// Final step: Filter out low frequency tags and edges, and remove redundant edges that clutter
178	// the graph.
179	g.TrimLowFrequencyTags(nodeCutoff)
180	droppedEdges = g.TrimLowFrequencyEdges(edgeCutoff)
181	if visualMode {
182		g.RemoveRedundantEdges()
183	}
184	return
185}
186
187func (rpt *Report) selectOutputUnit(g *graph.Graph) {
188	o := rpt.options
189
190	// Select best unit for profile output.
191	// Find the appropriate units for the smallest non-zero sample
192	if o.OutputUnit != "minimum" || len(g.Nodes) == 0 {
193		return
194	}
195	var minValue int64
196
197	for _, n := range g.Nodes {
198		nodeMin := abs64(n.FlatValue())
199		if nodeMin == 0 {
200			nodeMin = abs64(n.CumValue())
201		}
202		if nodeMin > 0 && (minValue == 0 || nodeMin < minValue) {
203			minValue = nodeMin
204		}
205	}
206	maxValue := rpt.total
207	if minValue == 0 {
208		minValue = maxValue
209	}
210
211	if r := o.Ratio; r > 0 && r != 1 {
212		minValue = int64(float64(minValue) * r)
213		maxValue = int64(float64(maxValue) * r)
214	}
215
216	_, minUnit := measurement.Scale(minValue, o.SampleUnit, "minimum")
217	_, maxUnit := measurement.Scale(maxValue, o.SampleUnit, "minimum")
218
219	unit := minUnit
220	if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind {
221		// Minimum and maximum values have different units. Scale
222		// minimum by 100 to use larger units, allowing minimum value to
223		// be scaled down to 0.01, except for callgrind reports since
224		// they can only represent integer values.
225		_, unit = measurement.Scale(100*minValue, o.SampleUnit, "minimum")
226	}
227
228	if unit != "" {
229		o.OutputUnit = unit
230	} else {
231		o.OutputUnit = o.SampleUnit
232	}
233}
234
235// newGraph creates a new graph for this report. If nodes is non-nil,
236// only nodes whose info matches are included. Otherwise, all nodes
237// are included, without trimming.
238func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
239	o := rpt.options
240
241	// Clean up file paths using heuristics.
242	prof := rpt.prof
243	for _, f := range prof.Function {
244		f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath)
245	}
246	// Removes all numeric tags except for the bytes tag prior
247	// to making graph.
248	// TODO: modify to select first numeric tag if no bytes tag
249	for _, s := range prof.Sample {
250		numLabels := make(map[string][]int64, len(s.NumLabel))
251		numUnits := make(map[string][]string, len(s.NumLabel))
252		for k, vs := range s.NumLabel {
253			if k == "bytes" {
254				unit := o.NumLabelUnits[k]
255				numValues := make([]int64, len(vs))
256				numUnit := make([]string, len(vs))
257				for i, v := range vs {
258					numValues[i] = v
259					numUnit[i] = unit
260				}
261				numLabels[k] = append(numLabels[k], numValues...)
262				numUnits[k] = append(numUnits[k], numUnit...)
263			}
264		}
265		s.NumLabel = numLabels
266		s.NumUnit = numUnits
267	}
268
269	// Remove label marking samples from the base profiles, so it does not appear
270	// as a nodelet in the graph view.
271	prof.RemoveLabel("pprof::base")
272
273	formatTag := func(v int64, key string) string {
274		return measurement.ScaledLabel(v, key, o.OutputUnit)
275	}
276
277	gopt := &graph.Options{
278		SampleValue:       o.SampleValue,
279		SampleMeanDivisor: o.SampleMeanDivisor,
280		FormatTag:         formatTag,
281		CallTree:          o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind),
282		DropNegative:      o.DropNegative,
283		KeptNodes:         nodes,
284	}
285
286	// Only keep binary names for disassembly-based reports, otherwise
287	// remove it to allow merging of functions across binaries.
288	switch o.OutputFormat {
289	case Raw, List, WebList, Dis, Callgrind:
290		gopt.ObjNames = true
291	}
292
293	return graph.New(rpt.prof, gopt)
294}
295
296// printProto writes the incoming proto via thw writer w.
297// If the divide_by option has been specified, samples are scaled appropriately.
298func printProto(w io.Writer, rpt *Report) error {
299	p, o := rpt.prof, rpt.options
300
301	// Apply the sample ratio to all samples before saving the profile.
302	if r := o.Ratio; r > 0 && r != 1 {
303		for _, sample := range p.Sample {
304			for i, v := range sample.Value {
305				sample.Value[i] = int64(float64(v) * r)
306			}
307		}
308	}
309	return p.Write(w)
310}
311
312// printTopProto writes a list of the hottest routines in a profile as a profile.proto.
313func printTopProto(w io.Writer, rpt *Report) error {
314	p := rpt.prof
315	o := rpt.options
316	g, _, _, _ := rpt.newTrimmedGraph()
317	rpt.selectOutputUnit(g)
318
319	out := profile.Profile{
320		SampleType: []*profile.ValueType{
321			{Type: "cum", Unit: o.OutputUnit},
322			{Type: "flat", Unit: o.OutputUnit},
323		},
324		TimeNanos:     p.TimeNanos,
325		DurationNanos: p.DurationNanos,
326		PeriodType:    p.PeriodType,
327		Period:        p.Period,
328	}
329	functionMap := make(functionMap)
330	for i, n := range g.Nodes {
331		f, added := functionMap.findOrAdd(n.Info)
332		if added {
333			out.Function = append(out.Function, f)
334		}
335		flat, cum := n.FlatValue(), n.CumValue()
336		l := &profile.Location{
337			ID:      uint64(i + 1),
338			Address: n.Info.Address,
339			Line: []profile.Line{
340				{
341					Line:     int64(n.Info.Lineno),
342					Function: f,
343				},
344			},
345		}
346
347		fv, _ := measurement.Scale(flat, o.SampleUnit, o.OutputUnit)
348		cv, _ := measurement.Scale(cum, o.SampleUnit, o.OutputUnit)
349		s := &profile.Sample{
350			Location: []*profile.Location{l},
351			Value:    []int64{int64(cv), int64(fv)},
352		}
353		out.Location = append(out.Location, l)
354		out.Sample = append(out.Sample, s)
355	}
356
357	return out.Write(w)
358}
359
360type functionMap map[string]*profile.Function
361
362// findOrAdd takes a node representing a function, adds the function
363// represented by the node to the map if the function is not already present,
364// and returns the function the node represents. This also returns a boolean,
365// which is true if the function was added and false otherwise.
366func (fm functionMap) findOrAdd(ni graph.NodeInfo) (*profile.Function, bool) {
367	fName := fmt.Sprintf("%q%q%q%d", ni.Name, ni.OrigName, ni.File, ni.StartLine)
368
369	if f := fm[fName]; f != nil {
370		return f, false
371	}
372
373	f := &profile.Function{
374		ID:         uint64(len(fm) + 1),
375		Name:       ni.Name,
376		SystemName: ni.OrigName,
377		Filename:   ni.File,
378		StartLine:  int64(ni.StartLine),
379	}
380	fm[fName] = f
381	return f, true
382}
383
384// printAssembly prints an annotated assembly listing.
385func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
386	return PrintAssembly(w, rpt, obj, -1)
387}
388
389// PrintAssembly prints annotated disassembly of rpt to w.
390func PrintAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFuncs int) error {
391	o := rpt.options
392	prof := rpt.prof
393
394	g := rpt.newGraph(nil)
395
396	// If the regexp source can be parsed as an address, also match
397	// functions that land on that address.
398	var address *uint64
399	if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil {
400		address = &hex
401	}
402
403	fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total))
404	symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj)
405	symNodes := nodesPerSymbol(g.Nodes, symbols)
406
407	// Sort for printing.
408	var syms []*objSymbol
409	for s := range symNodes {
410		syms = append(syms, s)
411	}
412	byName := func(a, b *objSymbol) bool {
413		if na, nb := a.sym.Name[0], b.sym.Name[0]; na != nb {
414			return na < nb
415		}
416		return a.sym.Start < b.sym.Start
417	}
418	if maxFuncs < 0 {
419		sort.Sort(orderSyms{syms, byName})
420	} else {
421		byFlatSum := func(a, b *objSymbol) bool {
422			suma, _ := symNodes[a].Sum()
423			sumb, _ := symNodes[b].Sum()
424			if suma != sumb {
425				return suma > sumb
426			}
427			return byName(a, b)
428		}
429		sort.Sort(orderSyms{syms, byFlatSum})
430		if len(syms) > maxFuncs {
431			syms = syms[:maxFuncs]
432		}
433	}
434
435	// Correlate the symbols from the binary with the profile samples.
436	for _, s := range syms {
437		sns := symNodes[s]
438
439		// Gather samples for this symbol.
440		flatSum, cumSum := sns.Sum()
441
442		// Get the function assembly.
443		insts, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End, o.IntelSyntax)
444		if err != nil {
445			return err
446		}
447
448		ns := annotateAssembly(insts, sns, s.base)
449
450		fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0])
451		for _, name := range s.sym.Name[1:] {
452			fmt.Fprintf(w, "    AKA ======================== %s\n", name)
453		}
454		fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
455			rpt.formatValue(flatSum), rpt.formatValue(cumSum),
456			measurement.Percentage(cumSum, rpt.total))
457
458		function, file, line := "", "", 0
459		for _, n := range ns {
460			locStr := ""
461			// Skip loc information if it hasn't changed from previous instruction.
462			if n.function != function || n.file != file || n.line != line {
463				function, file, line = n.function, n.file, n.line
464				if n.function != "" {
465					locStr = n.function + " "
466				}
467				if n.file != "" {
468					locStr += n.file
469					if n.line != 0 {
470						locStr += fmt.Sprintf(":%d", n.line)
471					}
472				}
473			}
474			switch {
475			case locStr == "":
476				// No location info, just print the instruction.
477				fmt.Fprintf(w, "%10s %10s %10x: %s\n",
478					valueOrDot(n.flatValue(), rpt),
479					valueOrDot(n.cumValue(), rpt),
480					n.address, n.instruction,
481				)
482			case len(n.instruction) < 40:
483				// Short instruction, print loc on the same line.
484				fmt.Fprintf(w, "%10s %10s %10x: %-40s;%s\n",
485					valueOrDot(n.flatValue(), rpt),
486					valueOrDot(n.cumValue(), rpt),
487					n.address, n.instruction,
488					locStr,
489				)
490			default:
491				// Long instruction, print loc on a separate line.
492				fmt.Fprintf(w, "%74s;%s\n", "", locStr)
493				fmt.Fprintf(w, "%10s %10s %10x: %s\n",
494					valueOrDot(n.flatValue(), rpt),
495					valueOrDot(n.cumValue(), rpt),
496					n.address, n.instruction,
497				)
498			}
499		}
500	}
501	return nil
502}
503
504// symbolsFromBinaries examines the binaries listed on the profile
505// that have associated samples, and identifies symbols matching rx.
506func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol {
507	hasSamples := make(map[string]bool)
508	// Only examine mappings that have samples that match the
509	// regexp. This is an optimization to speed up pprof.
510	for _, n := range g.Nodes {
511		if name := n.Info.PrintableName(); rx.MatchString(name) && n.Info.Objfile != "" {
512			hasSamples[n.Info.Objfile] = true
513		}
514	}
515
516	// Walk all mappings looking for matching functions with samples.
517	var objSyms []*objSymbol
518	for _, m := range prof.Mapping {
519		if !hasSamples[m.File] {
520			if address == nil || !(m.Start <= *address && *address <= m.Limit) {
521				continue
522			}
523		}
524
525		f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset)
526		if err != nil {
527			fmt.Printf("%v\n", err)
528			continue
529		}
530
531		// Find symbols in this binary matching the user regexp.
532		var addr uint64
533		if address != nil {
534			addr = *address
535		}
536		msyms, err := f.Symbols(rx, addr)
537		base := f.Base()
538		f.Close()
539		if err != nil {
540			continue
541		}
542		for _, ms := range msyms {
543			objSyms = append(objSyms,
544				&objSymbol{
545					sym:  ms,
546					base: base,
547					file: f,
548				},
549			)
550		}
551	}
552
553	return objSyms
554}
555
556// objSym represents a symbol identified from a binary. It includes
557// the SymbolInfo from the disasm package and the base that must be
558// added to correspond to sample addresses
559type objSymbol struct {
560	sym  *plugin.Sym
561	base uint64
562	file plugin.ObjFile
563}
564
565// orderSyms is a wrapper type to sort []*objSymbol by a supplied comparator.
566type orderSyms struct {
567	v    []*objSymbol
568	less func(a, b *objSymbol) bool
569}
570
571func (o orderSyms) Len() int           { return len(o.v) }
572func (o orderSyms) Less(i, j int) bool { return o.less(o.v[i], o.v[j]) }
573func (o orderSyms) Swap(i, j int)      { o.v[i], o.v[j] = o.v[j], o.v[i] }
574
575// nodesPerSymbol classifies nodes into a group of symbols.
576func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.Nodes {
577	symNodes := make(map[*objSymbol]graph.Nodes)
578	for _, s := range symbols {
579		// Gather samples for this symbol.
580		for _, n := range ns {
581			address := n.Info.Address - s.base
582			if address >= s.sym.Start && address < s.sym.End {
583				symNodes[s] = append(symNodes[s], n)
584			}
585		}
586	}
587	return symNodes
588}
589
590type assemblyInstruction struct {
591	address         uint64
592	instruction     string
593	function        string
594	file            string
595	line            int
596	flat, cum       int64
597	flatDiv, cumDiv int64
598	startsBlock     bool
599	inlineCalls     []callID
600}
601
602type callID struct {
603	file string
604	line int
605}
606
607func (a *assemblyInstruction) flatValue() int64 {
608	if a.flatDiv != 0 {
609		return a.flat / a.flatDiv
610	}
611	return a.flat
612}
613
614func (a *assemblyInstruction) cumValue() int64 {
615	if a.cumDiv != 0 {
616		return a.cum / a.cumDiv
617	}
618	return a.cum
619}
620
621// annotateAssembly annotates a set of assembly instructions with a
622// set of samples. It returns a set of nodes to display. base is an
623// offset to adjust the sample addresses.
624func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, base uint64) []assemblyInstruction {
625	// Add end marker to simplify printing loop.
626	insts = append(insts, plugin.Inst{
627		Addr: ^uint64(0),
628	})
629
630	// Ensure samples are sorted by address.
631	samples.Sort(graph.AddressOrder)
632
633	s := 0
634	asm := make([]assemblyInstruction, 0, len(insts))
635	for ix, in := range insts[:len(insts)-1] {
636		n := assemblyInstruction{
637			address:     in.Addr,
638			instruction: in.Text,
639			function:    in.Function,
640			line:        in.Line,
641		}
642		if in.File != "" {
643			n.file = filepath.Base(in.File)
644		}
645
646		// Sum all the samples until the next instruction (to account
647		// for samples attributed to the middle of an instruction).
648		for next := insts[ix+1].Addr; s < len(samples) && samples[s].Info.Address-base < next; s++ {
649			sample := samples[s]
650			n.flatDiv += sample.FlatDiv
651			n.flat += sample.Flat
652			n.cumDiv += sample.CumDiv
653			n.cum += sample.Cum
654			if f := sample.Info.File; f != "" && n.file == "" {
655				n.file = filepath.Base(f)
656			}
657			if ln := sample.Info.Lineno; ln != 0 && n.line == 0 {
658				n.line = ln
659			}
660			if f := sample.Info.Name; f != "" && n.function == "" {
661				n.function = f
662			}
663		}
664		asm = append(asm, n)
665	}
666
667	return asm
668}
669
670// valueOrDot formats a value according to a report, intercepting zero
671// values.
672func valueOrDot(value int64, rpt *Report) string {
673	if value == 0 {
674		return "."
675	}
676	return rpt.formatValue(value)
677}
678
679// printTags collects all tags referenced in the profile and prints
680// them in a sorted table.
681func printTags(w io.Writer, rpt *Report) error {
682	p := rpt.prof
683
684	o := rpt.options
685	formatTag := func(v int64, key string) string {
686		return measurement.ScaledLabel(v, key, o.OutputUnit)
687	}
688
689	// Hashtable to keep accumulate tags as key,value,count.
690	tagMap := make(map[string]map[string]int64)
691	for _, s := range p.Sample {
692		for key, vals := range s.Label {
693			for _, val := range vals {
694				valueMap, ok := tagMap[key]
695				if !ok {
696					valueMap = make(map[string]int64)
697					tagMap[key] = valueMap
698				}
699				valueMap[val] += o.SampleValue(s.Value)
700			}
701		}
702		for key, vals := range s.NumLabel {
703			unit := o.NumLabelUnits[key]
704			for _, nval := range vals {
705				val := formatTag(nval, unit)
706				valueMap, ok := tagMap[key]
707				if !ok {
708					valueMap = make(map[string]int64)
709					tagMap[key] = valueMap
710				}
711				valueMap[val] += o.SampleValue(s.Value)
712			}
713		}
714	}
715
716	tagKeys := make([]*graph.Tag, 0, len(tagMap))
717	for key := range tagMap {
718		tagKeys = append(tagKeys, &graph.Tag{Name: key})
719	}
720	tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight)
721	for _, tagKey := range graph.SortTags(tagKeys, true) {
722		var total int64
723		key := tagKey.Name
724		tags := make([]*graph.Tag, 0, len(tagMap[key]))
725		for t, c := range tagMap[key] {
726			total += c
727			tags = append(tags, &graph.Tag{Name: t, Flat: c})
728		}
729
730		f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit)
731		fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u)
732		for _, t := range graph.SortTags(tags, true) {
733			f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit)
734			if total > 0 {
735				fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name)
736			} else {
737				fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name)
738			}
739		}
740		fmt.Fprintln(tabw)
741	}
742	return tabw.Flush()
743}
744
745// printComments prints all freeform comments in the profile.
746func printComments(w io.Writer, rpt *Report) error {
747	p := rpt.prof
748
749	for _, c := range p.Comments {
750		fmt.Fprintln(w, c)
751	}
752	return nil
753}
754
755// TextItem holds a single text report entry.
756type TextItem struct {
757	Name                  string
758	InlineLabel           string // Not empty if inlined
759	Flat, Cum             int64  // Raw values
760	FlatFormat, CumFormat string // Formatted values
761}
762
763// TextItems returns a list of text items from the report and a list
764// of labels that describe the report.
765func TextItems(rpt *Report) ([]TextItem, []string) {
766	g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
767	rpt.selectOutputUnit(g)
768	labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false)
769
770	var items []TextItem
771	var flatSum int64
772	for _, n := range g.Nodes {
773		name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
774
775		var inline, noinline bool
776		for _, e := range n.In {
777			if e.Inline {
778				inline = true
779			} else {
780				noinline = true
781			}
782		}
783
784		var inl string
785		if inline {
786			if noinline {
787				inl = "(partial-inline)"
788			} else {
789				inl = "(inline)"
790			}
791		}
792
793		flatSum += flat
794		items = append(items, TextItem{
795			Name:        name,
796			InlineLabel: inl,
797			Flat:        flat,
798			Cum:         cum,
799			FlatFormat:  rpt.formatValue(flat),
800			CumFormat:   rpt.formatValue(cum),
801		})
802	}
803	return items, labels
804}
805
806// printText prints a flat text report for a profile.
807func printText(w io.Writer, rpt *Report) error {
808	items, labels := TextItems(rpt)
809	fmt.Fprintln(w, strings.Join(labels, "\n"))
810	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
811		"flat", "flat", "sum", "cum", "cum")
812	var flatSum int64
813	for _, item := range items {
814		inl := item.InlineLabel
815		if inl != "" {
816			inl = " " + inl
817		}
818		flatSum += item.Flat
819		fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
820			item.FlatFormat, measurement.Percentage(item.Flat, rpt.total),
821			measurement.Percentage(flatSum, rpt.total),
822			item.CumFormat, measurement.Percentage(item.Cum, rpt.total),
823			item.Name, inl)
824	}
825	return nil
826}
827
828// printTraces prints all traces from a profile.
829func printTraces(w io.Writer, rpt *Report) error {
830	fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n"))
831
832	prof := rpt.prof
833	o := rpt.options
834
835	const separator = "-----------+-------------------------------------------------------"
836
837	_, locations := graph.CreateNodes(prof, &graph.Options{})
838	for _, sample := range prof.Sample {
839		type stk struct {
840			*graph.NodeInfo
841			inline bool
842		}
843		var stack []stk
844		for _, loc := range sample.Location {
845			nodes := locations[loc.ID]
846			for i, n := range nodes {
847				// The inline flag may be inaccurate if 'show' or 'hide' filter is
848				// used. See https://github.com/google/pprof/issues/511.
849				inline := i != len(nodes)-1
850				stack = append(stack, stk{&n.Info, inline})
851			}
852		}
853
854		if len(stack) == 0 {
855			continue
856		}
857
858		fmt.Fprintln(w, separator)
859		// Print any text labels for the sample.
860		var labels []string
861		for s, vs := range sample.Label {
862			labels = append(labels, fmt.Sprintf("%10s:  %s\n", s, strings.Join(vs, " ")))
863		}
864		sort.Strings(labels)
865		fmt.Fprint(w, strings.Join(labels, ""))
866
867		// Print any numeric labels for the sample
868		var numLabels []string
869		for key, vals := range sample.NumLabel {
870			unit := o.NumLabelUnits[key]
871			numValues := make([]string, len(vals))
872			for i, vv := range vals {
873				numValues[i] = measurement.Label(vv, unit)
874			}
875			numLabels = append(numLabels, fmt.Sprintf("%10s:  %s\n", key, strings.Join(numValues, " ")))
876		}
877		sort.Strings(numLabels)
878		fmt.Fprint(w, strings.Join(numLabels, ""))
879
880		var d, v int64
881		v = o.SampleValue(sample.Value)
882		if o.SampleMeanDivisor != nil {
883			d = o.SampleMeanDivisor(sample.Value)
884		}
885		// Print call stack.
886		if d != 0 {
887			v = v / d
888		}
889		for i, s := range stack {
890			var vs, inline string
891			if i == 0 {
892				vs = rpt.formatValue(v)
893			}
894			if s.inline {
895				inline = " (inline)"
896			}
897			fmt.Fprintf(w, "%10s   %s%s\n", vs, s.PrintableName(), inline)
898		}
899	}
900	fmt.Fprintln(w, separator)
901	return nil
902}
903
904// printCallgrind prints a graph for a profile on callgrind format.
905func printCallgrind(w io.Writer, rpt *Report) error {
906	o := rpt.options
907	rpt.options.NodeFraction = 0
908	rpt.options.EdgeFraction = 0
909	rpt.options.NodeCount = 0
910
911	g, _, _, _ := rpt.newTrimmedGraph()
912	rpt.selectOutputUnit(g)
913
914	nodeNames := getDisambiguatedNames(g)
915
916	fmt.Fprintln(w, "positions: instr line")
917	fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")")
918
919	objfiles := make(map[string]int)
920	files := make(map[string]int)
921	names := make(map[string]int)
922
923	// prevInfo points to the previous NodeInfo.
924	// It is used to group cost lines together as much as possible.
925	var prevInfo *graph.NodeInfo
926	for _, n := range g.Nodes {
927		if prevInfo == nil || n.Info.Objfile != prevInfo.Objfile || n.Info.File != prevInfo.File || n.Info.Name != prevInfo.Name {
928			fmt.Fprintln(w)
929			fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.Info.Objfile))
930			fmt.Fprintln(w, "fl="+callgrindName(files, n.Info.File))
931			fmt.Fprintln(w, "fn="+callgrindName(names, n.Info.Name))
932		}
933
934		addr := callgrindAddress(prevInfo, n.Info.Address)
935		sv, _ := measurement.Scale(n.FlatValue(), o.SampleUnit, o.OutputUnit)
936		fmt.Fprintf(w, "%s %d %d\n", addr, n.Info.Lineno, int64(sv))
937
938		// Print outgoing edges.
939		for _, out := range n.Out.Sort() {
940			c, _ := measurement.Scale(out.Weight, o.SampleUnit, o.OutputUnit)
941			callee := out.Dest
942			fmt.Fprintln(w, "cfl="+callgrindName(files, callee.Info.File))
943			fmt.Fprintln(w, "cfn="+callgrindName(names, nodeNames[callee]))
944			// pprof doesn't have a flat weight for a call, leave as 0.
945			fmt.Fprintf(w, "calls=0 %s %d\n", callgrindAddress(prevInfo, callee.Info.Address), callee.Info.Lineno)
946			// TODO: This address may be in the middle of a call
947			// instruction. It would be best to find the beginning
948			// of the instruction, but the tools seem to handle
949			// this OK.
950			fmt.Fprintf(w, "* * %d\n", int64(c))
951		}
952
953		prevInfo = &n.Info
954	}
955
956	return nil
957}
958
959// getDisambiguatedNames returns a map from each node in the graph to
960// the name to use in the callgrind output. Callgrind merges all
961// functions with the same [file name, function name]. Add a [%d/n]
962// suffix to disambiguate nodes with different values of
963// node.Function, which we want to keep separate. In particular, this
964// affects graphs created with --call_tree, where nodes from different
965// contexts are associated to different Functions.
966func getDisambiguatedNames(g *graph.Graph) map[*graph.Node]string {
967	nodeName := make(map[*graph.Node]string, len(g.Nodes))
968
969	type names struct {
970		file, function string
971	}
972
973	// nameFunctionIndex maps the callgrind names (filename, function)
974	// to the node.Function values found for that name, and each
975	// node.Function value to a sequential index to be used on the
976	// disambiguated name.
977	nameFunctionIndex := make(map[names]map[*graph.Node]int)
978	for _, n := range g.Nodes {
979		nm := names{n.Info.File, n.Info.Name}
980		p, ok := nameFunctionIndex[nm]
981		if !ok {
982			p = make(map[*graph.Node]int)
983			nameFunctionIndex[nm] = p
984		}
985		if _, ok := p[n.Function]; !ok {
986			p[n.Function] = len(p)
987		}
988	}
989
990	for _, n := range g.Nodes {
991		nm := names{n.Info.File, n.Info.Name}
992		nodeName[n] = n.Info.Name
993		if p := nameFunctionIndex[nm]; len(p) > 1 {
994			// If there is more than one function, add suffix to disambiguate.
995			nodeName[n] += fmt.Sprintf(" [%d/%d]", p[n.Function]+1, len(p))
996		}
997	}
998	return nodeName
999}
1000
1001// callgrindName implements the callgrind naming compression scheme.
1002// For names not previously seen returns "(N) name", where N is a
1003// unique index. For names previously seen returns "(N)" where N is
1004// the index returned the first time.
1005func callgrindName(names map[string]int, name string) string {
1006	if name == "" {
1007		return ""
1008	}
1009	if id, ok := names[name]; ok {
1010		return fmt.Sprintf("(%d)", id)
1011	}
1012	id := len(names) + 1
1013	names[name] = id
1014	return fmt.Sprintf("(%d) %s", id, name)
1015}
1016
1017// callgrindAddress implements the callgrind subposition compression scheme if
1018// possible. If prevInfo != nil, it contains the previous address. The current
1019// address can be given relative to the previous address, with an explicit +/-
1020// to indicate it is relative, or * for the same address.
1021func callgrindAddress(prevInfo *graph.NodeInfo, curr uint64) string {
1022	abs := fmt.Sprintf("%#x", curr)
1023	if prevInfo == nil {
1024		return abs
1025	}
1026
1027	prev := prevInfo.Address
1028	if prev == curr {
1029		return "*"
1030	}
1031
1032	diff := int64(curr - prev)
1033	relative := fmt.Sprintf("%+d", diff)
1034
1035	// Only bother to use the relative address if it is actually shorter.
1036	if len(relative) < len(abs) {
1037		return relative
1038	}
1039
1040	return abs
1041}
1042
1043// printTree prints a tree-based report in text form.
1044func printTree(w io.Writer, rpt *Report) error {
1045	const separator = "----------------------------------------------------------+-------------"
1046	const legend = "      flat  flat%   sum%        cum   cum%   calls calls% + context 	 	 "
1047
1048	g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
1049	rpt.selectOutputUnit(g)
1050
1051	fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n"))
1052
1053	fmt.Fprintln(w, separator)
1054	fmt.Fprintln(w, legend)
1055	var flatSum int64
1056
1057	rx := rpt.options.Symbol
1058	for _, n := range g.Nodes {
1059		name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
1060
1061		// Skip any entries that do not match the regexp (for the "peek" command).
1062		if rx != nil && !rx.MatchString(name) {
1063			continue
1064		}
1065
1066		fmt.Fprintln(w, separator)
1067		// Print incoming edges.
1068		inEdges := n.In.Sort()
1069		for _, in := range inEdges {
1070			var inline string
1071			if in.Inline {
1072				inline = " (inline)"
1073			}
1074			fmt.Fprintf(w, "%50s %s |   %s%s\n", rpt.formatValue(in.Weight),
1075				measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline)
1076		}
1077
1078		// Print current node.
1079		flatSum += flat
1080		fmt.Fprintf(w, "%10s %s %s %10s %s                | %s\n",
1081			rpt.formatValue(flat),
1082			measurement.Percentage(flat, rpt.total),
1083			measurement.Percentage(flatSum, rpt.total),
1084			rpt.formatValue(cum),
1085			measurement.Percentage(cum, rpt.total),
1086			name)
1087
1088		// Print outgoing edges.
1089		outEdges := n.Out.Sort()
1090		for _, out := range outEdges {
1091			var inline string
1092			if out.Inline {
1093				inline = " (inline)"
1094			}
1095			fmt.Fprintf(w, "%50s %s |   %s%s\n", rpt.formatValue(out.Weight),
1096				measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline)
1097		}
1098	}
1099	if len(g.Nodes) > 0 {
1100		fmt.Fprintln(w, separator)
1101	}
1102	return nil
1103}
1104
1105// GetDOT returns a graph suitable for dot processing along with some
1106// configuration information.
1107func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) {
1108	g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
1109	rpt.selectOutputUnit(g)
1110	labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
1111
1112	c := &graph.DotConfig{
1113		Title:       rpt.options.Title,
1114		Labels:      labels,
1115		FormatValue: rpt.formatValue,
1116		Total:       rpt.total,
1117	}
1118	return g, c
1119}
1120
1121// printDOT prints an annotated callgraph in DOT format.
1122func printDOT(w io.Writer, rpt *Report) error {
1123	g, c := GetDOT(rpt)
1124	graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
1125	return nil
1126}
1127
1128// ProfileLabels returns printable labels for a profile.
1129func ProfileLabels(rpt *Report) []string {
1130	label := []string{}
1131	prof := rpt.prof
1132	o := rpt.options
1133	if len(prof.Mapping) > 0 {
1134		if prof.Mapping[0].File != "" {
1135			label = append(label, "File: "+filepath.Base(prof.Mapping[0].File))
1136		}
1137		if prof.Mapping[0].BuildID != "" {
1138			label = append(label, "Build ID: "+prof.Mapping[0].BuildID)
1139		}
1140	}
1141	// Only include comments that do not start with '#'.
1142	for _, c := range prof.Comments {
1143		if !strings.HasPrefix(c, "#") {
1144			label = append(label, c)
1145		}
1146	}
1147	if o.SampleType != "" {
1148		label = append(label, "Type: "+o.SampleType)
1149	}
1150	if prof.TimeNanos != 0 {
1151		const layout = "Jan 2, 2006 at 3:04pm (MST)"
1152		label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout))
1153	}
1154	if prof.DurationNanos != 0 {
1155		duration := measurement.Label(prof.DurationNanos, "nanoseconds")
1156		totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds")
1157		var ratio string
1158		if totalUnit == "ns" && totalNanos != 0 {
1159			ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")"
1160		}
1161		label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio))
1162	}
1163	return label
1164}
1165
1166// reportLabels returns printable labels for a report. Includes
1167// profileLabels.
1168func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedEdges int, fullHeaders bool) []string {
1169	nodeFraction := rpt.options.NodeFraction
1170	edgeFraction := rpt.options.EdgeFraction
1171	nodeCount := len(g.Nodes)
1172
1173	var label []string
1174	if len(rpt.options.ProfileLabels) > 0 {
1175		label = append(label, rpt.options.ProfileLabels...)
1176	} else if fullHeaders || !rpt.options.CompactLabels {
1177		label = ProfileLabels(rpt)
1178	}
1179
1180	var flatSum int64
1181	for _, n := range g.Nodes {
1182		flatSum = flatSum + n.FlatValue()
1183	}
1184
1185	if len(rpt.options.ActiveFilters) > 0 {
1186		activeFilters := legendActiveFilters(rpt.options.ActiveFilters)
1187		label = append(label, activeFilters...)
1188	}
1189
1190	label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(measurement.Percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total)))
1191
1192	if rpt.total != 0 {
1193		if droppedNodes > 0 {
1194			label = append(label, genLabel(droppedNodes, "node", "cum",
1195				rpt.formatValue(abs64(int64(float64(rpt.total)*nodeFraction)))))
1196		}
1197		if droppedEdges > 0 {
1198			label = append(label, genLabel(droppedEdges, "edge", "freq",
1199				rpt.formatValue(abs64(int64(float64(rpt.total)*edgeFraction)))))
1200		}
1201		if nodeCount > 0 && nodeCount < origCount {
1202			label = append(label, fmt.Sprintf("Showing top %d nodes out of %d",
1203				nodeCount, origCount))
1204		}
1205	}
1206
1207	// Help new users understand the graph.
1208	// A new line is intentionally added here to better show this message.
1209	if fullHeaders {
1210		label = append(label, "\nSee https://git.io/JfYMW for how to read the graph")
1211	}
1212
1213	return label
1214}
1215
1216func legendActiveFilters(activeFilters []string) []string {
1217	legendActiveFilters := make([]string, len(activeFilters)+1)
1218	legendActiveFilters[0] = "Active filters:"
1219	for i, s := range activeFilters {
1220		if len(s) > 80 {
1221			s = s[:80] + "…"
1222		}
1223		legendActiveFilters[i+1] = "   " + s
1224	}
1225	return legendActiveFilters
1226}
1227
1228func genLabel(d int, n, l, f string) string {
1229	if d > 1 {
1230		n = n + "s"
1231	}
1232	return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f)
1233}
1234
1235// New builds a new report indexing the sample values interpreting the
1236// samples with the provided function.
1237func New(prof *profile.Profile, o *Options) *Report {
1238	format := func(v int64) string {
1239		if r := o.Ratio; r > 0 && r != 1 {
1240			fv := float64(v) * r
1241			v = int64(fv)
1242		}
1243		return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit)
1244	}
1245	return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor),
1246		o, format}
1247}
1248
1249// NewDefault builds a new report indexing the last sample value
1250// available.
1251func NewDefault(prof *profile.Profile, options Options) *Report {
1252	index := len(prof.SampleType) - 1
1253	o := &options
1254	if o.Title == "" && len(prof.Mapping) > 0 && prof.Mapping[0].File != "" {
1255		o.Title = filepath.Base(prof.Mapping[0].File)
1256	}
1257	o.SampleType = prof.SampleType[index].Type
1258	o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit)
1259	o.SampleValue = func(v []int64) int64 {
1260		return v[index]
1261	}
1262	return New(prof, o)
1263}
1264
1265// computeTotal computes the sum of the absolute value of all sample values.
1266// If any samples have label indicating they belong to the diff base, then the
1267// total will only include samples with that label.
1268func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
1269	var div, total, diffDiv, diffTotal int64
1270	for _, sample := range prof.Sample {
1271		var d, v int64
1272		v = value(sample.Value)
1273		if meanDiv != nil {
1274			d = meanDiv(sample.Value)
1275		}
1276		if v < 0 {
1277			v = -v
1278		}
1279		total += v
1280		div += d
1281		if sample.DiffBaseSample() {
1282			diffTotal += v
1283			diffDiv += d
1284		}
1285	}
1286	if diffTotal > 0 {
1287		total = diffTotal
1288		div = diffDiv
1289	}
1290	if div != 0 {
1291		return total / div
1292	}
1293	return total
1294}
1295
1296// Report contains the data and associated routines to extract a
1297// report from a profile.
1298type Report struct {
1299	prof        *profile.Profile
1300	total       int64
1301	options     *Options
1302	formatValue func(int64) string
1303}
1304
1305// Total returns the total number of samples in a report.
1306func (rpt *Report) Total() int64 { return rpt.total }
1307
1308func abs64(i int64) int64 {
1309	if i < 0 {
1310		return -i
1311	}
1312	return i
1313}
1314