// 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 }