| // 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 report |
| |
| // This file contains routines related to the generation of annotated |
| // source listings. |
| |
| import ( |
| "bufio" |
| "fmt" |
| "html/template" |
| "io" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/google/pprof/internal/graph" |
| "github.com/google/pprof/internal/plugin" |
| ) |
| |
| // printSource prints an annotated source listing, include all |
| // functions with samples that match the regexp rpt.options.symbol. |
| // The sources are sorted by function name and then by filename to |
| // eliminate potential nondeterminism. |
| func printSource(w io.Writer, rpt *Report) error { |
| o := rpt.options |
| g := rpt.newGraph(nil) |
| |
| // Identify all the functions that match the regexp provided. |
| // Group nodes for each matching function. |
| var functions graph.Nodes |
| functionNodes := make(map[string]graph.Nodes) |
| for _, n := range g.Nodes { |
| if !o.Symbol.MatchString(n.Info.Name) { |
| continue |
| } |
| if functionNodes[n.Info.Name] == nil { |
| functions = append(functions, n) |
| } |
| functionNodes[n.Info.Name] = append(functionNodes[n.Info.Name], n) |
| } |
| functions.Sort(graph.NameOrder) |
| |
| sourcePath := o.SourcePath |
| if sourcePath == "" { |
| wd, err := os.Getwd() |
| if err != nil { |
| return fmt.Errorf("Could not stat current dir: %v", err) |
| } |
| sourcePath = wd |
| } |
| reader := newSourceReader(sourcePath) |
| |
| fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) |
| for _, fn := range functions { |
| name := fn.Info.Name |
| |
| // Identify all the source files associated to this function. |
| // Group nodes for each source file. |
| var sourceFiles graph.Nodes |
| fileNodes := make(map[string]graph.Nodes) |
| for _, n := range functionNodes[name] { |
| if n.Info.File == "" { |
| continue |
| } |
| if fileNodes[n.Info.File] == nil { |
| sourceFiles = append(sourceFiles, n) |
| } |
| fileNodes[n.Info.File] = append(fileNodes[n.Info.File], n) |
| } |
| |
| if len(sourceFiles) == 0 { |
| fmt.Fprintf(w, "No source information for %s\n", name) |
| continue |
| } |
| |
| sourceFiles.Sort(graph.FileOrder) |
| |
| // Print each file associated with this function. |
| for _, fl := range sourceFiles { |
| filename := fl.Info.File |
| fns := fileNodes[filename] |
| flatSum, cumSum := fns.Sum() |
| |
| fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0) |
| fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename) |
| fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", |
| rpt.formatValue(flatSum), rpt.formatValue(cumSum), |
| percentage(cumSum, rpt.total)) |
| |
| if err != nil { |
| fmt.Fprintf(w, " Error: %v\n", err) |
| continue |
| } |
| |
| for _, fn := range fnodes { |
| fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), fn.Info.Lineno, fn.Info.Name) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // printWebSource prints an annotated source listing, include all |
| // functions with samples that match the regexp rpt.options.symbol. |
| func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error { |
| printHeader(w, rpt) |
| if err := PrintWebList(w, rpt, obj, -1); err != nil { |
| return err |
| } |
| printPageClosing(w) |
| return nil |
| } |
| |
| // PrintWebList prints annotated source listing of rpt to w. |
| func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error { |
| o := rpt.options |
| g := rpt.newGraph(nil) |
| |
| // If the regexp source can be parsed as an address, also match |
| // functions that land on that address. |
| var address *uint64 |
| if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { |
| address = &hex |
| } |
| |
| sourcePath := o.SourcePath |
| if sourcePath == "" { |
| wd, err := os.Getwd() |
| if err != nil { |
| return fmt.Errorf("Could not stat current dir: %v", err) |
| } |
| sourcePath = wd |
| } |
| reader := newSourceReader(sourcePath) |
| |
| type fileFunction struct { |
| fileName, functionName string |
| } |
| |
| // Extract interesting symbols from binary files in the profile and |
| // classify samples per symbol. |
| symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj) |
| symNodes := nodesPerSymbol(g.Nodes, symbols) |
| |
| // Identify sources associated to a symbol by examining |
| // symbol samples. Classify samples per source file. |
| fileNodes := make(map[fileFunction]graph.Nodes) |
| if len(symNodes) == 0 { |
| for _, n := range g.Nodes { |
| if n.Info.File == "" || !o.Symbol.MatchString(n.Info.Name) { |
| continue |
| } |
| ff := fileFunction{n.Info.File, n.Info.Name} |
| fileNodes[ff] = append(fileNodes[ff], n) |
| } |
| } else { |
| for _, nodes := range symNodes { |
| for _, n := range nodes { |
| if n.Info.File != "" { |
| ff := fileFunction{n.Info.File, n.Info.Name} |
| fileNodes[ff] = append(fileNodes[ff], n) |
| } |
| } |
| } |
| } |
| |
| if len(fileNodes) == 0 { |
| return fmt.Errorf("No source information for %s", o.Symbol.String()) |
| } |
| |
| sourceFiles := make(graph.Nodes, 0, len(fileNodes)) |
| for _, nodes := range fileNodes { |
| sNode := *nodes[0] |
| sNode.Flat, sNode.Cum = nodes.Sum() |
| sourceFiles = append(sourceFiles, &sNode) |
| } |
| |
| // Limit number of files printed? |
| if maxFiles < 0 { |
| sourceFiles.Sort(graph.FileOrder) |
| } else { |
| sourceFiles.Sort(graph.FlatNameOrder) |
| if maxFiles < len(sourceFiles) { |
| sourceFiles = sourceFiles[:maxFiles] |
| } |
| } |
| |
| // Print each file associated with this function. |
| for _, n := range sourceFiles { |
| ff := fileFunction{n.Info.File, n.Info.Name} |
| fns := fileNodes[ff] |
| |
| asm := assemblyPerSourceLine(symbols, fns, ff.fileName, obj) |
| start, end := sourceCoordinates(asm) |
| |
| fnodes, path, err := getSourceFromFile(ff.fileName, reader, fns, start, end) |
| if err != nil { |
| fnodes, path = getMissingFunctionSource(ff.fileName, asm, start, end) |
| } |
| |
| printFunctionHeader(w, ff.functionName, path, n.Flat, n.Cum, rpt) |
| for _, fn := range fnodes { |
| printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], reader, rpt) |
| } |
| printFunctionClosing(w) |
| } |
| return nil |
| } |
| |
| // sourceCoordinates returns the lowest and highest line numbers from |
| // a set of assembly statements. |
| func sourceCoordinates(asm map[int][]assemblyInstruction) (start, end int) { |
| for l := range asm { |
| if start == 0 || l < start { |
| start = l |
| } |
| if end == 0 || l > end { |
| end = l |
| } |
| } |
| return start, end |
| } |
| |
| // assemblyPerSourceLine disassembles the binary containing a symbol |
| // and classifies the assembly instructions according to its |
| // corresponding source line, annotating them with a set of samples. |
| func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj plugin.ObjTool) map[int][]assemblyInstruction { |
| assembly := make(map[int][]assemblyInstruction) |
| // Identify symbol to use for this collection of samples. |
| o := findMatchingSymbol(objSyms, rs) |
| if o == nil { |
| return assembly |
| } |
| |
| // Extract assembly for matched symbol |
| insts, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End) |
| if err != nil { |
| return assembly |
| } |
| |
| srcBase := filepath.Base(src) |
| anodes := annotateAssembly(insts, rs, o.base) |
| var lineno = 0 |
| var prevline = 0 |
| for _, an := range anodes { |
| // Do not rely solely on the line number produced by Disasm |
| // since it is not what we want in the presence of inlining. |
| // |
| // E.g., suppose we are printing source code for F and this |
| // instruction is from H where F called G called H and both |
| // of those calls were inlined. We want to use the line |
| // number from F, not from H (which is what Disasm gives us). |
| // |
| // So find the outer-most linenumber in the source file. |
| found := false |
| if frames, err := o.file.SourceLine(an.address + o.base); err == nil { |
| for i := len(frames) - 1; i >= 0; i-- { |
| if filepath.Base(frames[i].File) == srcBase { |
| for j := i - 1; j >= 0; j-- { |
| an.inlineCalls = append(an.inlineCalls, callID{frames[j].File, frames[j].Line}) |
| } |
| lineno = frames[i].Line |
| found = true |
| break |
| } |
| } |
| } |
| if !found && filepath.Base(an.file) == srcBase { |
| lineno = an.line |
| } |
| |
| if lineno != 0 { |
| if lineno != prevline { |
| // This instruction starts a new block |
| // of contiguous instructions on this line. |
| an.startsBlock = true |
| } |
| prevline = lineno |
| assembly[lineno] = append(assembly[lineno], an) |
| } |
| } |
| |
| return assembly |
| } |
| |
| // findMatchingSymbol looks for the symbol that corresponds to a set |
| // of samples, by comparing their addresses. |
| func findMatchingSymbol(objSyms []*objSymbol, ns graph.Nodes) *objSymbol { |
| for _, n := range ns { |
| for _, o := range objSyms { |
| if filepath.Base(o.sym.File) == filepath.Base(n.Info.Objfile) && |
| o.sym.Start <= n.Info.Address-o.base && |
| n.Info.Address-o.base <= o.sym.End { |
| return o |
| } |
| } |
| } |
| return nil |
| } |
| |
| // printHeader prints the page header for a weblist report. |
| func printHeader(w io.Writer, rpt *Report) { |
| fmt.Fprintln(w, ` |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Pprof listing</title>`) |
| fmt.Fprintln(w, weblistPageCSS) |
| fmt.Fprintln(w, weblistPageScript) |
| fmt.Fprint(w, "</head>\n<body>\n\n") |
| |
| var labels []string |
| for _, l := range ProfileLabels(rpt) { |
| labels = append(labels, template.HTMLEscapeString(l)) |
| } |
| |
| fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`, |
| strings.Join(labels, "<br>\n"), |
| rpt.formatValue(rpt.total), |
| ) |
| } |
| |
| // printFunctionHeader prints a function header for a weblist report. |
| func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) { |
| fmt.Fprintf(w, `<h1>%s</h1>%s |
| <pre onClick="pprof_toggle_asm(event)"> |
| Total: %10s %10s (flat, cum) %s |
| `, |
| template.HTMLEscapeString(name), template.HTMLEscapeString(path), |
| rpt.formatValue(flatSum), rpt.formatValue(cumSum), |
| percentage(cumSum, rpt.total)) |
| } |
| |
| // printFunctionSourceLine prints a source line and the corresponding assembly. |
| func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) { |
| if len(assembly) == 0 { |
| fmt.Fprintf(w, |
| "<span class=line> %6d</span> <span class=nop> %10s %10s %8s %s </span>\n", |
| fn.Info.Lineno, |
| valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), |
| "", template.HTMLEscapeString(fn.Info.Name)) |
| return |
| } |
| |
| fmt.Fprintf(w, |
| "<span class=line> %6d</span> <span class=deadsrc> %10s %10s %8s %s </span>", |
| fn.Info.Lineno, |
| valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), |
| "", template.HTMLEscapeString(fn.Info.Name)) |
| srcIndent := indentation(fn.Info.Name) |
| fmt.Fprint(w, "<span class=asm>") |
| var curCalls []callID |
| for i, an := range assembly { |
| if an.startsBlock && i != 0 { |
| // Insert a separator between discontiguous blocks. |
| fmt.Fprintf(w, " %8s %28s\n", "", "⋮") |
| } |
| |
| var fileline string |
| if an.file != "" { |
| fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.file), an.line) |
| } |
| flat, cum := an.flat, an.cum |
| if an.flatDiv != 0 { |
| flat = flat / an.flatDiv |
| } |
| if an.cumDiv != 0 { |
| cum = cum / an.cumDiv |
| } |
| |
| // Print inlined call context. |
| for j, c := range an.inlineCalls { |
| if j < len(curCalls) && curCalls[j] == c { |
| // Skip if same as previous instruction. |
| continue |
| } |
| curCalls = nil |
| fname := trimPath(c.file) |
| fline, ok := reader.line(fname, c.line) |
| if !ok { |
| fline = "" |
| } |
| text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline) |
| fmt.Fprintf(w, " %8s %10s %10s %8s <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n", |
| "", "", "", "", |
| template.HTMLEscapeString(fmt.Sprintf("%-80s", text)), |
| template.HTMLEscapeString(filepath.Base(fname)), c.line) |
| } |
| curCalls = an.inlineCalls |
| text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction |
| fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n", |
| "", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address, |
| template.HTMLEscapeString(fmt.Sprintf("%-80s", text)), |
| template.HTMLEscapeString(fileline)) |
| } |
| fmt.Fprintln(w, "</span>") |
| } |
| |
| // printFunctionClosing prints the end of a function in a weblist report. |
| func printFunctionClosing(w io.Writer) { |
| fmt.Fprintln(w, "</pre>") |
| } |
| |
| // printPageClosing prints the end of the page in a weblist report. |
| func printPageClosing(w io.Writer) { |
| fmt.Fprintln(w, weblistPageClosing) |
| } |
| |
| // getSourceFromFile collects the sources of a function from a source |
| // file and annotates it with the samples in fns. Returns the sources |
| // as nodes, using the info.name field to hold the source code. |
| func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) { |
| file = trimPath(file) |
| lineNodes := make(map[int]graph.Nodes) |
| |
| // Collect source coordinates from profile. |
| const margin = 5 // Lines before first/after last sample. |
| if start == 0 { |
| if fns[0].Info.StartLine != 0 { |
| start = fns[0].Info.StartLine |
| } else { |
| start = fns[0].Info.Lineno - margin |
| } |
| } else { |
| start -= margin |
| } |
| if end == 0 { |
| end = fns[0].Info.Lineno |
| } |
| end += margin |
| for _, n := range fns { |
| lineno := n.Info.Lineno |
| nodeStart := n.Info.StartLine |
| if nodeStart == 0 { |
| nodeStart = lineno - margin |
| } |
| nodeEnd := lineno + margin |
| if nodeStart < start { |
| start = nodeStart |
| } else if nodeEnd > end { |
| end = nodeEnd |
| } |
| lineNodes[lineno] = append(lineNodes[lineno], n) |
| } |
| if start < 1 { |
| start = 1 |
| } |
| |
| var src graph.Nodes |
| for lineno := start; lineno <= end; lineno++ { |
| line, ok := reader.line(file, lineno) |
| if !ok { |
| break |
| } |
| flat, cum := lineNodes[lineno].Sum() |
| src = append(src, &graph.Node{ |
| Info: graph.NodeInfo{ |
| Name: strings.TrimRight(line, "\n"), |
| Lineno: lineno, |
| }, |
| Flat: flat, |
| Cum: cum, |
| }) |
| } |
| if err := reader.fileError(file); err != nil { |
| return nil, file, err |
| } |
| return src, file, nil |
| } |
| |
| // getMissingFunctionSource creates a dummy function body to point to |
| // the source file and annotates it with the samples in asm. |
| func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction, start, end int) (graph.Nodes, string) { |
| var fnodes graph.Nodes |
| for i := start; i <= end; i++ { |
| insts := asm[i] |
| if len(insts) == 0 { |
| continue |
| } |
| var group assemblyInstruction |
| for _, insn := range insts { |
| group.flat += insn.flat |
| group.cum += insn.cum |
| group.flatDiv += insn.flatDiv |
| group.cumDiv += insn.cumDiv |
| } |
| flat := group.flatValue() |
| cum := group.cumValue() |
| fnodes = append(fnodes, &graph.Node{ |
| Info: graph.NodeInfo{ |
| Name: "???", |
| Lineno: i, |
| }, |
| Flat: flat, |
| Cum: cum, |
| }) |
| } |
| return fnodes, filename |
| } |
| |
| // sourceReader provides access to source code with caching of file contents. |
| type sourceReader struct { |
| searchPath string |
| |
| // files maps from path name to a list of lines. |
| // files[*][0] is unused since line numbering starts at 1. |
| files map[string][]string |
| |
| // errors collects errors encountered per file. These errors are |
| // consulted before returning out of these module. |
| errors map[string]error |
| } |
| |
| func newSourceReader(searchPath string) *sourceReader { |
| return &sourceReader{ |
| searchPath, |
| make(map[string][]string), |
| make(map[string]error), |
| } |
| } |
| |
| func (reader *sourceReader) fileError(path string) error { |
| return reader.errors[path] |
| } |
| |
| func (reader *sourceReader) line(path string, lineno int) (string, bool) { |
| lines, ok := reader.files[path] |
| if !ok { |
| // Read and cache file contents. |
| lines = []string{""} // Skip 0th line |
| f, err := openSourceFile(path, reader.searchPath) |
| if err != nil { |
| reader.errors[path] = err |
| } else { |
| s := bufio.NewScanner(f) |
| for s.Scan() { |
| lines = append(lines, s.Text()) |
| } |
| f.Close() |
| if s.Err() != nil { |
| reader.errors[path] = err |
| } |
| } |
| reader.files[path] = lines |
| } |
| if lineno <= 0 || lineno >= len(lines) { |
| return "", false |
| } |
| return lines[lineno], true |
| } |
| |
| // openSourceFile opens a source file from a name encoded in a |
| // profile. File names in a profile after often relative paths, so |
| // search them in each of the paths in searchPath (or CWD by default), |
| // and their parents. |
| func openSourceFile(path, searchPath string) (*os.File, error) { |
| if filepath.IsAbs(path) { |
| f, err := os.Open(path) |
| return f, err |
| } |
| |
| // Scan each component of the path |
| for _, dir := range strings.Split(searchPath, ":") { |
| // Search up for every parent of each possible path. |
| for { |
| filename := filepath.Join(dir, path) |
| if f, err := os.Open(filename); err == nil { |
| return f, nil |
| } |
| parent := filepath.Dir(dir) |
| if parent == dir { |
| break |
| } |
| dir = parent |
| } |
| } |
| |
| return nil, fmt.Errorf("Could not find file %s on path %s", path, searchPath) |
| } |
| |
| // trimPath cleans up a path by removing prefixes that are commonly |
| // found on profiles. |
| func trimPath(path string) string { |
| basePaths := []string{ |
| "/proc/self/cwd/./", |
| "/proc/self/cwd/", |
| } |
| |
| sPath := filepath.ToSlash(path) |
| |
| for _, base := range basePaths { |
| if strings.HasPrefix(sPath, base) { |
| return filepath.FromSlash(sPath[len(base):]) |
| } |
| } |
| return path |
| } |
| |
| func indentation(line string) int { |
| column := 0 |
| for _, c := range line { |
| if c == ' ' { |
| column++ |
| } else if c == '\t' { |
| column++ |
| for column%8 != 0 { |
| column++ |
| } |
| } else { |
| break |
| } |
| } |
| return column |
| } |