Home | History | Annotate | Download | only in report
      1 // Copyright 2014 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package report
      6 
      7 // This file contains routines related to the generation of annotated
      8 // source listings.
      9 
     10 import (
     11 	"bufio"
     12 	"fmt"
     13 	"html/template"
     14 	"io"
     15 	"os"
     16 	"path/filepath"
     17 	"sort"
     18 	"strconv"
     19 	"strings"
     20 
     21 	"cmd/pprof/internal/plugin"
     22 )
     23 
     24 // printSource prints an annotated source listing, include all
     25 // functions with samples that match the regexp rpt.options.symbol.
     26 // The sources are sorted by function name and then by filename to
     27 // eliminate potential nondeterminism.
     28 func printSource(w io.Writer, rpt *Report) error {
     29 	o := rpt.options
     30 	g, err := newGraph(rpt)
     31 	if err != nil {
     32 		return err
     33 	}
     34 
     35 	// Identify all the functions that match the regexp provided.
     36 	// Group nodes for each matching function.
     37 	var functions nodes
     38 	functionNodes := make(map[string]nodes)
     39 	for _, n := range g.ns {
     40 		if !o.Symbol.MatchString(n.info.name) {
     41 			continue
     42 		}
     43 		if functionNodes[n.info.name] == nil {
     44 			functions = append(functions, n)
     45 		}
     46 		functionNodes[n.info.name] = append(functionNodes[n.info.name], n)
     47 	}
     48 	functions.sort(nameOrder)
     49 
     50 	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
     51 	for _, fn := range functions {
     52 		name := fn.info.name
     53 
     54 		// Identify all the source files associated to this function.
     55 		// Group nodes for each source file.
     56 		var sourceFiles nodes
     57 		fileNodes := make(map[string]nodes)
     58 		for _, n := range functionNodes[name] {
     59 			if n.info.file == "" {
     60 				continue
     61 			}
     62 			if fileNodes[n.info.file] == nil {
     63 				sourceFiles = append(sourceFiles, n)
     64 			}
     65 			fileNodes[n.info.file] = append(fileNodes[n.info.file], n)
     66 		}
     67 
     68 		if len(sourceFiles) == 0 {
     69 			fmt.Printf("No source information for %s\n", name)
     70 			continue
     71 		}
     72 
     73 		sourceFiles.sort(fileOrder)
     74 
     75 		// Print each file associated with this function.
     76 		for _, fl := range sourceFiles {
     77 			filename := fl.info.file
     78 			fns := fileNodes[filename]
     79 			flatSum, cumSum := sumNodes(fns)
     80 
     81 			fnodes, path, err := getFunctionSource(name, filename, fns, 0, 0)
     82 			fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, path)
     83 			fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
     84 				rpt.formatValue(flatSum), rpt.formatValue(cumSum),
     85 				percentage(cumSum, rpt.total))
     86 
     87 			if err != nil {
     88 				fmt.Fprintf(w, " Error: %v\n", err)
     89 				continue
     90 			}
     91 
     92 			for _, fn := range fnodes {
     93 				fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), fn.info.lineno, fn.info.name)
     94 			}
     95 		}
     96 	}
     97 	return nil
     98 }
     99 
    100 // printWebSource prints an annotated source listing, include all
    101 // functions with samples that match the regexp rpt.options.symbol.
    102 func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
    103 	o := rpt.options
    104 	g, err := newGraph(rpt)
    105 	if err != nil {
    106 		return err
    107 	}
    108 
    109 	// If the regexp source can be parsed as an address, also match
    110 	// functions that land on that address.
    111 	var address *uint64
    112 	if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil {
    113 		address = &hex
    114 	}
    115 
    116 	// Extract interesting symbols from binary files in the profile and
    117 	// classify samples per symbol.
    118 	symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj)
    119 	symNodes := nodesPerSymbol(g.ns, symbols)
    120 
    121 	// Sort symbols for printing.
    122 	var syms objSymbols
    123 	for s := range symNodes {
    124 		syms = append(syms, s)
    125 	}
    126 	sort.Sort(syms)
    127 
    128 	if len(syms) == 0 {
    129 		return fmt.Errorf("no samples found on routines matching: %s", o.Symbol.String())
    130 	}
    131 
    132 	printHeader(w, rpt)
    133 	for _, s := range syms {
    134 		name := s.sym.Name[0]
    135 		// Identify sources associated to a symbol by examining
    136 		// symbol samples. Classify samples per source file.
    137 		var sourceFiles nodes
    138 		fileNodes := make(map[string]nodes)
    139 		for _, n := range symNodes[s] {
    140 			if n.info.file == "" {
    141 				continue
    142 			}
    143 			if fileNodes[n.info.file] == nil {
    144 				sourceFiles = append(sourceFiles, n)
    145 			}
    146 			fileNodes[n.info.file] = append(fileNodes[n.info.file], n)
    147 		}
    148 
    149 		if len(sourceFiles) == 0 {
    150 			fmt.Printf("No source information for %s\n", name)
    151 			continue
    152 		}
    153 
    154 		sourceFiles.sort(fileOrder)
    155 
    156 		// Print each file associated with this function.
    157 		for _, fl := range sourceFiles {
    158 			filename := fl.info.file
    159 			fns := fileNodes[filename]
    160 
    161 			asm := assemblyPerSourceLine(symbols, fns, filename, obj)
    162 			start, end := sourceCoordinates(asm)
    163 
    164 			fnodes, path, err := getFunctionSource(name, filename, fns, start, end)
    165 			if err != nil {
    166 				fnodes, path = getMissingFunctionSource(filename, asm, start, end)
    167 			}
    168 
    169 			flatSum, cumSum := sumNodes(fnodes)
    170 			printFunctionHeader(w, name, path, flatSum, cumSum, rpt)
    171 			for _, fn := range fnodes {
    172 				printFunctionSourceLine(w, fn, asm[fn.info.lineno], rpt)
    173 			}
    174 			printFunctionClosing(w)
    175 		}
    176 	}
    177 	printPageClosing(w)
    178 	return nil
    179 }
    180 
    181 // sourceCoordinates returns the lowest and highest line numbers from
    182 // a set of assembly statements.
    183 func sourceCoordinates(asm map[int]nodes) (start, end int) {
    184 	for l := range asm {
    185 		if start == 0 || l < start {
    186 			start = l
    187 		}
    188 		if end == 0 || l > end {
    189 			end = l
    190 		}
    191 	}
    192 	return start, end
    193 }
    194 
    195 // assemblyPerSourceLine disassembles the binary containing a symbol
    196 // and classifies the assembly instructions according to its
    197 // corresponding source line, annotating them with a set of samples.
    198 func assemblyPerSourceLine(objSyms []*objSymbol, rs nodes, src string, obj plugin.ObjTool) map[int]nodes {
    199 	assembly := make(map[int]nodes)
    200 	// Identify symbol to use for this collection of samples.
    201 	o := findMatchingSymbol(objSyms, rs)
    202 	if o == nil {
    203 		return assembly
    204 	}
    205 
    206 	// Extract assembly for matched symbol
    207 	insns, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End)
    208 	if err != nil {
    209 		return assembly
    210 	}
    211 
    212 	srcBase := filepath.Base(src)
    213 	anodes := annotateAssembly(insns, rs, o.base)
    214 	var lineno = 0
    215 	for _, an := range anodes {
    216 		if filepath.Base(an.info.file) == srcBase {
    217 			lineno = an.info.lineno
    218 		}
    219 		if lineno != 0 {
    220 			assembly[lineno] = append(assembly[lineno], an)
    221 		}
    222 	}
    223 
    224 	return assembly
    225 }
    226 
    227 // findMatchingSymbol looks for the symbol that corresponds to a set
    228 // of samples, by comparing their addresses.
    229 func findMatchingSymbol(objSyms []*objSymbol, ns nodes) *objSymbol {
    230 	for _, n := range ns {
    231 		for _, o := range objSyms {
    232 			if o.sym.File == n.info.objfile &&
    233 				o.sym.Start <= n.info.address-o.base &&
    234 				n.info.address-o.base <= o.sym.End {
    235 				return o
    236 			}
    237 		}
    238 	}
    239 	return nil
    240 }
    241 
    242 // printHeader prints the page header for a weblist report.
    243 func printHeader(w io.Writer, rpt *Report) {
    244 	fmt.Fprintln(w, weblistPageHeader)
    245 
    246 	var labels []string
    247 	for _, l := range legendLabels(rpt) {
    248 		labels = append(labels, template.HTMLEscapeString(l))
    249 	}
    250 
    251 	fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`,
    252 		strings.Join(labels, "<br>\n"),
    253 		rpt.formatValue(rpt.total),
    254 	)
    255 }
    256 
    257 // printFunctionHeader prints a function header for a weblist report.
    258 func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) {
    259 	fmt.Fprintf(w, `<h1>%s</h1>%s
    260 <pre onClick="pprof_toggle_asm(event)">
    261   Total:  %10s %10s (flat, cum) %s
    262 `,
    263 		template.HTMLEscapeString(name), template.HTMLEscapeString(path),
    264 		rpt.formatValue(flatSum), rpt.formatValue(cumSum),
    265 		percentage(cumSum, rpt.total))
    266 }
    267 
    268 // printFunctionSourceLine prints a source line and the corresponding assembly.
    269 func printFunctionSourceLine(w io.Writer, fn *node, assembly nodes, rpt *Report) {
    270 	if len(assembly) == 0 {
    271 		fmt.Fprintf(w,
    272 			"<span class=line> %6d</span> <span class=nop>  %10s %10s %s </span>\n",
    273 			fn.info.lineno,
    274 			valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt),
    275 			template.HTMLEscapeString(fn.info.name))
    276 		return
    277 	}
    278 
    279 	fmt.Fprintf(w,
    280 		"<span class=line> %6d</span> <span class=deadsrc>  %10s %10s %s </span>",
    281 		fn.info.lineno,
    282 		valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt),
    283 		template.HTMLEscapeString(fn.info.name))
    284 	fmt.Fprint(w, "<span class=asm>")
    285 	for _, an := range assembly {
    286 		var fileline string
    287 		class := "disasmloc"
    288 		if an.info.file != "" {
    289 			fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.info.file), an.info.lineno)
    290 			if an.info.lineno != fn.info.lineno {
    291 				class = "unimportant"
    292 			}
    293 		}
    294 		fmt.Fprintf(w, " %8s %10s %10s %8x: %-48s <span class=%s>%s</span>\n", "",
    295 			valueOrDot(an.flat, rpt), valueOrDot(an.cum, rpt),
    296 			an.info.address,
    297 			template.HTMLEscapeString(an.info.name),
    298 			class,
    299 			template.HTMLEscapeString(fileline))
    300 	}
    301 	fmt.Fprintln(w, "</span>")
    302 }
    303 
    304 // printFunctionClosing prints the end of a function in a weblist report.
    305 func printFunctionClosing(w io.Writer) {
    306 	fmt.Fprintln(w, "</pre>")
    307 }
    308 
    309 // printPageClosing prints the end of the page in a weblist report.
    310 func printPageClosing(w io.Writer) {
    311 	fmt.Fprintln(w, weblistPageClosing)
    312 }
    313 
    314 // getFunctionSource collects the sources of a function from a source
    315 // file and annotates it with the samples in fns. Returns the sources
    316 // as nodes, using the info.name field to hold the source code.
    317 func getFunctionSource(fun, file string, fns nodes, start, end int) (nodes, string, error) {
    318 	f, file, err := adjustSourcePath(file)
    319 	if err != nil {
    320 		return nil, file, err
    321 	}
    322 
    323 	lineNodes := make(map[int]nodes)
    324 
    325 	// Collect source coordinates from profile.
    326 	const margin = 5 // Lines before first/after last sample.
    327 	if start == 0 {
    328 		if fns[0].info.startLine != 0 {
    329 			start = fns[0].info.startLine
    330 		} else {
    331 			start = fns[0].info.lineno - margin
    332 		}
    333 	} else {
    334 		start -= margin
    335 	}
    336 	if end == 0 {
    337 		end = fns[0].info.lineno
    338 	}
    339 	end += margin
    340 	for _, n := range fns {
    341 		lineno := n.info.lineno
    342 		nodeStart := n.info.startLine
    343 		if nodeStart == 0 {
    344 			nodeStart = lineno - margin
    345 		}
    346 		nodeEnd := lineno + margin
    347 		if nodeStart < start {
    348 			start = nodeStart
    349 		} else if nodeEnd > end {
    350 			end = nodeEnd
    351 		}
    352 		lineNodes[lineno] = append(lineNodes[lineno], n)
    353 	}
    354 
    355 	var src nodes
    356 	buf := bufio.NewReader(f)
    357 	lineno := 1
    358 	for {
    359 		line, err := buf.ReadString('\n')
    360 		if err != nil {
    361 			if err != io.EOF {
    362 				return nil, file, err
    363 			}
    364 			if line == "" {
    365 				// end was at or past EOF; that's okay
    366 				break
    367 			}
    368 		}
    369 		if lineno >= start {
    370 			flat, cum := sumNodes(lineNodes[lineno])
    371 
    372 			src = append(src, &node{
    373 				info: nodeInfo{
    374 					name:   strings.TrimRight(line, "\n"),
    375 					lineno: lineno,
    376 				},
    377 				flat: flat,
    378 				cum:  cum,
    379 			})
    380 		}
    381 		lineno++
    382 		if lineno > end {
    383 			break
    384 		}
    385 	}
    386 	return src, file, nil
    387 }
    388 
    389 // getMissingFunctionSource creates a dummy function body to point to
    390 // the source file and annotates it with the samples in asm.
    391 func getMissingFunctionSource(filename string, asm map[int]nodes, start, end int) (nodes, string) {
    392 	var fnodes nodes
    393 	for i := start; i <= end; i++ {
    394 		lrs := asm[i]
    395 		if len(lrs) == 0 {
    396 			continue
    397 		}
    398 		flat, cum := sumNodes(lrs)
    399 		fnodes = append(fnodes, &node{
    400 			info: nodeInfo{
    401 				name:   "???",
    402 				lineno: i,
    403 			},
    404 			flat: flat,
    405 			cum:  cum,
    406 		})
    407 	}
    408 	return fnodes, filename
    409 }
    410 
    411 // adjustSourcePath adjusts the path for a source file by trimming
    412 // known prefixes and searching for the file on all parents of the
    413 // current working dir.
    414 func adjustSourcePath(path string) (*os.File, string, error) {
    415 	path = trimPath(path)
    416 	f, err := os.Open(path)
    417 	if err == nil {
    418 		return f, path, nil
    419 	}
    420 
    421 	if dir, wderr := os.Getwd(); wderr == nil {
    422 		for {
    423 			parent := filepath.Dir(dir)
    424 			if parent == dir {
    425 				break
    426 			}
    427 			if f, err := os.Open(filepath.Join(parent, path)); err == nil {
    428 				return f, filepath.Join(parent, path), nil
    429 			}
    430 
    431 			dir = parent
    432 		}
    433 	}
    434 
    435 	return nil, path, err
    436 }
    437 
    438 // trimPath cleans up a path by removing prefixes that are commonly
    439 // found on profiles.
    440 func trimPath(path string) string {
    441 	basePaths := []string{
    442 		"/proc/self/cwd/./",
    443 		"/proc/self/cwd/",
    444 	}
    445 
    446 	sPath := filepath.ToSlash(path)
    447 
    448 	for _, base := range basePaths {
    449 		if strings.HasPrefix(sPath, base) {
    450 			return filepath.FromSlash(sPath[len(base):])
    451 		}
    452 	}
    453 	return path
    454 }
    455