| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package report |
| |
| // This file contains routines related to the generation of annotated |
| // source listings. |
| |
| import ( |
| "bufio" |
| "fmt" |
| "html/template" |
| "io" |
| "os" |
| "path/filepath" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "cmd/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, err := newGraph(rpt) |
| if err != nil { |
| return err |
| } |
| |
| // Identify all the functions that match the regexp provided. |
| // Group nodes for each matching function. |
| var functions nodes |
| functionNodes := make(map[string]nodes) |
| for _, n := range g.ns { |
| 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(nameOrder) |
| |
| 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 nodes |
| fileNodes := make(map[string]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.Printf("No source information for %s\n", name) |
| continue |
| } |
| |
| sourceFiles.sort(fileOrder) |
| |
| // Print each file associated with this function. |
| for _, fl := range sourceFiles { |
| filename := fl.info.file |
| fns := fileNodes[filename] |
| flatSum, cumSum := sumNodes(fns) |
| |
| fnodes, path, err := getFunctionSource(name, filename, fns, 0, 0) |
| fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, path) |
| 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 { |
| o := rpt.options |
| g, err := newGraph(rpt) |
| if err != nil { |
| return err |
| } |
| |
| // 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 |
| } |
| |
| // 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.ns, symbols) |
| |
| // Sort symbols for printing. |
| var syms objSymbols |
| for s := range symNodes { |
| syms = append(syms, s) |
| } |
| sort.Sort(syms) |
| |
| if len(syms) == 0 { |
| return fmt.Errorf("no samples found on routines matching: %s", o.Symbol.String()) |
| } |
| |
| printHeader(w, rpt) |
| for _, s := range syms { |
| name := s.sym.Name[0] |
| // Identify sources associated to a symbol by examining |
| // symbol samples. Classify samples per source file. |
| var sourceFiles nodes |
| fileNodes := make(map[string]nodes) |
| for _, n := range symNodes[s] { |
| 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.Printf("No source information for %s\n", name) |
| continue |
| } |
| |
| sourceFiles.sort(fileOrder) |
| |
| // Print each file associated with this function. |
| for _, fl := range sourceFiles { |
| filename := fl.info.file |
| fns := fileNodes[filename] |
| |
| asm := assemblyPerSourceLine(symbols, fns, filename, obj) |
| start, end := sourceCoordinates(asm) |
| |
| fnodes, path, err := getFunctionSource(name, filename, fns, start, end) |
| if err != nil { |
| fnodes, path = getMissingFunctionSource(filename, asm, start, end) |
| } |
| |
| flatSum, cumSum := sumNodes(fnodes) |
| printFunctionHeader(w, name, path, flatSum, cumSum, rpt) |
| for _, fn := range fnodes { |
| printFunctionSourceLine(w, fn, asm[fn.info.lineno], rpt) |
| } |
| printFunctionClosing(w) |
| } |
| } |
| printPageClosing(w) |
| return nil |
| } |
| |
| // sourceCoordinates returns the lowest and highest line numbers from |
| // a set of assembly statements. |
| func sourceCoordinates(asm map[int]nodes) (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 nodes, src string, obj plugin.ObjTool) map[int]nodes { |
| assembly := make(map[int]nodes) |
| // Identify symbol to use for this collection of samples. |
| o := findMatchingSymbol(objSyms, rs) |
| if o == nil { |
| return assembly |
| } |
| |
| // Extract assembly for matched symbol |
| insns, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End) |
| if err != nil { |
| return assembly |
| } |
| |
| srcBase := filepath.Base(src) |
| anodes := annotateAssembly(insns, rs, o.base) |
| var lineno = 0 |
| for _, an := range anodes { |
| if filepath.Base(an.info.file) == srcBase { |
| lineno = an.info.lineno |
| } |
| if lineno != 0 { |
| 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 nodes) *objSymbol { |
| for _, n := range ns { |
| for _, o := range objSyms { |
| if filepath.Base(o.sym.File) == 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, weblistPageHeader) |
| |
| var labels []string |
| for _, l := range legendLabels(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()"> |
| 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 *node, assembly nodes, rpt *Report) { |
| if len(assembly) == 0 { |
| fmt.Fprintf(w, |
| "<span class=line> %6d</span> <span class=nop> %10s %10s %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 %s </span>", |
| fn.info.lineno, |
| valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), |
| template.HTMLEscapeString(fn.info.name)) |
| fmt.Fprint(w, "<span class=asm>") |
| for _, an := range assembly { |
| var fileline string |
| class := "disasmloc" |
| if an.info.file != "" { |
| fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.info.file), an.info.lineno) |
| if an.info.lineno != fn.info.lineno { |
| class = "unimportant" |
| } |
| } |
| fmt.Fprintf(w, " %8s %10s %10s %8x: %-48s <span class=%s>%s</span>\n", "", |
| valueOrDot(an.flat, rpt), valueOrDot(an.cum, rpt), |
| an.info.address, |
| template.HTMLEscapeString(an.info.name), |
| class, |
| 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) |
| } |
| |
| // getFunctionSource 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 getFunctionSource(fun, file string, fns nodes, start, end int) (nodes, string, error) { |
| f, file, err := adjustSourcePath(file) |
| if err != nil { |
| return nil, file, err |
| } |
| |
| lineNodes := make(map[int]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) |
| } |
| |
| var src nodes |
| buf := bufio.NewReader(f) |
| lineno := 1 |
| for { |
| line, err := buf.ReadString('\n') |
| if err != nil { |
| if line == "" || err != io.EOF { |
| return nil, file, err |
| } |
| } |
| if lineno >= start { |
| flat, cum := sumNodes(lineNodes[lineno]) |
| |
| src = append(src, &node{ |
| info: nodeInfo{ |
| name: strings.TrimRight(line, "\n"), |
| lineno: lineno, |
| }, |
| flat: flat, |
| cum: cum, |
| }) |
| } |
| lineno++ |
| if lineno > end { |
| break |
| } |
| } |
| 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]nodes, start, end int) (nodes, string) { |
| var fnodes nodes |
| for i := start; i <= end; i++ { |
| lrs := asm[i] |
| if len(lrs) == 0 { |
| continue |
| } |
| flat, cum := sumNodes(lrs) |
| fnodes = append(fnodes, &node{ |
| info: nodeInfo{ |
| name: "???", |
| lineno: i, |
| }, |
| flat: flat, |
| cum: cum, |
| }) |
| } |
| return fnodes, filename |
| } |
| |
| // adjustSourcePath adjusts the pathe for a source file by trimmming |
| // known prefixes and searching for the file on all parents of the |
| // current working dir. |
| func adjustSourcePath(path string) (*os.File, string, error) { |
| path = trimPath(path) |
| f, err := os.Open(path) |
| if err == nil { |
| return f, path, nil |
| } |
| |
| if dir, wderr := os.Getwd(); wderr == nil { |
| for { |
| parent := filepath.Dir(dir) |
| if parent == dir { |
| break |
| } |
| if f, err := os.Open(filepath.Join(parent, path)); err == nil { |
| return f, filepath.Join(parent, path), nil |
| } |
| |
| dir = parent |
| } |
| } |
| |
| return nil, path, err |
| } |
| |
| // 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 |
| } |