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 driver implements the core pprof functionality. It can be
16// parameterized with a flag implementation, fetch and symbolize
17// mechanisms.
18package driver
19
20import (
21	"bytes"
22	"fmt"
23	"io"
24	"os"
25	"path/filepath"
26	"regexp"
27	"strings"
28
29	"github.com/google/pprof/internal/plugin"
30	"github.com/google/pprof/internal/report"
31	"github.com/google/pprof/profile"
32)
33
34// PProf acquires a profile, and symbolizes it using a profile
35// manager. Then it generates a report formatted according to the
36// options selected through the flags package.
37func PProf(eo *plugin.Options) error {
38	// Remove any temporary files created during pprof processing.
39	defer cleanupTempFiles()
40
41	o := setDefaults(eo)
42
43	src, cmd, err := parseFlags(o)
44	if err != nil {
45		return err
46	}
47
48	p, err := fetchProfiles(src, o)
49	if err != nil {
50		return err
51	}
52
53	if cmd != nil {
54		return generateReport(p, cmd, currentConfig(), o)
55	}
56
57	if src.HTTPHostport != "" {
58		return serveWebInterface(src.HTTPHostport, p, o, src.HTTPDisableBrowser)
59	}
60	return interactive(p, o)
61}
62
63// generateRawReport is allowed to modify p.
64func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) (*command, *report.Report, error) {
65	// Identify units of numeric tags in profile.
66	numLabelUnits := identifyNumLabelUnits(p, o.UI)
67
68	// Get report output format
69	c := pprofCommands[cmd[0]]
70	if c == nil {
71		panic("unexpected nil command")
72	}
73
74	cfg = applyCommandOverrides(cmd[0], c.format, cfg)
75
76	// Create label pseudo nodes before filtering, in case the filters use
77	// the generated nodes.
78	generateTagRootsLeaves(p, cfg, o.UI)
79
80	// Delay focus after configuring report to get percentages on all samples.
81	relative := cfg.RelativePercentages
82	if relative {
83		if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
84			return nil, nil, err
85		}
86	}
87	ropt, err := reportOptions(p, numLabelUnits, cfg)
88	if err != nil {
89		return nil, nil, err
90	}
91	ropt.OutputFormat = c.format
92	if len(cmd) == 2 {
93		s, err := regexp.Compile(cmd[1])
94		if err != nil {
95			return nil, nil, fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
96		}
97		ropt.Symbol = s
98	}
99
100	rpt := report.New(p, ropt)
101	if !relative {
102		if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
103			return nil, nil, err
104		}
105	}
106	if err := aggregate(p, cfg); err != nil {
107		return nil, nil, err
108	}
109
110	return c, rpt, nil
111}
112
113// generateReport is allowed to modify p.
114func generateReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
115	c, rpt, err := generateRawReport(p, cmd, cfg, o)
116	if err != nil {
117		return err
118	}
119
120	// Generate the report.
121	dst := new(bytes.Buffer)
122	switch rpt.OutputFormat() {
123	case report.WebList:
124		// We need template expansion, so generate here instead of in report.
125		err = printWebList(dst, rpt, o.Obj)
126	default:
127		err = report.Generate(dst, rpt, o.Obj)
128	}
129	if err != nil {
130		return err
131	}
132	src := dst
133
134	// If necessary, perform any data post-processing.
135	if c.postProcess != nil {
136		dst = new(bytes.Buffer)
137		if err := c.postProcess(src, dst, o.UI); err != nil {
138			return err
139		}
140		src = dst
141	}
142
143	// If no output is specified, use default visualizer.
144	output := cfg.Output
145	if output == "" {
146		if c.visualizer != nil {
147			return c.visualizer(src, os.Stdout, o.UI)
148		}
149		_, err := src.WriteTo(os.Stdout)
150		return err
151	}
152
153	// Output to specified file.
154	o.UI.PrintErr("Generating report in ", output)
155	out, err := o.Writer.Open(output)
156	if err != nil {
157		return err
158	}
159	if _, err := src.WriteTo(out); err != nil {
160		out.Close()
161		return err
162	}
163	return out.Close()
164}
165
166func printWebList(dst io.Writer, rpt *report.Report, obj plugin.ObjTool) error {
167	listing, err := report.MakeWebList(rpt, obj, -1)
168	if err != nil {
169		return err
170	}
171	legend := report.ProfileLabels(rpt)
172	return renderHTML(dst, "sourcelisting", rpt, nil, legend, webArgs{
173		Standalone: true,
174		Listing:    listing,
175	})
176}
177
178func applyCommandOverrides(cmd string, outputFormat int, cfg config) config {
179	// Some report types override the trim flag to false below. This is to make
180	// sure the default heuristics of excluding insignificant nodes and edges
181	// from the call graph do not apply. One example where it is important is
182	// annotated source or disassembly listing. Those reports run on a specific
183	// function (or functions), but the trimming is applied before the function
184	// data is selected. So, with trimming enabled, the report could end up
185	// showing no data if the specified function is "uninteresting" as far as the
186	// trimming is concerned.
187	trim := cfg.Trim
188
189	switch cmd {
190	case "disasm":
191		trim = false
192		cfg.Granularity = "addresses"
193		// Force the 'noinlines' mode so that source locations for a given address
194		// collapse and there is only one for the given address. Without this
195		// cumulative metrics would be double-counted when annotating the assembly.
196		// This is because the merge is done by address and in case of an inlined
197		// stack each of the inlined entries is a separate callgraph node.
198		cfg.NoInlines = true
199	case "weblist":
200		trim = false
201		cfg.Granularity = "addresses"
202		cfg.NoInlines = false // Need inline info to support call expansion
203	case "peek":
204		trim = false
205	case "list":
206		trim = false
207		cfg.Granularity = "lines"
208		// Do not force 'noinlines' to be false so that specifying
209		// "-list foo -noinlines" is supported and works as expected.
210	case "text", "top", "topproto":
211		if cfg.NodeCount == -1 {
212			cfg.NodeCount = 0
213		}
214	default:
215		if cfg.NodeCount == -1 {
216			cfg.NodeCount = 80
217		}
218	}
219
220	switch outputFormat {
221	case report.Proto, report.Raw, report.Callgrind:
222		trim = false
223		cfg.Granularity = "addresses"
224	}
225
226	if !trim {
227		cfg.NodeCount = 0
228		cfg.NodeFraction = 0
229		cfg.EdgeFraction = 0
230	}
231	return cfg
232}
233
234// generateTagRootsLeaves generates extra nodes from the tagroot and tagleaf options.
235func generateTagRootsLeaves(prof *profile.Profile, cfg config, ui plugin.UI) {
236	tagRootLabelKeys := dropEmptyStrings(strings.Split(cfg.TagRoot, ","))
237	tagLeafLabelKeys := dropEmptyStrings(strings.Split(cfg.TagLeaf, ","))
238	rootm, leafm := addLabelNodes(prof, tagRootLabelKeys, tagLeafLabelKeys, cfg.Unit)
239	warnNoMatches(cfg.TagRoot == "" || rootm, "TagRoot", ui)
240	warnNoMatches(cfg.TagLeaf == "" || leafm, "TagLeaf", ui)
241}
242
243// dropEmptyStrings filters a slice to only non-empty strings
244func dropEmptyStrings(in []string) (out []string) {
245	for _, s := range in {
246		if s != "" {
247			out = append(out, s)
248		}
249	}
250	return
251}
252
253func aggregate(prof *profile.Profile, cfg config) error {
254	var function, filename, linenumber, address bool
255	inlines := !cfg.NoInlines
256	switch cfg.Granularity {
257	case "addresses":
258		if inlines {
259			return nil
260		}
261		function = true
262		filename = true
263		linenumber = true
264		address = true
265	case "lines":
266		function = true
267		filename = true
268		linenumber = true
269	case "files":
270		filename = true
271	case "functions":
272		function = true
273	case "filefunctions":
274		function = true
275		filename = true
276	default:
277		return fmt.Errorf("unexpected granularity")
278	}
279	return prof.Aggregate(inlines, function, filename, linenumber, cfg.ShowColumns, address)
280}
281
282func reportOptions(p *profile.Profile, numLabelUnits map[string]string, cfg config) (*report.Options, error) {
283	si, mean := cfg.SampleIndex, cfg.Mean
284	value, meanDiv, sample, err := sampleFormat(p, si, mean)
285	if err != nil {
286		return nil, err
287	}
288
289	stype := sample.Type
290	if mean {
291		stype = "mean_" + stype
292	}
293
294	if cfg.DivideBy == 0 {
295		return nil, fmt.Errorf("zero divisor specified")
296	}
297
298	var filters []string
299	addFilter := func(k string, v string) {
300		if v != "" {
301			filters = append(filters, k+"="+v)
302		}
303	}
304	addFilter("focus", cfg.Focus)
305	addFilter("ignore", cfg.Ignore)
306	addFilter("hide", cfg.Hide)
307	addFilter("show", cfg.Show)
308	addFilter("show_from", cfg.ShowFrom)
309	addFilter("tagfocus", cfg.TagFocus)
310	addFilter("tagignore", cfg.TagIgnore)
311	addFilter("tagshow", cfg.TagShow)
312	addFilter("taghide", cfg.TagHide)
313
314	ropt := &report.Options{
315		CumSort:      cfg.Sort == "cum",
316		CallTree:     cfg.CallTree,
317		DropNegative: cfg.DropNegative,
318
319		CompactLabels: cfg.CompactLabels,
320		Ratio:         1 / cfg.DivideBy,
321
322		NodeCount:    cfg.NodeCount,
323		NodeFraction: cfg.NodeFraction,
324		EdgeFraction: cfg.EdgeFraction,
325
326		ActiveFilters: filters,
327		NumLabelUnits: numLabelUnits,
328
329		SampleValue:       value,
330		SampleMeanDivisor: meanDiv,
331		SampleType:        stype,
332		SampleUnit:        sample.Unit,
333
334		OutputUnit: cfg.Unit,
335
336		SourcePath: cfg.SourcePath,
337		TrimPath:   cfg.TrimPath,
338
339		IntelSyntax: cfg.IntelSyntax,
340	}
341
342	if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
343		ropt.Title = filepath.Base(p.Mapping[0].File)
344	}
345
346	return ropt, nil
347}
348
349// identifyNumLabelUnits returns a map of numeric label keys to the units
350// associated with those keys.
351func identifyNumLabelUnits(p *profile.Profile, ui plugin.UI) map[string]string {
352	numLabelUnits, ignoredUnits := p.NumLabelUnits()
353
354	// Print errors for tags with multiple units associated with
355	// a single key.
356	for k, units := range ignoredUnits {
357		ui.PrintErr(fmt.Sprintf("For tag %s used unit %s, also encountered unit(s) %s", k, numLabelUnits[k], strings.Join(units, ", ")))
358	}
359	return numLabelUnits
360}
361
362type sampleValueFunc func([]int64) int64
363
364// sampleFormat returns a function to extract values out of a profile.Sample,
365// and the type/units of those values.
366func sampleFormat(p *profile.Profile, sampleIndex string, mean bool) (value, meanDiv sampleValueFunc, v *profile.ValueType, err error) {
367	if len(p.SampleType) == 0 {
368		return nil, nil, nil, fmt.Errorf("profile has no samples")
369	}
370	index, err := p.SampleIndexByName(sampleIndex)
371	if err != nil {
372		return nil, nil, nil, err
373	}
374	value = valueExtractor(index)
375	if mean {
376		meanDiv = valueExtractor(0)
377	}
378	v = p.SampleType[index]
379	return
380}
381
382func valueExtractor(ix int) sampleValueFunc {
383	return func(v []int64) int64 {
384		return v[ix]
385	}
386}
387
388// profileCopier can be used to obtain a fresh copy of a profile.
389// It is useful since reporting code may mutate the profile handed to it.
390type profileCopier []byte
391
392func makeProfileCopier(src *profile.Profile) profileCopier {
393	// Pre-serialize the profile. We will deserialize every time a fresh copy is needed.
394	var buf bytes.Buffer
395	src.WriteUncompressed(&buf)
396	return profileCopier(buf.Bytes())
397}
398
399// newCopy returns a new copy of the profile.
400func (c profileCopier) newCopy() *profile.Profile {
401	p, err := profile.ParseUncompressed([]byte(c))
402	if err != nil {
403		panic(err)
404	}
405	return p
406}
407