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	if len(syms) == 0 {
436		return fmt.Errorf("no matches found for regexp: %s", o.Symbol)
437	}
438
439	// Correlate the symbols from the binary with the profile samples.
440	for _, s := range syms {
441		sns := symNodes[s]
442
443		// Gather samples for this symbol.
444		flatSum, cumSum := sns.Sum()
445
446		// Get the function assembly.
447		insts, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End, o.IntelSyntax)
448		if err != nil {
449			return err
450		}
451
452		ns := annotateAssembly(insts, sns, s.file)
453
454		fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0])
455		for _, name := range s.sym.Name[1:] {
456			fmt.Fprintf(w, "    AKA ======================== %s\n", name)
457		}
458		fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
459			rpt.formatValue(flatSum), rpt.formatValue(cumSum),
460			measurement.Percentage(cumSum, rpt.total))
461
462		function, file, line := "", "", 0
463		for _, n := range ns {
464			locStr := ""
465			// Skip loc information if it hasn't changed from previous instruction.
466			if n.function != function || n.file != file || n.line != line {
467				function, file, line = n.function, n.file, n.line
468				if n.function != "" {
469					locStr = n.function + " "
470				}
471				if n.file != "" {
472					locStr += n.file
473					if n.line != 0 {
474						locStr += fmt.Sprintf(":%d", n.line)
475					}
476				}
477			}
478			switch {
479			case locStr == "":
480				// No location info, just print the instruction.
481				fmt.Fprintf(w, "%10s %10s %10x: %s\n",
482					valueOrDot(n.flatValue(), rpt),
483					valueOrDot(n.cumValue(), rpt),
484					n.address, n.instruction,
485				)
486			case len(n.instruction) < 40:
487				// Short instruction, print loc on the same line.
488				fmt.Fprintf(w, "%10s %10s %10x: %-40s;%s\n",
489					valueOrDot(n.flatValue(), rpt),
490					valueOrDot(n.cumValue(), rpt),
491					n.address, n.instruction,
492					locStr,
493				)
494			default:
495				// Long instruction, print loc on a separate line.
496				fmt.Fprintf(w, "%74s;%s\n", "", locStr)
497				fmt.Fprintf(w, "%10s %10s %10x: %s\n",
498					valueOrDot(n.flatValue(), rpt),
499					valueOrDot(n.cumValue(), rpt),
500					n.address, n.instruction,
501				)
502			}
503		}
504	}
505	return nil
506}
507
508// symbolsFromBinaries examines the binaries listed on the profile
509// that have associated samples, and identifies symbols matching rx.
510func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol {
511	hasSamples := make(map[string]bool)
512	// Only examine mappings that have samples that match the
513	// regexp. This is an optimization to speed up pprof.
514	for _, n := range g.Nodes {
515		if name := n.Info.PrintableName(); rx.MatchString(name) && n.Info.Objfile != "" {
516			hasSamples[n.Info.Objfile] = true
517		}
518	}
519
520	// Walk all mappings looking for matching functions with samples.
521	var objSyms []*objSymbol
522	for _, m := range prof.Mapping {
523		if !hasSamples[m.File] {
524			if address == nil || !(m.Start <= *address && *address <= m.Limit) {
525				continue
526			}
527		}
528
529		f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset)
530		if err != nil {
531			fmt.Printf("%v\n", err)
532			continue
533		}
534
535		// Find symbols in this binary matching the user regexp.
536		var addr uint64
537		if address != nil {
538			addr = *address
539		}
540		msyms, err := f.Symbols(rx, addr)
541		f.Close()
542		if err != nil {
543			continue
544		}
545		for _, ms := range msyms {
546			objSyms = append(objSyms,
547				&objSymbol{
548					sym:  ms,
549					file: f,
550				},
551			)
552		}
553	}
554
555	return objSyms
556}
557
558// objSym represents a symbol identified from a binary. It includes
559// the SymbolInfo from the disasm package and the base that must be
560// added to correspond to sample addresses
561type objSymbol struct {
562	sym  *plugin.Sym
563	file plugin.ObjFile
564}
565
566// orderSyms is a wrapper type to sort []*objSymbol by a supplied comparator.
567type orderSyms struct {
568	v    []*objSymbol
569	less func(a, b *objSymbol) bool
570}
571
572func (o orderSyms) Len() int           { return len(o.v) }
573func (o orderSyms) Less(i, j int) bool { return o.less(o.v[i], o.v[j]) }
574func (o orderSyms) Swap(i, j int)      { o.v[i], o.v[j] = o.v[j], o.v[i] }
575
576// nodesPerSymbol classifies nodes into a group of symbols.
577func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.Nodes {
578	symNodes := make(map[*objSymbol]graph.Nodes)
579	for _, s := range symbols {
580		// Gather samples for this symbol.
581		for _, n := range ns {
582			if address, err := s.file.ObjAddr(n.Info.Address); err == nil && 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, file plugin.ObjFile) []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); s++ {
649			if addr, err := file.ObjAddr(samples[s].Info.Address); err != nil || addr >= next {
650				break
651			}
652			sample := samples[s]
653			n.flatDiv += sample.FlatDiv
654			n.flat += sample.Flat
655			n.cumDiv += sample.CumDiv
656			n.cum += sample.Cum
657			if f := sample.Info.File; f != "" && n.file == "" {
658				n.file = filepath.Base(f)
659			}
660			if ln := sample.Info.Lineno; ln != 0 && n.line == 0 {
661				n.line = ln
662			}
663			if f := sample.Info.Name; f != "" && n.function == "" {
664				n.function = f
665			}
666		}
667		asm = append(asm, n)
668	}
669
670	return asm
671}
672
673// valueOrDot formats a value according to a report, intercepting zero
674// values.
675func valueOrDot(value int64, rpt *Report) string {
676	if value == 0 {
677		return "."
678	}
679	return rpt.formatValue(value)
680}
681
682// printTags collects all tags referenced in the profile and prints
683// them in a sorted table.
684func printTags(w io.Writer, rpt *Report) error {
685	p := rpt.prof
686
687	o := rpt.options
688	formatTag := func(v int64, key string) string {
689		return measurement.ScaledLabel(v, key, o.OutputUnit)
690	}
691
692	// Hashtable to keep accumulate tags as key,value,count.
693	tagMap := make(map[string]map[string]int64)
694	for _, s := range p.Sample {
695		for key, vals := range s.Label {
696			for _, val := range vals {
697				valueMap, ok := tagMap[key]
698				if !ok {
699					valueMap = make(map[string]int64)
700					tagMap[key] = valueMap
701				}
702				valueMap[val] += o.SampleValue(s.Value)
703			}
704		}
705		for key, vals := range s.NumLabel {
706			unit := o.NumLabelUnits[key]
707			for _, nval := range vals {
708				val := formatTag(nval, unit)
709				valueMap, ok := tagMap[key]
710				if !ok {
711					valueMap = make(map[string]int64)
712					tagMap[key] = valueMap
713				}
714				valueMap[val] += o.SampleValue(s.Value)
715			}
716		}
717	}
718
719	tagKeys := make([]*graph.Tag, 0, len(tagMap))
720	for key := range tagMap {
721		tagKeys = append(tagKeys, &graph.Tag{Name: key})
722	}
723	tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight)
724	for _, tagKey := range graph.SortTags(tagKeys, true) {
725		var total int64
726		key := tagKey.Name
727		tags := make([]*graph.Tag, 0, len(tagMap[key]))
728		for t, c := range tagMap[key] {
729			total += c
730			tags = append(tags, &graph.Tag{Name: t, Flat: c})
731		}
732
733		f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit)
734		fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u)
735		for _, t := range graph.SortTags(tags, true) {
736			f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit)
737			if total > 0 {
738				fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name)
739			} else {
740				fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name)
741			}
742		}
743		fmt.Fprintln(tabw)
744	}
745	return tabw.Flush()
746}
747
748// printComments prints all freeform comments in the profile.
749func printComments(w io.Writer, rpt *Report) error {
750	p := rpt.prof
751
752	for _, c := range p.Comments {
753		fmt.Fprintln(w, c)
754	}
755	return nil
756}
757
758// TextItem holds a single text report entry.
759type TextItem struct {
760	Name                  string
761	InlineLabel           string // Not empty if inlined
762	Flat, Cum             int64  // Raw values
763	FlatFormat, CumFormat string // Formatted values
764}
765
766// TextItems returns a list of text items from the report and a list
767// of labels that describe the report.
768func TextItems(rpt *Report) ([]TextItem, []string) {
769	g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
770	rpt.selectOutputUnit(g)
771	labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false)
772
773	var items []TextItem
774	var flatSum int64
775	for _, n := range g.Nodes {
776		name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
777
778		var inline, noinline bool
779		for _, e := range n.In {
780			if e.Inline {
781				inline = true
782			} else {
783				noinline = true
784			}
785		}
786
787		var inl string
788		if inline {
789			if noinline {
790				inl = "(partial-inline)"
791			} else {
792				inl = "(inline)"
793			}
794		}
795
796		flatSum += flat
797		items = append(items, TextItem{
798			Name:        name,
799			InlineLabel: inl,
800			Flat:        flat,
801			Cum:         cum,
802			FlatFormat:  rpt.formatValue(flat),
803			CumFormat:   rpt.formatValue(cum),
804		})
805	}
806	return items, labels
807}
808
809// printText prints a flat text report for a profile.
810func printText(w io.Writer, rpt *Report) error {
811	items, labels := TextItems(rpt)
812	fmt.Fprintln(w, strings.Join(labels, "\n"))
813	fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n",
814		"flat", "flat", "sum", "cum", "cum")
815	var flatSum int64
816	for _, item := range items {
817		inl := item.InlineLabel
818		if inl != "" {
819			inl = " " + inl
820		}
821		flatSum += item.Flat
822		fmt.Fprintf(w, "%10s %s %s %10s %s  %s%s\n",
823			item.FlatFormat, measurement.Percentage(item.Flat, rpt.total),
824			measurement.Percentage(flatSum, rpt.total),
825			item.CumFormat, measurement.Percentage(item.Cum, rpt.total),
826			item.Name, inl)
827	}
828	return nil
829}
830
831// printTraces prints all traces from a profile.
832func printTraces(w io.Writer, rpt *Report) error {
833	fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n"))
834
835	prof := rpt.prof
836	o := rpt.options
837
838	const separator = "-----------+-------------------------------------------------------"
839
840	_, locations := graph.CreateNodes(prof, &graph.Options{})
841	for _, sample := range prof.Sample {
842		type stk struct {
843			*graph.NodeInfo
844			inline bool
845		}
846		var stack []stk
847		for _, loc := range sample.Location {
848			nodes := locations[loc.ID]
849			for i, n := range nodes {
850				// The inline flag may be inaccurate if 'show' or 'hide' filter is
851				// used. See https://github.com/google/pprof/issues/511.
852				inline := i != len(nodes)-1
853				stack = append(stack, stk{&n.Info, inline})
854			}
855		}
856
857		if len(stack) == 0 {
858			continue
859		}
860
861		fmt.Fprintln(w, separator)
862		// Print any text labels for the sample.
863		var labels []string
864		for s, vs := range sample.Label {
865			labels = append(labels, fmt.Sprintf("%10s:  %s\n", s, strings.Join(vs, " ")))
866		}
867		sort.Strings(labels)
868		fmt.Fprint(w, strings.Join(labels, ""))
869
870		// Print any numeric labels for the sample
871		var numLabels []string
872		for key, vals := range sample.NumLabel {
873			unit := o.NumLabelUnits[key]
874			numValues := make([]string, len(vals))
875			for i, vv := range vals {
876				numValues[i] = measurement.Label(vv, unit)
877			}
878			numLabels = append(numLabels, fmt.Sprintf("%10s:  %s\n", key, strings.Join(numValues, " ")))
879		}
880		sort.Strings(numLabels)
881		fmt.Fprint(w, strings.Join(numLabels, ""))
882
883		var d, v int64
884		v = o.SampleValue(sample.Value)
885		if o.SampleMeanDivisor != nil {
886			d = o.SampleMeanDivisor(sample.Value)
887		}
888		// Print call stack.
889		if d != 0 {
890			v = v / d
891		}
892		for i, s := range stack {
893			var vs, inline string
894			if i == 0 {
895				vs = rpt.formatValue(v)
896			}
897			if s.inline {
898				inline = " (inline)"
899			}
900			fmt.Fprintf(w, "%10s   %s%s\n", vs, s.PrintableName(), inline)
901		}
902	}
903	fmt.Fprintln(w, separator)
904	return nil
905}
906
907// printCallgrind prints a graph for a profile on callgrind format.
908func printCallgrind(w io.Writer, rpt *Report) error {
909	o := rpt.options
910	rpt.options.NodeFraction = 0
911	rpt.options.EdgeFraction = 0
912	rpt.options.NodeCount = 0
913
914	g, _, _, _ := rpt.newTrimmedGraph()
915	rpt.selectOutputUnit(g)
916
917	nodeNames := getDisambiguatedNames(g)
918
919	fmt.Fprintln(w, "positions: instr line")
920	fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")")
921
922	objfiles := make(map[string]int)
923	files := make(map[string]int)
924	names := make(map[string]int)
925
926	// prevInfo points to the previous NodeInfo.
927	// It is used to group cost lines together as much as possible.
928	var prevInfo *graph.NodeInfo
929	for _, n := range g.Nodes {
930		if prevInfo == nil || n.Info.Objfile != prevInfo.Objfile || n.Info.File != prevInfo.File || n.Info.Name != prevInfo.Name {
931			fmt.Fprintln(w)
932			fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.Info.Objfile))
933			fmt.Fprintln(w, "fl="+callgrindName(files, n.Info.File))
934			fmt.Fprintln(w, "fn="+callgrindName(names, n.Info.Name))
935		}
936
937		addr := callgrindAddress(prevInfo, n.Info.Address)
938		sv, _ := measurement.Scale(n.FlatValue(), o.SampleUnit, o.OutputUnit)
939		fmt.Fprintf(w, "%s %d %d\n", addr, n.Info.Lineno, int64(sv))
940
941		// Print outgoing edges.
942		for _, out := range n.Out.Sort() {
943			c, _ := measurement.Scale(out.Weight, o.SampleUnit, o.OutputUnit)
944			callee := out.Dest
945			fmt.Fprintln(w, "cfl="+callgrindName(files, callee.Info.File))
946			fmt.Fprintln(w, "cfn="+callgrindName(names, nodeNames[callee]))
947			// pprof doesn't have a flat weight for a call, leave as 0.
948			fmt.Fprintf(w, "calls=0 %s %d\n", callgrindAddress(prevInfo, callee.Info.Address), callee.Info.Lineno)
949			// TODO: This address may be in the middle of a call
950			// instruction. It would be best to find the beginning
951			// of the instruction, but the tools seem to handle
952			// this OK.
953			fmt.Fprintf(w, "* * %d\n", int64(c))
954		}
955
956		prevInfo = &n.Info
957	}
958
959	return nil
960}
961
962// getDisambiguatedNames returns a map from each node in the graph to
963// the name to use in the callgrind output. Callgrind merges all
964// functions with the same [file name, function name]. Add a [%d/n]
965// suffix to disambiguate nodes with different values of
966// node.Function, which we want to keep separate. In particular, this
967// affects graphs created with --call_tree, where nodes from different
968// contexts are associated to different Functions.
969func getDisambiguatedNames(g *graph.Graph) map[*graph.Node]string {
970	nodeName := make(map[*graph.Node]string, len(g.Nodes))
971
972	type names struct {
973		file, function string
974	}
975
976	// nameFunctionIndex maps the callgrind names (filename, function)
977	// to the node.Function values found for that name, and each
978	// node.Function value to a sequential index to be used on the
979	// disambiguated name.
980	nameFunctionIndex := make(map[names]map[*graph.Node]int)
981	for _, n := range g.Nodes {
982		nm := names{n.Info.File, n.Info.Name}
983		p, ok := nameFunctionIndex[nm]
984		if !ok {
985			p = make(map[*graph.Node]int)
986			nameFunctionIndex[nm] = p
987		}
988		if _, ok := p[n.Function]; !ok {
989			p[n.Function] = len(p)
990		}
991	}
992
993	for _, n := range g.Nodes {
994		nm := names{n.Info.File, n.Info.Name}
995		nodeName[n] = n.Info.Name
996		if p := nameFunctionIndex[nm]; len(p) > 1 {
997			// If there is more than one function, add suffix to disambiguate.
998			nodeName[n] += fmt.Sprintf(" [%d/%d]", p[n.Function]+1, len(p))
999		}
1000	}
1001	return nodeName
1002}
1003
1004// callgrindName implements the callgrind naming compression scheme.
1005// For names not previously seen returns "(N) name", where N is a
1006// unique index. For names previously seen returns "(N)" where N is
1007// the index returned the first time.
1008func callgrindName(names map[string]int, name string) string {
1009	if name == "" {
1010		return ""
1011	}
1012	if id, ok := names[name]; ok {
1013		return fmt.Sprintf("(%d)", id)
1014	}
1015	id := len(names) + 1
1016	names[name] = id
1017	return fmt.Sprintf("(%d) %s", id, name)
1018}
1019
1020// callgrindAddress implements the callgrind subposition compression scheme if
1021// possible. If prevInfo != nil, it contains the previous address. The current
1022// address can be given relative to the previous address, with an explicit +/-
1023// to indicate it is relative, or * for the same address.
1024func callgrindAddress(prevInfo *graph.NodeInfo, curr uint64) string {
1025	abs := fmt.Sprintf("%#x", curr)
1026	if prevInfo == nil {
1027		return abs
1028	}
1029
1030	prev := prevInfo.Address
1031	if prev == curr {
1032		return "*"
1033	}
1034
1035	diff := int64(curr - prev)
1036	relative := fmt.Sprintf("%+d", diff)
1037
1038	// Only bother to use the relative address if it is actually shorter.
1039	if len(relative) < len(abs) {
1040		return relative
1041	}
1042
1043	return abs
1044}
1045
1046// printTree prints a tree-based report in text form.
1047func printTree(w io.Writer, rpt *Report) error {
1048	const separator = "----------------------------------------------------------+-------------"
1049	const legend = "      flat  flat%   sum%        cum   cum%   calls calls% + context 	 	 "
1050
1051	g, origCount, droppedNodes, _ := rpt.newTrimmedGraph()
1052	rpt.selectOutputUnit(g)
1053
1054	fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n"))
1055
1056	fmt.Fprintln(w, separator)
1057	fmt.Fprintln(w, legend)
1058	var flatSum int64
1059
1060	rx := rpt.options.Symbol
1061	matched := 0
1062	for _, n := range g.Nodes {
1063		name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue()
1064
1065		// Skip any entries that do not match the regexp (for the "peek" command).
1066		if rx != nil && !rx.MatchString(name) {
1067			continue
1068		}
1069		matched++
1070
1071		fmt.Fprintln(w, separator)
1072		// Print incoming edges.
1073		inEdges := n.In.Sort()
1074		for _, in := range inEdges {
1075			var inline string
1076			if in.Inline {
1077				inline = " (inline)"
1078			}
1079			fmt.Fprintf(w, "%50s %s |   %s%s\n", rpt.formatValue(in.Weight),
1080				measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline)
1081		}
1082
1083		// Print current node.
1084		flatSum += flat
1085		fmt.Fprintf(w, "%10s %s %s %10s %s                | %s\n",
1086			rpt.formatValue(flat),
1087			measurement.Percentage(flat, rpt.total),
1088			measurement.Percentage(flatSum, rpt.total),
1089			rpt.formatValue(cum),
1090			measurement.Percentage(cum, rpt.total),
1091			name)
1092
1093		// Print outgoing edges.
1094		outEdges := n.Out.Sort()
1095		for _, out := range outEdges {
1096			var inline string
1097			if out.Inline {
1098				inline = " (inline)"
1099			}
1100			fmt.Fprintf(w, "%50s %s |   %s%s\n", rpt.formatValue(out.Weight),
1101				measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline)
1102		}
1103	}
1104	if len(g.Nodes) > 0 {
1105		fmt.Fprintln(w, separator)
1106	}
1107	if rx != nil && matched == 0 {
1108		return fmt.Errorf("no matches found for regexp: %s", rx)
1109	}
1110	return nil
1111}
1112
1113// GetDOT returns a graph suitable for dot processing along with some
1114// configuration information.
1115func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) {
1116	g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph()
1117	rpt.selectOutputUnit(g)
1118	labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true)
1119
1120	c := &graph.DotConfig{
1121		Title:       rpt.options.Title,
1122		Labels:      labels,
1123		FormatValue: rpt.formatValue,
1124		Total:       rpt.total,
1125	}
1126	return g, c
1127}
1128
1129// printDOT prints an annotated callgraph in DOT format.
1130func printDOT(w io.Writer, rpt *Report) error {
1131	g, c := GetDOT(rpt)
1132	graph.ComposeDot(w, g, &graph.DotAttributes{}, c)
1133	return nil
1134}
1135
1136// ProfileLabels returns printable labels for a profile.
1137func ProfileLabels(rpt *Report) []string {
1138	label := []string{}
1139	prof := rpt.prof
1140	o := rpt.options
1141	if len(prof.Mapping) > 0 {
1142		if prof.Mapping[0].File != "" {
1143			label = append(label, "File: "+filepath.Base(prof.Mapping[0].File))
1144		}
1145		if prof.Mapping[0].BuildID != "" {
1146			label = append(label, "Build ID: "+prof.Mapping[0].BuildID)
1147		}
1148	}
1149	// Only include comments that do not start with '#'.
1150	for _, c := range prof.Comments {
1151		if !strings.HasPrefix(c, "#") {
1152			label = append(label, c)
1153		}
1154	}
1155	if o.SampleType != "" {
1156		label = append(label, "Type: "+o.SampleType)
1157	}
1158	if prof.TimeNanos != 0 {
1159		const layout = "Jan 2, 2006 at 3:04pm (MST)"
1160		label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout))
1161	}
1162	if prof.DurationNanos != 0 {
1163		duration := measurement.Label(prof.DurationNanos, "nanoseconds")
1164		totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds")
1165		var ratio string
1166		if totalUnit == "ns" && totalNanos != 0 {
1167			ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")"
1168		}
1169		label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio))
1170	}
1171	return label
1172}
1173
1174// reportLabels returns printable labels for a report. Includes
1175// profileLabels.
1176func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedEdges int, fullHeaders bool) []string {
1177	nodeFraction := rpt.options.NodeFraction
1178	edgeFraction := rpt.options.EdgeFraction
1179	nodeCount := len(g.Nodes)
1180
1181	var label []string
1182	if len(rpt.options.ProfileLabels) > 0 {
1183		label = append(label, rpt.options.ProfileLabels...)
1184	} else if fullHeaders || !rpt.options.CompactLabels {
1185		label = ProfileLabels(rpt)
1186	}
1187
1188	var flatSum int64
1189	for _, n := range g.Nodes {
1190		flatSum = flatSum + n.FlatValue()
1191	}
1192
1193	if len(rpt.options.ActiveFilters) > 0 {
1194		activeFilters := legendActiveFilters(rpt.options.ActiveFilters)
1195		label = append(label, activeFilters...)
1196	}
1197
1198	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)))
1199
1200	if rpt.total != 0 {
1201		if droppedNodes > 0 {
1202			label = append(label, genLabel(droppedNodes, "node", "cum",
1203				rpt.formatValue(abs64(int64(float64(rpt.total)*nodeFraction)))))
1204		}
1205		if droppedEdges > 0 {
1206			label = append(label, genLabel(droppedEdges, "edge", "freq",
1207				rpt.formatValue(abs64(int64(float64(rpt.total)*edgeFraction)))))
1208		}
1209		if nodeCount > 0 && nodeCount < origCount {
1210			label = append(label, fmt.Sprintf("Showing top %d nodes out of %d",
1211				nodeCount, origCount))
1212		}
1213	}
1214
1215	// Help new users understand the graph.
1216	// A new line is intentionally added here to better show this message.
1217	if fullHeaders {
1218		label = append(label, "\nSee https://git.io/JfYMW for how to read the graph")
1219	}
1220
1221	return label
1222}
1223
1224func legendActiveFilters(activeFilters []string) []string {
1225	legendActiveFilters := make([]string, len(activeFilters)+1)
1226	legendActiveFilters[0] = "Active filters:"
1227	for i, s := range activeFilters {
1228		if len(s) > 80 {
1229			s = s[:80] + "…"
1230		}
1231		legendActiveFilters[i+1] = "   " + s
1232	}
1233	return legendActiveFilters
1234}
1235
1236func genLabel(d int, n, l, f string) string {
1237	if d > 1 {
1238		n = n + "s"
1239	}
1240	return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f)
1241}
1242
1243// New builds a new report indexing the sample values interpreting the
1244// samples with the provided function.
1245func New(prof *profile.Profile, o *Options) *Report {
1246	format := func(v int64) string {
1247		if r := o.Ratio; r > 0 && r != 1 {
1248			fv := float64(v) * r
1249			v = int64(fv)
1250		}
1251		return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit)
1252	}
1253	return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor),
1254		o, format}
1255}
1256
1257// NewDefault builds a new report indexing the last sample value
1258// available.
1259func NewDefault(prof *profile.Profile, options Options) *Report {
1260	index := len(prof.SampleType) - 1
1261	o := &options
1262	if o.Title == "" && len(prof.Mapping) > 0 && prof.Mapping[0].File != "" {
1263		o.Title = filepath.Base(prof.Mapping[0].File)
1264	}
1265	o.SampleType = prof.SampleType[index].Type
1266	o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit)
1267	o.SampleValue = func(v []int64) int64 {
1268		return v[index]
1269	}
1270	return New(prof, o)
1271}
1272
1273// computeTotal computes the sum of the absolute value of all sample values.
1274// If any samples have label indicating they belong to the diff base, then the
1275// total will only include samples with that label.
1276func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
1277	var div, total, diffDiv, diffTotal int64
1278	for _, sample := range prof.Sample {
1279		var d, v int64
1280		v = value(sample.Value)
1281		if meanDiv != nil {
1282			d = meanDiv(sample.Value)
1283		}
1284		if v < 0 {
1285			v = -v
1286		}
1287		total += v
1288		div += d
1289		if sample.DiffBaseSample() {
1290			diffTotal += v
1291			diffDiv += d
1292		}
1293	}
1294	if diffTotal > 0 {
1295		total = diffTotal
1296		div = diffDiv
1297	}
1298	if div != 0 {
1299		return total / div
1300	}
1301	return total
1302}
1303
1304// Report contains the data and associated routines to extract a
1305// report from a profile.
1306type Report struct {
1307	prof        *profile.Profile
1308	total       int64
1309	options     *Options
1310	formatValue func(int64) string
1311}
1312
1313// Total returns the total number of samples in a report.
1314func (rpt *Report) Total() int64 { return rpt.total }
1315
1316func abs64(i int64) int64 {
1317	if i < 0 {
1318		return -i
1319	}
1320	return i
1321}
1322