| // Copyright 2014 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package driver implements the core pprof functionality. It can be |
| // parameterized with a flag implementation, fetch and symbolize |
| // mechanisms. |
| package driver |
| |
| import ( |
| "bytes" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "github.com/google/pprof/internal/plugin" |
| "github.com/google/pprof/internal/report" |
| "github.com/google/pprof/profile" |
| ) |
| |
| // PProf acquires a profile, and symbolizes it using a profile |
| // manager. Then it generates a report formatted according to the |
| // options selected through the flags package. |
| func PProf(eo *plugin.Options) error { |
| // Remove any temporary files created during pprof processing. |
| defer cleanupTempFiles() |
| |
| o := setDefaults(eo) |
| |
| src, cmd, err := parseFlags(o) |
| if err != nil { |
| return err |
| } |
| |
| p, err := fetchProfiles(src, o) |
| if err != nil { |
| return err |
| } |
| |
| if cmd != nil { |
| return generateReport(p, cmd, currentConfig(), o) |
| } |
| |
| if src.HTTPHostport != "" { |
| return serveWebInterface(src.HTTPHostport, p, o, src.HTTPDisableBrowser) |
| } |
| return interactive(p, o) |
| } |
| |
| func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) (*command, *report.Report, error) { |
| p = p.Copy() // Prevent modification to the incoming profile. |
| |
| // Identify units of numeric tags in profile. |
| numLabelUnits := identifyNumLabelUnits(p, o.UI) |
| |
| // Get report output format |
| c := pprofCommands[cmd[0]] |
| if c == nil { |
| panic("unexpected nil command") |
| } |
| |
| cfg = applyCommandOverrides(cmd[0], c.format, cfg) |
| |
| // Delay focus after configuring report to get percentages on all samples. |
| relative := cfg.RelativePercentages |
| if relative { |
| if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil { |
| return nil, nil, err |
| } |
| } |
| ropt, err := reportOptions(p, numLabelUnits, cfg) |
| if err != nil { |
| return nil, nil, err |
| } |
| ropt.OutputFormat = c.format |
| if len(cmd) == 2 { |
| s, err := regexp.Compile(cmd[1]) |
| if err != nil { |
| return nil, nil, fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err) |
| } |
| ropt.Symbol = s |
| } |
| |
| rpt := report.New(p, ropt) |
| if !relative { |
| if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil { |
| return nil, nil, err |
| } |
| } |
| if err := aggregate(p, cfg); err != nil { |
| return nil, nil, err |
| } |
| |
| return c, rpt, nil |
| } |
| |
| func generateReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error { |
| c, rpt, err := generateRawReport(p, cmd, cfg, o) |
| if err != nil { |
| return err |
| } |
| |
| // Generate the report. |
| dst := new(bytes.Buffer) |
| if err := report.Generate(dst, rpt, o.Obj); err != nil { |
| return err |
| } |
| src := dst |
| |
| // If necessary, perform any data post-processing. |
| if c.postProcess != nil { |
| dst = new(bytes.Buffer) |
| if err := c.postProcess(src, dst, o.UI); err != nil { |
| return err |
| } |
| src = dst |
| } |
| |
| // If no output is specified, use default visualizer. |
| output := cfg.Output |
| if output == "" { |
| if c.visualizer != nil { |
| return c.visualizer(src, os.Stdout, o.UI) |
| } |
| _, err := src.WriteTo(os.Stdout) |
| return err |
| } |
| |
| // Output to specified file. |
| o.UI.PrintErr("Generating report in ", output) |
| out, err := o.Writer.Open(output) |
| if err != nil { |
| return err |
| } |
| if _, err := src.WriteTo(out); err != nil { |
| out.Close() |
| return err |
| } |
| return out.Close() |
| } |
| |
| func applyCommandOverrides(cmd string, outputFormat int, cfg config) config { |
| // Some report types override the trim flag to false below. This is to make |
| // sure the default heuristics of excluding insignificant nodes and edges |
| // from the call graph do not apply. One example where it is important is |
| // annotated source or disassembly listing. Those reports run on a specific |
| // function (or functions), but the trimming is applied before the function |
| // data is selected. So, with trimming enabled, the report could end up |
| // showing no data if the specified function is "uninteresting" as far as the |
| // trimming is concerned. |
| trim := cfg.Trim |
| |
| switch cmd { |
| case "disasm", "weblist": |
| trim = false |
| cfg.Granularity = "addresses" |
| // Force the 'noinlines' mode so that source locations for a given address |
| // collapse and there is only one for the given address. Without this |
| // cumulative metrics would be double-counted when annotating the assembly. |
| // This is because the merge is done by address and in case of an inlined |
| // stack each of the inlined entries is a separate callgraph node. |
| cfg.NoInlines = true |
| case "peek": |
| trim = false |
| case "list": |
| trim = false |
| cfg.Granularity = "lines" |
| // Do not force 'noinlines' to be false so that specifying |
| // "-list foo -noinlines" is supported and works as expected. |
| case "text", "top", "topproto": |
| if cfg.NodeCount == -1 { |
| cfg.NodeCount = 0 |
| } |
| default: |
| if cfg.NodeCount == -1 { |
| cfg.NodeCount = 80 |
| } |
| } |
| |
| switch outputFormat { |
| case report.Proto, report.Raw, report.Callgrind: |
| trim = false |
| cfg.Granularity = "addresses" |
| cfg.NoInlines = false |
| } |
| |
| if !trim { |
| cfg.NodeCount = 0 |
| cfg.NodeFraction = 0 |
| cfg.EdgeFraction = 0 |
| } |
| return cfg |
| } |
| |
| func aggregate(prof *profile.Profile, cfg config) error { |
| var function, filename, linenumber, address bool |
| inlines := !cfg.NoInlines |
| switch cfg.Granularity { |
| case "addresses": |
| if inlines { |
| return nil |
| } |
| function = true |
| filename = true |
| linenumber = true |
| address = true |
| case "lines": |
| function = true |
| filename = true |
| linenumber = true |
| case "files": |
| filename = true |
| case "functions": |
| function = true |
| case "filefunctions": |
| function = true |
| filename = true |
| default: |
| return fmt.Errorf("unexpected granularity") |
| } |
| return prof.Aggregate(inlines, function, filename, linenumber, address) |
| } |
| |
| func reportOptions(p *profile.Profile, numLabelUnits map[string]string, cfg config) (*report.Options, error) { |
| si, mean := cfg.SampleIndex, cfg.Mean |
| value, meanDiv, sample, err := sampleFormat(p, si, mean) |
| if err != nil { |
| return nil, err |
| } |
| |
| stype := sample.Type |
| if mean { |
| stype = "mean_" + stype |
| } |
| |
| if cfg.DivideBy == 0 { |
| return nil, fmt.Errorf("zero divisor specified") |
| } |
| |
| var filters []string |
| addFilter := func(k string, v string) { |
| if v != "" { |
| filters = append(filters, k+"="+v) |
| } |
| } |
| addFilter("focus", cfg.Focus) |
| addFilter("ignore", cfg.Ignore) |
| addFilter("hide", cfg.Hide) |
| addFilter("show", cfg.Show) |
| addFilter("show_from", cfg.ShowFrom) |
| addFilter("tagfocus", cfg.TagFocus) |
| addFilter("tagignore", cfg.TagIgnore) |
| addFilter("tagshow", cfg.TagShow) |
| addFilter("taghide", cfg.TagHide) |
| |
| ropt := &report.Options{ |
| CumSort: cfg.Sort == "cum", |
| CallTree: cfg.CallTree, |
| DropNegative: cfg.DropNegative, |
| |
| CompactLabels: cfg.CompactLabels, |
| Ratio: 1 / cfg.DivideBy, |
| |
| NodeCount: cfg.NodeCount, |
| NodeFraction: cfg.NodeFraction, |
| EdgeFraction: cfg.EdgeFraction, |
| |
| ActiveFilters: filters, |
| NumLabelUnits: numLabelUnits, |
| |
| SampleValue: value, |
| SampleMeanDivisor: meanDiv, |
| SampleType: stype, |
| SampleUnit: sample.Unit, |
| |
| OutputUnit: cfg.Unit, |
| |
| SourcePath: cfg.SourcePath, |
| TrimPath: cfg.TrimPath, |
| |
| IntelSyntax: cfg.IntelSyntax, |
| } |
| |
| if len(p.Mapping) > 0 && p.Mapping[0].File != "" { |
| ropt.Title = filepath.Base(p.Mapping[0].File) |
| } |
| |
| return ropt, nil |
| } |
| |
| // identifyNumLabelUnits returns a map of numeric label keys to the units |
| // associated with those keys. |
| func identifyNumLabelUnits(p *profile.Profile, ui plugin.UI) map[string]string { |
| numLabelUnits, ignoredUnits := p.NumLabelUnits() |
| |
| // Print errors for tags with multiple units associated with |
| // a single key. |
| for k, units := range ignoredUnits { |
| ui.PrintErr(fmt.Sprintf("For tag %s used unit %s, also encountered unit(s) %s", k, numLabelUnits[k], strings.Join(units, ", "))) |
| } |
| return numLabelUnits |
| } |
| |
| type sampleValueFunc func([]int64) int64 |
| |
| // sampleFormat returns a function to extract values out of a profile.Sample, |
| // and the type/units of those values. |
| func sampleFormat(p *profile.Profile, sampleIndex string, mean bool) (value, meanDiv sampleValueFunc, v *profile.ValueType, err error) { |
| if len(p.SampleType) == 0 { |
| return nil, nil, nil, fmt.Errorf("profile has no samples") |
| } |
| index, err := p.SampleIndexByName(sampleIndex) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| value = valueExtractor(index) |
| if mean { |
| meanDiv = valueExtractor(0) |
| } |
| v = p.SampleType[index] |
| return |
| } |
| |
| func valueExtractor(ix int) sampleValueFunc { |
| return func(v []int64) int64 { |
| return v[ix] |
| } |
| } |