github.com/akaros/go-akaros@v0.0.0-20181004170632-85005d477eab/src/cmd/pprof/internal/report/source.go (about)

     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 filepath.Base(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()">
   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 line == "" || err != io.EOF {
   362  				return nil, file, err
   363  			}
   364  		}
   365  		if lineno >= start {
   366  			flat, cum := sumNodes(lineNodes[lineno])
   367  
   368  			src = append(src, &node{
   369  				info: nodeInfo{
   370  					name:   strings.TrimRight(line, "\n"),
   371  					lineno: lineno,
   372  				},
   373  				flat: flat,
   374  				cum:  cum,
   375  			})
   376  		}
   377  		lineno++
   378  		if lineno > end {
   379  			break
   380  		}
   381  	}
   382  	return src, file, nil
   383  }
   384  
   385  // getMissingFunctionSource creates a dummy function body to point to
   386  // the source file and annotates it with the samples in asm.
   387  func getMissingFunctionSource(filename string, asm map[int]nodes, start, end int) (nodes, string) {
   388  	var fnodes nodes
   389  	for i := start; i <= end; i++ {
   390  		lrs := asm[i]
   391  		if len(lrs) == 0 {
   392  			continue
   393  		}
   394  		flat, cum := sumNodes(lrs)
   395  		fnodes = append(fnodes, &node{
   396  			info: nodeInfo{
   397  				name:   "???",
   398  				lineno: i,
   399  			},
   400  			flat: flat,
   401  			cum:  cum,
   402  		})
   403  	}
   404  	return fnodes, filename
   405  }
   406  
   407  // adjustSourcePath adjusts the pathe for a source file by trimmming
   408  // known prefixes and searching for the file on all parents of the
   409  // current working dir.
   410  func adjustSourcePath(path string) (*os.File, string, error) {
   411  	path = trimPath(path)
   412  	f, err := os.Open(path)
   413  	if err == nil {
   414  		return f, path, nil
   415  	}
   416  
   417  	if dir, wderr := os.Getwd(); wderr == nil {
   418  		for {
   419  			parent := filepath.Dir(dir)
   420  			if parent == dir {
   421  				break
   422  			}
   423  			if f, err := os.Open(filepath.Join(parent, path)); err == nil {
   424  				return f, filepath.Join(parent, path), nil
   425  			}
   426  
   427  			dir = parent
   428  		}
   429  	}
   430  
   431  	return nil, path, err
   432  }
   433  
   434  // trimPath cleans up a path by removing prefixes that are commonly
   435  // found on profiles.
   436  func trimPath(path string) string {
   437  	basePaths := []string{
   438  		"/proc/self/cwd/./",
   439  		"/proc/self/cwd/",
   440  	}
   441  
   442  	sPath := filepath.ToSlash(path)
   443  
   444  	for _, base := range basePaths {
   445  		if strings.HasPrefix(sPath, base) {
   446  			return filepath.FromSlash(sPath[len(base):])
   447  		}
   448  	}
   449  	return path
   450  }