Home | History | Annotate | Download | only in report
      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 report
     16 
     17 // This file contains routines related to the generation of annotated
     18 // source listings.
     19 
     20 import (
     21 	"bufio"
     22 	"fmt"
     23 	"html/template"
     24 	"io"
     25 	"os"
     26 	"path/filepath"
     27 	"strconv"
     28 	"strings"
     29 
     30 	"github.com/google/pprof/internal/graph"
     31 	"github.com/google/pprof/internal/plugin"
     32 )
     33 
     34 // printSource prints an annotated source listing, include all
     35 // functions with samples that match the regexp rpt.options.symbol.
     36 // The sources are sorted by function name and then by filename to
     37 // eliminate potential nondeterminism.
     38 func printSource(w io.Writer, rpt *Report) error {
     39 	o := rpt.options
     40 	g := rpt.newGraph(nil)
     41 
     42 	// Identify all the functions that match the regexp provided.
     43 	// Group nodes for each matching function.
     44 	var functions graph.Nodes
     45 	functionNodes := make(map[string]graph.Nodes)
     46 	for _, n := range g.Nodes {
     47 		if !o.Symbol.MatchString(n.Info.Name) {
     48 			continue
     49 		}
     50 		if functionNodes[n.Info.Name] == nil {
     51 			functions = append(functions, n)
     52 		}
     53 		functionNodes[n.Info.Name] = append(functionNodes[n.Info.Name], n)
     54 	}
     55 	functions.Sort(graph.NameOrder)
     56 
     57 	sourcePath := o.SourcePath
     58 	if sourcePath == "" {
     59 		wd, err := os.Getwd()
     60 		if err != nil {
     61 			return fmt.Errorf("Could not stat current dir: %v", err)
     62 		}
     63 		sourcePath = wd
     64 	}
     65 	reader := newSourceReader(sourcePath)
     66 
     67 	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
     68 	for _, fn := range functions {
     69 		name := fn.Info.Name
     70 
     71 		// Identify all the source files associated to this function.
     72 		// Group nodes for each source file.
     73 		var sourceFiles graph.Nodes
     74 		fileNodes := make(map[string]graph.Nodes)
     75 		for _, n := range functionNodes[name] {
     76 			if n.Info.File == "" {
     77 				continue
     78 			}
     79 			if fileNodes[n.Info.File] == nil {
     80 				sourceFiles = append(sourceFiles, n)
     81 			}
     82 			fileNodes[n.Info.File] = append(fileNodes[n.Info.File], n)
     83 		}
     84 
     85 		if len(sourceFiles) == 0 {
     86 			fmt.Fprintf(w, "No source information for %s\n", name)
     87 			continue
     88 		}
     89 
     90 		sourceFiles.Sort(graph.FileOrder)
     91 
     92 		// Print each file associated with this function.
     93 		for _, fl := range sourceFiles {
     94 			filename := fl.Info.File
     95 			fns := fileNodes[filename]
     96 			flatSum, cumSum := fns.Sum()
     97 
     98 			fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0)
     99 			fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename)
    100 			fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
    101 				rpt.formatValue(flatSum), rpt.formatValue(cumSum),
    102 				percentage(cumSum, rpt.total))
    103 
    104 			if err != nil {
    105 				fmt.Fprintf(w, " Error: %v\n", err)
    106 				continue
    107 			}
    108 
    109 			for _, fn := range fnodes {
    110 				fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), fn.Info.Lineno, fn.Info.Name)
    111 			}
    112 		}
    113 	}
    114 	return nil
    115 }
    116 
    117 // printWebSource prints an annotated source listing, include all
    118 // functions with samples that match the regexp rpt.options.symbol.
    119 func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
    120 	printHeader(w, rpt)
    121 	if err := PrintWebList(w, rpt, obj, -1); err != nil {
    122 		return err
    123 	}
    124 	printPageClosing(w)
    125 	return nil
    126 }
    127 
    128 // PrintWebList prints annotated source listing of rpt to w.
    129 func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error {
    130 	o := rpt.options
    131 	g := rpt.newGraph(nil)
    132 
    133 	// If the regexp source can be parsed as an address, also match
    134 	// functions that land on that address.
    135 	var address *uint64
    136 	if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil {
    137 		address = &hex
    138 	}
    139 
    140 	sourcePath := o.SourcePath
    141 	if sourcePath == "" {
    142 		wd, err := os.Getwd()
    143 		if err != nil {
    144 			return fmt.Errorf("Could not stat current dir: %v", err)
    145 		}
    146 		sourcePath = wd
    147 	}
    148 	reader := newSourceReader(sourcePath)
    149 
    150 	type fileFunction struct {
    151 		fileName, functionName string
    152 	}
    153 
    154 	// Extract interesting symbols from binary files in the profile and
    155 	// classify samples per symbol.
    156 	symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj)
    157 	symNodes := nodesPerSymbol(g.Nodes, symbols)
    158 
    159 	// Identify sources associated to a symbol by examining
    160 	// symbol samples. Classify samples per source file.
    161 	fileNodes := make(map[fileFunction]graph.Nodes)
    162 	if len(symNodes) == 0 {
    163 		for _, n := range g.Nodes {
    164 			if n.Info.File == "" || !o.Symbol.MatchString(n.Info.Name) {
    165 				continue
    166 			}
    167 			ff := fileFunction{n.Info.File, n.Info.Name}
    168 			fileNodes[ff] = append(fileNodes[ff], n)
    169 		}
    170 	} else {
    171 		for _, nodes := range symNodes {
    172 			for _, n := range nodes {
    173 				if n.Info.File != "" {
    174 					ff := fileFunction{n.Info.File, n.Info.Name}
    175 					fileNodes[ff] = append(fileNodes[ff], n)
    176 				}
    177 			}
    178 		}
    179 	}
    180 
    181 	if len(fileNodes) == 0 {
    182 		return fmt.Errorf("No source information for %s", o.Symbol.String())
    183 	}
    184 
    185 	sourceFiles := make(graph.Nodes, 0, len(fileNodes))
    186 	for _, nodes := range fileNodes {
    187 		sNode := *nodes[0]
    188 		sNode.Flat, sNode.Cum = nodes.Sum()
    189 		sourceFiles = append(sourceFiles, &sNode)
    190 	}
    191 
    192 	// Limit number of files printed?
    193 	if maxFiles < 0 {
    194 		sourceFiles.Sort(graph.FileOrder)
    195 	} else {
    196 		sourceFiles.Sort(graph.FlatNameOrder)
    197 		if maxFiles < len(sourceFiles) {
    198 			sourceFiles = sourceFiles[:maxFiles]
    199 		}
    200 	}
    201 
    202 	// Print each file associated with this function.
    203 	for _, n := range sourceFiles {
    204 		ff := fileFunction{n.Info.File, n.Info.Name}
    205 		fns := fileNodes[ff]
    206 
    207 		asm := assemblyPerSourceLine(symbols, fns, ff.fileName, obj)
    208 		start, end := sourceCoordinates(asm)
    209 
    210 		fnodes, path, err := getSourceFromFile(ff.fileName, reader, fns, start, end)
    211 		if err != nil {
    212 			fnodes, path = getMissingFunctionSource(ff.fileName, asm, start, end)
    213 		}
    214 
    215 		printFunctionHeader(w, ff.functionName, path, n.Flat, n.Cum, rpt)
    216 		for _, fn := range fnodes {
    217 			printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], reader, rpt)
    218 		}
    219 		printFunctionClosing(w)
    220 	}
    221 	return nil
    222 }
    223 
    224 // sourceCoordinates returns the lowest and highest line numbers from
    225 // a set of assembly statements.
    226 func sourceCoordinates(asm map[int][]assemblyInstruction) (start, end int) {
    227 	for l := range asm {
    228 		if start == 0 || l < start {
    229 			start = l
    230 		}
    231 		if end == 0 || l > end {
    232 			end = l
    233 		}
    234 	}
    235 	return start, end
    236 }
    237 
    238 // assemblyPerSourceLine disassembles the binary containing a symbol
    239 // and classifies the assembly instructions according to its
    240 // corresponding source line, annotating them with a set of samples.
    241 func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj plugin.ObjTool) map[int][]assemblyInstruction {
    242 	assembly := make(map[int][]assemblyInstruction)
    243 	// Identify symbol to use for this collection of samples.
    244 	o := findMatchingSymbol(objSyms, rs)
    245 	if o == nil {
    246 		return assembly
    247 	}
    248 
    249 	// Extract assembly for matched symbol
    250 	insts, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End)
    251 	if err != nil {
    252 		return assembly
    253 	}
    254 
    255 	srcBase := filepath.Base(src)
    256 	anodes := annotateAssembly(insts, rs, o.base)
    257 	var lineno = 0
    258 	var prevline = 0
    259 	for _, an := range anodes {
    260 		// Do not rely solely on the line number produced by Disasm
    261 		// since it is not what we want in the presence of inlining.
    262 		//
    263 		// E.g., suppose we are printing source code for F and this
    264 		// instruction is from H where F called G called H and both
    265 		// of those calls were inlined.  We want to use the line
    266 		// number from F, not from H (which is what Disasm gives us).
    267 		//
    268 		// So find the outer-most linenumber in the source file.
    269 		found := false
    270 		if frames, err := o.file.SourceLine(an.address + o.base); err == nil {
    271 			for i := len(frames) - 1; i >= 0; i-- {
    272 				if filepath.Base(frames[i].File) == srcBase {
    273 					for j := i - 1; j >= 0; j-- {
    274 						an.inlineCalls = append(an.inlineCalls, callID{frames[j].File, frames[j].Line})
    275 					}
    276 					lineno = frames[i].Line
    277 					found = true
    278 					break
    279 				}
    280 			}
    281 		}
    282 		if !found && filepath.Base(an.file) == srcBase {
    283 			lineno = an.line
    284 		}
    285 
    286 		if lineno != 0 {
    287 			if lineno != prevline {
    288 				// This instruction starts a new block
    289 				// of contiguous instructions on this line.
    290 				an.startsBlock = true
    291 			}
    292 			prevline = lineno
    293 			assembly[lineno] = append(assembly[lineno], an)
    294 		}
    295 	}
    296 
    297 	return assembly
    298 }
    299 
    300 // findMatchingSymbol looks for the symbol that corresponds to a set
    301 // of samples, by comparing their addresses.
    302 func findMatchingSymbol(objSyms []*objSymbol, ns graph.Nodes) *objSymbol {
    303 	for _, n := range ns {
    304 		for _, o := range objSyms {
    305 			if filepath.Base(o.sym.File) == filepath.Base(n.Info.Objfile) &&
    306 				o.sym.Start <= n.Info.Address-o.base &&
    307 				n.Info.Address-o.base <= o.sym.End {
    308 				return o
    309 			}
    310 		}
    311 	}
    312 	return nil
    313 }
    314 
    315 // printHeader prints the page header for a weblist report.
    316 func printHeader(w io.Writer, rpt *Report) {
    317 	fmt.Fprintln(w, `
    318 <!DOCTYPE html>
    319 <html>
    320 <head>
    321 <meta charset="UTF-8">
    322 <title>Pprof listing</title>`)
    323 	fmt.Fprintln(w, weblistPageCSS)
    324 	fmt.Fprintln(w, weblistPageScript)
    325 	fmt.Fprint(w, "</head>\n<body>\n\n")
    326 
    327 	var labels []string
    328 	for _, l := range ProfileLabels(rpt) {
    329 		labels = append(labels, template.HTMLEscapeString(l))
    330 	}
    331 
    332 	fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`,
    333 		strings.Join(labels, "<br>\n"),
    334 		rpt.formatValue(rpt.total),
    335 	)
    336 }
    337 
    338 // printFunctionHeader prints a function header for a weblist report.
    339 func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) {
    340 	fmt.Fprintf(w, `<h1>%s</h1>%s
    341 <pre onClick="pprof_toggle_asm(event)">
    342   Total:  %10s %10s (flat, cum) %s
    343 `,
    344 		template.HTMLEscapeString(name), template.HTMLEscapeString(path),
    345 		rpt.formatValue(flatSum), rpt.formatValue(cumSum),
    346 		percentage(cumSum, rpt.total))
    347 }
    348 
    349 // printFunctionSourceLine prints a source line and the corresponding assembly.
    350 func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
    351 	if len(assembly) == 0 {
    352 		fmt.Fprintf(w,
    353 			"<span class=line> %6d</span> <span class=nop>  %10s %10s %8s  %s </span>\n",
    354 			fn.Info.Lineno,
    355 			valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
    356 			"", template.HTMLEscapeString(fn.Info.Name))
    357 		return
    358 	}
    359 
    360 	fmt.Fprintf(w,
    361 		"<span class=line> %6d</span> <span class=deadsrc>  %10s %10s %8s  %s </span>",
    362 		fn.Info.Lineno,
    363 		valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
    364 		"", template.HTMLEscapeString(fn.Info.Name))
    365 	srcIndent := indentation(fn.Info.Name)
    366 	fmt.Fprint(w, "<span class=asm>")
    367 	var curCalls []callID
    368 	for i, an := range assembly {
    369 		if an.startsBlock && i != 0 {
    370 			// Insert a separator between discontiguous blocks.
    371 			fmt.Fprintf(w, " %8s %28s\n", "", "")
    372 		}
    373 
    374 		var fileline string
    375 		if an.file != "" {
    376 			fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.file), an.line)
    377 		}
    378 		flat, cum := an.flat, an.cum
    379 		if an.flatDiv != 0 {
    380 			flat = flat / an.flatDiv
    381 		}
    382 		if an.cumDiv != 0 {
    383 			cum = cum / an.cumDiv
    384 		}
    385 
    386 		// Print inlined call context.
    387 		for j, c := range an.inlineCalls {
    388 			if j < len(curCalls) && curCalls[j] == c {
    389 				// Skip if same as previous instruction.
    390 				continue
    391 			}
    392 			curCalls = nil
    393 			fname := trimPath(c.file)
    394 			fline, ok := reader.line(fname, c.line)
    395 			if !ok {
    396 				fline = ""
    397 			}
    398 			text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline)
    399 			fmt.Fprintf(w, " %8s %10s %10s %8s  <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
    400 				"", "", "", "",
    401 				template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
    402 				template.HTMLEscapeString(filepath.Base(fname)), c.line)
    403 		}
    404 		curCalls = an.inlineCalls
    405 		text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
    406 		fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n",
    407 			"", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address,
    408 			template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
    409 			template.HTMLEscapeString(fileline))
    410 	}
    411 	fmt.Fprintln(w, "</span>")
    412 }
    413 
    414 // printFunctionClosing prints the end of a function in a weblist report.
    415 func printFunctionClosing(w io.Writer) {
    416 	fmt.Fprintln(w, "</pre>")
    417 }
    418 
    419 // printPageClosing prints the end of the page in a weblist report.
    420 func printPageClosing(w io.Writer) {
    421 	fmt.Fprintln(w, weblistPageClosing)
    422 }
    423 
    424 // getSourceFromFile collects the sources of a function from a source
    425 // file and annotates it with the samples in fns. Returns the sources
    426 // as nodes, using the info.name field to hold the source code.
    427 func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
    428 	file = trimPath(file)
    429 	lineNodes := make(map[int]graph.Nodes)
    430 
    431 	// Collect source coordinates from profile.
    432 	const margin = 5 // Lines before first/after last sample.
    433 	if start == 0 {
    434 		if fns[0].Info.StartLine != 0 {
    435 			start = fns[0].Info.StartLine
    436 		} else {
    437 			start = fns[0].Info.Lineno - margin
    438 		}
    439 	} else {
    440 		start -= margin
    441 	}
    442 	if end == 0 {
    443 		end = fns[0].Info.Lineno
    444 	}
    445 	end += margin
    446 	for _, n := range fns {
    447 		lineno := n.Info.Lineno
    448 		nodeStart := n.Info.StartLine
    449 		if nodeStart == 0 {
    450 			nodeStart = lineno - margin
    451 		}
    452 		nodeEnd := lineno + margin
    453 		if nodeStart < start {
    454 			start = nodeStart
    455 		} else if nodeEnd > end {
    456 			end = nodeEnd
    457 		}
    458 		lineNodes[lineno] = append(lineNodes[lineno], n)
    459 	}
    460 	if start < 1 {
    461 		start = 1
    462 	}
    463 
    464 	var src graph.Nodes
    465 	for lineno := start; lineno <= end; lineno++ {
    466 		line, ok := reader.line(file, lineno)
    467 		if !ok {
    468 			break
    469 		}
    470 		flat, cum := lineNodes[lineno].Sum()
    471 		src = append(src, &graph.Node{
    472 			Info: graph.NodeInfo{
    473 				Name:   strings.TrimRight(line, "\n"),
    474 				Lineno: lineno,
    475 			},
    476 			Flat: flat,
    477 			Cum:  cum,
    478 		})
    479 	}
    480 	if err := reader.fileError(file); err != nil {
    481 		return nil, file, err
    482 	}
    483 	return src, file, nil
    484 }
    485 
    486 // getMissingFunctionSource creates a dummy function body to point to
    487 // the source file and annotates it with the samples in asm.
    488 func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction, start, end int) (graph.Nodes, string) {
    489 	var fnodes graph.Nodes
    490 	for i := start; i <= end; i++ {
    491 		insts := asm[i]
    492 		if len(insts) == 0 {
    493 			continue
    494 		}
    495 		var group assemblyInstruction
    496 		for _, insn := range insts {
    497 			group.flat += insn.flat
    498 			group.cum += insn.cum
    499 			group.flatDiv += insn.flatDiv
    500 			group.cumDiv += insn.cumDiv
    501 		}
    502 		flat := group.flatValue()
    503 		cum := group.cumValue()
    504 		fnodes = append(fnodes, &graph.Node{
    505 			Info: graph.NodeInfo{
    506 				Name:   "???",
    507 				Lineno: i,
    508 			},
    509 			Flat: flat,
    510 			Cum:  cum,
    511 		})
    512 	}
    513 	return fnodes, filename
    514 }
    515 
    516 // sourceReader provides access to source code with caching of file contents.
    517 type sourceReader struct {
    518 	searchPath string
    519 
    520 	// files maps from path name to a list of lines.
    521 	// files[*][0] is unused since line numbering starts at 1.
    522 	files map[string][]string
    523 
    524 	// errors collects errors encountered per file.  These errors are
    525 	// consulted before returning out of these module.
    526 	errors map[string]error
    527 }
    528 
    529 func newSourceReader(searchPath string) *sourceReader {
    530 	return &sourceReader{
    531 		searchPath,
    532 		make(map[string][]string),
    533 		make(map[string]error),
    534 	}
    535 }
    536 
    537 func (reader *sourceReader) fileError(path string) error {
    538 	return reader.errors[path]
    539 }
    540 
    541 func (reader *sourceReader) line(path string, lineno int) (string, bool) {
    542 	lines, ok := reader.files[path]
    543 	if !ok {
    544 		// Read and cache file contents.
    545 		lines = []string{""} // Skip 0th line
    546 		f, err := openSourceFile(path, reader.searchPath)
    547 		if err != nil {
    548 			reader.errors[path] = err
    549 		} else {
    550 			s := bufio.NewScanner(f)
    551 			for s.Scan() {
    552 				lines = append(lines, s.Text())
    553 			}
    554 			f.Close()
    555 			if s.Err() != nil {
    556 				reader.errors[path] = err
    557 			}
    558 		}
    559 		reader.files[path] = lines
    560 	}
    561 	if lineno <= 0 || lineno >= len(lines) {
    562 		return "", false
    563 	}
    564 	return lines[lineno], true
    565 }
    566 
    567 // openSourceFile opens a source file from a name encoded in a
    568 // profile. File names in a profile after often relative paths, so
    569 // search them in each of the paths in searchPath (or CWD by default),
    570 // and their parents.
    571 func openSourceFile(path, searchPath string) (*os.File, error) {
    572 	if filepath.IsAbs(path) {
    573 		f, err := os.Open(path)
    574 		return f, err
    575 	}
    576 
    577 	// Scan each component of the path
    578 	for _, dir := range strings.Split(searchPath, ":") {
    579 		// Search up for every parent of each possible path.
    580 		for {
    581 			filename := filepath.Join(dir, path)
    582 			if f, err := os.Open(filename); err == nil {
    583 				return f, nil
    584 			}
    585 			parent := filepath.Dir(dir)
    586 			if parent == dir {
    587 				break
    588 			}
    589 			dir = parent
    590 		}
    591 	}
    592 
    593 	return nil, fmt.Errorf("Could not find file %s on path %s", path, searchPath)
    594 }
    595 
    596 // trimPath cleans up a path by removing prefixes that are commonly
    597 // found on profiles.
    598 func trimPath(path string) string {
    599 	basePaths := []string{
    600 		"/proc/self/cwd/./",
    601 		"/proc/self/cwd/",
    602 	}
    603 
    604 	sPath := filepath.ToSlash(path)
    605 
    606 	for _, base := range basePaths {
    607 		if strings.HasPrefix(sPath, base) {
    608 			return filepath.FromSlash(sPath[len(base):])
    609 		}
    610 	}
    611 	return path
    612 }
    613 
    614 func indentation(line string) int {
    615 	column := 0
    616 	for _, c := range line {
    617 		if c == ' ' {
    618 			column++
    619 		} else if c == '\t' {
    620 			column++
    621 			for column%8 != 0 {
    622 				column++
    623 			}
    624 		} else {
    625 			break
    626 		}
    627 	}
    628 	return column
    629 }
    630