// 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 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(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 *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 err != io.EOF {
return nil, file, err
}
if line == "" {
// end was at or past EOF; that's okay
break
}
}
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 path for a source file by trimming
// 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
}