// Copyright 2014 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package report // This file contains routines related to the generation of annotated // source listings. import ( "bufio" "fmt" "html/template" "io" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "github.com/google/pprof/internal/graph" "github.com/google/pprof/internal/measurement" "github.com/google/pprof/internal/plugin" "github.com/google/pprof/profile" ) // 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 := rpt.newGraph(nil) // Identify all the functions that match the regexp provided. // Group nodes for each matching function. var functions graph.Nodes functionNodes := make(map[string]graph.Nodes) for _, n := range g.Nodes { 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(graph.NameOrder) if len(functionNodes) == 0 { return fmt.Errorf("no matches found for regexp: %s", o.Symbol) } sourcePath := o.SourcePath if sourcePath == "" { wd, err := os.Getwd() if err != nil { return fmt.Errorf("could not stat current dir: %v", err) } sourcePath = wd } reader := newSourceReader(sourcePath, o.TrimPath) 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 graph.Nodes fileNodes := make(map[string]graph.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.Fprintf(w, "No source information for %s\n", name) continue } sourceFiles.Sort(graph.FileOrder) // Print each file associated with this function. for _, fl := range sourceFiles { filename := fl.Info.File fns := fileNodes[filename] flatSum, cumSum := fns.Sum() fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0) fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename) fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", rpt.formatValue(flatSum), rpt.formatValue(cumSum), measurement.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 { printHeader(w, rpt) if err := PrintWebList(w, rpt, obj, -1); err != nil { return err } printPageClosing(w) return nil } // sourcePrinter holds state needed for generating source+asm HTML listing. type sourcePrinter struct { reader *sourceReader synth *synthCode objectTool plugin.ObjTool objects map[string]plugin.ObjFile // Opened object files sym *regexp.Regexp // May be nil files map[string]*sourceFile // Set of files to print. insts map[uint64]instructionInfo // Instructions of interest (keyed by address). // Set of function names that we are interested in (because they had // a sample and match sym). interest map[string]bool // Mapping from system function names to printable names. prettyNames map[string]string } // addrInfo holds information for an address we are interested in. type addrInfo struct { loc *profile.Location // Always non-nil obj plugin.ObjFile // May be nil } // instructionInfo holds collected information for an instruction. type instructionInfo struct { objAddr uint64 // Address in object file (with base subtracted out) length int // Instruction length in bytes disasm string // Disassembly of instruction file string // For top-level function in which instruction occurs line int // For top-level function in which instruction occurs flat, cum int64 // Samples to report (divisor already applied) } // sourceFile contains collected information for files we will print. type sourceFile struct { fname string cum int64 flat int64 lines map[int][]sourceInst // Instructions to show per line funcName map[int]string // Function name per line } // sourceInst holds information for an instruction to be displayed. type sourceInst struct { addr uint64 stack []callID // Inlined call-stack } // sourceFunction contains information for a contiguous range of lines per function we // will print. type sourceFunction struct { name string begin, end int // Line numbers (end is not included in the range) flat, cum int64 } // addressRange is a range of addresses plus the object file that contains it. type addressRange struct { begin, end uint64 obj plugin.ObjFile mapping *profile.Mapping score int64 // Used to order ranges for processing } // PrintWebList prints annotated source listing of rpt to w. // rpt.prof should contain inlined call info. func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error { sourcePath := rpt.options.SourcePath if sourcePath == "" { wd, err := os.Getwd() if err != nil { return fmt.Errorf("could not stat current dir: %v", err) } sourcePath = wd } sp := newSourcePrinter(rpt, obj, sourcePath) if len(sp.interest) == 0 { return fmt.Errorf("no matches found for regexp: %s", rpt.options.Symbol) } sp.print(w, maxFiles, rpt) sp.close() return nil } func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourcePrinter { sp := &sourcePrinter{ reader: newSourceReader(sourcePath, rpt.options.TrimPath), synth: newSynthCode(rpt.prof.Mapping), objectTool: obj, objects: map[string]plugin.ObjFile{}, sym: rpt.options.Symbol, files: map[string]*sourceFile{}, insts: map[uint64]instructionInfo{}, prettyNames: map[string]string{}, interest: map[string]bool{}, } // If the regexp source can be parsed as an address, also match // functions that land on that address. var address *uint64 if sp.sym != nil { if hex, err := strconv.ParseUint(sp.sym.String(), 0, 64); err == nil { address = &hex } } addrs := map[uint64]addrInfo{} flat := map[uint64]int64{} cum := map[uint64]int64{} // Record an interest in the function corresponding to lines[index]. markInterest := func(addr uint64, loc *profile.Location, index int) { fn := loc.Line[index] if fn.Function == nil { return } sp.interest[fn.Function.Name] = true sp.interest[fn.Function.SystemName] = true if _, ok := addrs[addr]; !ok { addrs[addr] = addrInfo{loc, sp.objectFile(loc.Mapping)} } } // See if sp.sym matches line. matches := func(line profile.Line) bool { if line.Function == nil { return false } return sp.sym.MatchString(line.Function.Name) || sp.sym.MatchString(line.Function.SystemName) || sp.sym.MatchString(line.Function.Filename) } // Extract sample counts and compute set of interesting functions. for _, sample := range rpt.prof.Sample { value := rpt.options.SampleValue(sample.Value) if rpt.options.SampleMeanDivisor != nil { div := rpt.options.SampleMeanDivisor(sample.Value) if div != 0 { value /= div } } // Find call-sites matching sym. for i := len(sample.Location) - 1; i >= 0; i-- { loc := sample.Location[i] for _, line := range loc.Line { if line.Function == nil { continue } sp.prettyNames[line.Function.SystemName] = line.Function.Name } addr := loc.Address if addr == 0 { // Some profiles are missing valid addresses. addr = sp.synth.address(loc) } cum[addr] += value if i == 0 { flat[addr] += value } if sp.sym == nil || (address != nil && addr == *address) { // Interested in top-level entry of stack. if len(loc.Line) > 0 { markInterest(addr, loc, len(loc.Line)-1) } continue } // Search in inlined stack for a match. matchFile := (loc.Mapping != nil && sp.sym.MatchString(loc.Mapping.File)) for j, line := range loc.Line { if (j == 0 && matchFile) || matches(line) { markInterest(addr, loc, j) } } } } sp.expandAddresses(rpt, addrs, flat) sp.initSamples(flat, cum) return sp } func (sp *sourcePrinter) close() { for _, objFile := range sp.objects { if objFile != nil { objFile.Close() } } } func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]addrInfo, flat map[uint64]int64) { // We found interesting addresses (ones with non-zero samples) above. // Get covering address ranges and disassemble the ranges. ranges, unprocessed := sp.splitIntoRanges(rpt.prof, addrs, flat) sp.handleUnprocessed(addrs, unprocessed) // Trim ranges if there are too many. const maxRanges = 25 sort.Slice(ranges, func(i, j int) bool { return ranges[i].score > ranges[j].score }) if len(ranges) > maxRanges { ranges = ranges[:maxRanges] } for _, r := range ranges { objBegin, err := r.obj.ObjAddr(r.begin) if err != nil { fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range start %x: %v\n", r.begin, err) continue } objEnd, err := r.obj.ObjAddr(r.end) if err != nil { fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range end %x: %v\n", r.end, err) continue } base := r.begin - objBegin insts, err := sp.objectTool.Disasm(r.mapping.File, objBegin, objEnd, rpt.options.IntelSyntax) if err != nil { // TODO(sanjay): Report that the covered addresses are missing. continue } var lastFrames []plugin.Frame var lastAddr, maxAddr uint64 for i, inst := range insts { addr := inst.Addr + base // Guard against duplicate output from Disasm. if addr <= maxAddr { continue } maxAddr = addr length := 1 if i+1 < len(insts) && insts[i+1].Addr > inst.Addr { // Extend to next instruction. length = int(insts[i+1].Addr - inst.Addr) } // Get inlined-call-stack for address. frames, err := r.obj.SourceLine(addr) if err != nil { // Construct a frame from disassembler output. frames = []plugin.Frame{{Func: inst.Function, File: inst.File, Line: inst.Line}} } x := instructionInfo{objAddr: inst.Addr, length: length, disasm: inst.Text} if len(frames) > 0 { // We could consider using the outer-most caller's source // location so we give the some hint as to where the // inlining happened that led to this instruction. So for // example, suppose we have the following (inlined) call // chains for this instruction: // F1->G->H // F2->G->H // We could tag the instructions from the first call with // F1 and instructions from the second call with F2. But // that leads to a somewhat confusing display. So for now, // we stick with just the inner-most location (i.e., H). // In the future we will consider changing the display to // make caller info more visible. index := 0 // Inner-most frame x.file = frames[index].File x.line = frames[index].Line } sp.insts[addr] = x // We sometimes get instructions with a zero reported line number. // Make such instructions have the same line info as the preceding // instruction, if an earlier instruction is found close enough. const neighborhood = 32 if len(frames) > 0 && frames[0].Line != 0 { lastFrames = frames lastAddr = addr } else if (addr-lastAddr <= neighborhood) && lastFrames != nil { frames = lastFrames } sp.addStack(addr, frames) } } } func (sp *sourcePrinter) addStack(addr uint64, frames []plugin.Frame) { // See if the stack contains a function we are interested in. for i, f := range frames { if !sp.interest[f.Func] { continue } // Record sub-stack under frame's file/line. fname := canonicalizeFileName(f.File) file := sp.files[fname] if file == nil { file = &sourceFile{ fname: fname, lines: map[int][]sourceInst{}, funcName: map[int]string{}, } sp.files[fname] = file } callees := frames[:i] stack := make([]callID, 0, len(callees)) for j := len(callees) - 1; j >= 0; j-- { // Reverse so caller is first stack = append(stack, callID{ file: callees[j].File, line: callees[j].Line, }) } file.lines[f.Line] = append(file.lines[f.Line], sourceInst{addr, stack}) // Remember the first function name encountered per source line // and assume that that line belongs to that function. if _, ok := file.funcName[f.Line]; !ok { file.funcName[f.Line] = f.Func } } } // synthAsm is the special disassembler value used for instructions without an object file. const synthAsm = "" // handleUnprocessed handles addresses that were skipped by splitIntoRanges because they // did not belong to a known object file. func (sp *sourcePrinter) handleUnprocessed(addrs map[uint64]addrInfo, unprocessed []uint64) { // makeFrames synthesizes a []plugin.Frame list for the specified address. // The result will typically have length 1, but may be longer if address corresponds // to inlined calls. makeFrames := func(addr uint64) []plugin.Frame { loc := addrs[addr].loc stack := make([]plugin.Frame, 0, len(loc.Line)) for _, line := range loc.Line { fn := line.Function if fn == nil { continue } stack = append(stack, plugin.Frame{ Func: fn.Name, File: fn.Filename, Line: int(line.Line), }) } return stack } for _, addr := range unprocessed { frames := makeFrames(addr) x := instructionInfo{ objAddr: addr, length: 1, disasm: synthAsm, } if len(frames) > 0 { x.file = frames[0].File x.line = frames[0].Line } sp.insts[addr] = x sp.addStack(addr, frames) } } // splitIntoRanges converts the set of addresses we are interested in into a set of address // ranges to disassemble. It also returns the set of addresses found that did not have an // associated object file and were therefore not added to an address range. func (sp *sourcePrinter) splitIntoRanges(prof *profile.Profile, addrMap map[uint64]addrInfo, flat map[uint64]int64) ([]addressRange, []uint64) { // Partition addresses into two sets: ones with a known object file, and ones without. var addrs, unprocessed []uint64 for addr, info := range addrMap { if info.obj != nil { addrs = append(addrs, addr) } else { unprocessed = append(unprocessed, addr) } } sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] }) const expand = 500 // How much to expand range to pick up nearby addresses. var result []addressRange for i, n := 0, len(addrs); i < n; { begin, end := addrs[i], addrs[i] sum := flat[begin] i++ info := addrMap[begin] m := info.loc.Mapping obj := info.obj // Non-nil because of the partitioning done above. // Find following addresses that are close enough to addrs[i]. for i < n && addrs[i] <= end+2*expand && addrs[i] < m.Limit { // When we expand ranges by "expand" on either side, the ranges // for addrs[i] and addrs[i-1] will merge. end = addrs[i] sum += flat[end] i++ } if m.Start-begin >= expand { begin -= expand } else { begin = m.Start } if m.Limit-end >= expand { end += expand } else { end = m.Limit } result = append(result, addressRange{begin, end, obj, m, sum}) } return result, unprocessed } func (sp *sourcePrinter) initSamples(flat, cum map[uint64]int64) { for addr, inst := range sp.insts { // Move all samples that were assigned to the middle of an instruction to the // beginning of that instruction. This takes care of samples that were recorded // against pc+1. instEnd := addr + uint64(inst.length) for p := addr; p < instEnd; p++ { inst.flat += flat[p] inst.cum += cum[p] } sp.insts[addr] = inst } } func (sp *sourcePrinter) print(w io.Writer, maxFiles int, rpt *Report) { // Finalize per-file counts. for _, file := range sp.files { seen := map[uint64]bool{} for _, line := range file.lines { for _, x := range line { if seen[x.addr] { // Same address can be displayed multiple times in a file // (e.g., if we show multiple inlined functions). // Avoid double-counting samples in this case. continue } seen[x.addr] = true inst := sp.insts[x.addr] file.cum += inst.cum file.flat += inst.flat } } } // Get sorted list of files to print. var files []*sourceFile for _, f := range sp.files { files = append(files, f) } order := func(i, j int) bool { return files[i].flat > files[j].flat } if maxFiles < 0 { // Order by name for compatibility with old code. order = func(i, j int) bool { return files[i].fname < files[j].fname } maxFiles = len(files) } sort.Slice(files, order) for i, f := range files { if i < maxFiles { sp.printFile(w, f, rpt) } } } func (sp *sourcePrinter) printFile(w io.Writer, f *sourceFile, rpt *Report) { for _, fn := range sp.functions(f) { if fn.cum == 0 { continue } printFunctionHeader(w, fn.name, f.fname, fn.flat, fn.cum, rpt) var asm []assemblyInstruction for l := fn.begin; l < fn.end; l++ { lineContents, ok := sp.reader.line(f.fname, l) if !ok { if len(f.lines[l]) == 0 { // Outside of range of valid lines and nothing to print. continue } if l == 0 { // Line number 0 shows up if line number is not known. lineContents = "" } else { // Past end of file, but have data to print. lineContents = "???" } } // Make list of assembly instructions. asm = asm[:0] var flatSum, cumSum int64 var lastAddr uint64 for _, inst := range f.lines[l] { addr := inst.addr x := sp.insts[addr] flatSum += x.flat cumSum += x.cum startsBlock := (addr != lastAddr+uint64(sp.insts[lastAddr].length)) lastAddr = addr // divisors already applied, so leave flatDiv,cumDiv as 0 asm = append(asm, assemblyInstruction{ address: x.objAddr, instruction: x.disasm, function: fn.name, file: x.file, line: x.line, flat: x.flat, cum: x.cum, startsBlock: startsBlock, inlineCalls: inst.stack, }) } printFunctionSourceLine(w, l, flatSum, cumSum, lineContents, asm, sp.reader, rpt) } printFunctionClosing(w) } } // functions splits apart the lines to show in a file into a list of per-function ranges. func (sp *sourcePrinter) functions(f *sourceFile) []sourceFunction { var funcs []sourceFunction // Get interesting lines in sorted order. lines := make([]int, 0, len(f.lines)) for l := range f.lines { lines = append(lines, l) } sort.Ints(lines) // Merge adjacent lines that are in same function and not too far apart. const mergeLimit = 20 for _, l := range lines { name := f.funcName[l] if pretty, ok := sp.prettyNames[name]; ok { // Use demangled name if available. name = pretty } fn := sourceFunction{name: name, begin: l, end: l + 1} for _, x := range f.lines[l] { inst := sp.insts[x.addr] fn.flat += inst.flat fn.cum += inst.cum } // See if we should merge into preceding function. if len(funcs) > 0 { last := funcs[len(funcs)-1] if l-last.end < mergeLimit && last.name == name { last.end = l + 1 last.flat += fn.flat last.cum += fn.cum funcs[len(funcs)-1] = last continue } } // Add new function. funcs = append(funcs, fn) } // Expand function boundaries to show neighborhood. const expand = 5 for i, f := range funcs { if i == 0 { // Extend backwards, stopping at line number 1, but do not disturb 0 // since that is a special line number that can show up when addr2line // cannot determine the real line number. if f.begin > expand { f.begin -= expand } else if f.begin > 1 { f.begin = 1 } } else { // Find gap from predecessor and divide between predecessor and f. halfGap := (f.begin - funcs[i-1].end) / 2 if halfGap > expand { halfGap = expand } funcs[i-1].end += halfGap f.begin -= halfGap } funcs[i] = f } // Also extend the ending point of the last function. if len(funcs) > 0 { funcs[len(funcs)-1].end += expand } return funcs } // objectFile return the object for the specified mapping, opening it if necessary. // It returns nil on error. func (sp *sourcePrinter) objectFile(m *profile.Mapping) plugin.ObjFile { if m == nil { return nil } if object, ok := sp.objects[m.File]; ok { return object // May be nil if we detected an error earlier. } object, err := sp.objectTool.Open(m.File, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol) if err != nil { object = nil } sp.objects[m.File] = object // Cache even on error. return object } // printHeader prints the page header for a weblist report. func printHeader(w io.Writer, rpt *Report) { fmt.Fprintln(w, ` Pprof listing`) fmt.Fprintln(w, weblistPageCSS) fmt.Fprintln(w, weblistPageScript) fmt.Fprint(w, "\n\n\n") var labels []string for _, l := range ProfileLabels(rpt) { labels = append(labels, template.HTMLEscapeString(l)) } fmt.Fprintf(w, `
%s
Total: %s
`, strings.Join(labels, "
\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, `

%s

%s

  Total:  %10s %10s (flat, cum) %s
`,
		template.HTMLEscapeString(name), template.HTMLEscapeString(path),
		rpt.formatValue(flatSum), rpt.formatValue(cumSum),
		measurement.Percentage(cumSum, rpt.total))
}

// printFunctionSourceLine prints a source line and the corresponding assembly.
func printFunctionSourceLine(w io.Writer, lineNo int, flat, cum int64, lineContents string,
	assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
	if len(assembly) == 0 {
		fmt.Fprintf(w,
			" %6d   %10s %10s %8s  %s \n",
			lineNo,
			valueOrDot(flat, rpt), valueOrDot(cum, rpt),
			"", template.HTMLEscapeString(lineContents))
		return
	}

	nestedInfo := false
	cl := "deadsrc"
	for _, an := range assembly {
		if len(an.inlineCalls) > 0 || an.instruction != synthAsm {
			nestedInfo = true
			cl = "livesrc"
		}
	}

	fmt.Fprintf(w,
		" %6d   %10s %10s %8s  %s ",
		lineNo, cl,
		valueOrDot(flat, rpt), valueOrDot(cum, rpt),
		"", template.HTMLEscapeString(lineContents))
	if nestedInfo {
		srcIndent := indentation(lineContents)
		printNested(w, srcIndent, assembly, reader, rpt)
	}
	fmt.Fprintln(w)
}

func printNested(w io.Writer, srcIndent int, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
	fmt.Fprint(w, "")
	var curCalls []callID
	for i, an := range assembly {
		if an.startsBlock && i != 0 {
			// Insert a separator between discontiguous blocks.
			fmt.Fprintf(w, " %8s %28s\n", "", "⋮")
		}

		var fileline string
		if an.file != "" {
			fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(filepath.Base(an.file)), an.line)
		}
		flat, cum := an.flat, an.cum

		// Print inlined call context.
		for j, c := range an.inlineCalls {
			if j < len(curCalls) && curCalls[j] == c {
				// Skip if same as previous instruction.
				continue
			}
			curCalls = nil
			fline, ok := reader.line(c.file, c.line)
			if !ok {
				fline = ""
			}
			text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline)
			fmt.Fprintf(w, " %8s %10s %10s %8s  %s %s:%d\n",
				"", "", "", "",
				template.HTMLEscapeString(rightPad(text, 80)),
				template.HTMLEscapeString(filepath.Base(c.file)), c.line)
		}
		curCalls = an.inlineCalls
		if an.instruction == synthAsm {
			continue
		}
		text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
		fmt.Fprintf(w, " %8s %10s %10s %8x: %s %s\n",
			"", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address,
			template.HTMLEscapeString(rightPad(text, 80)),
			// fileline should not be escaped since it was formed by appending
			// line number (just digits) to an escaped file name. Escaping here
			// would cause double-escaping of file name.
			fileline)
	}
	fmt.Fprint(w, "")
}

// printFunctionClosing prints the end of a function in a weblist report.
func printFunctionClosing(w io.Writer) {
	fmt.Fprintln(w, "
") } // printPageClosing prints the end of the page in a weblist report. func printPageClosing(w io.Writer) { fmt.Fprintln(w, weblistPageClosing) } // getSourceFromFile 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 getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) { lineNodes := make(map[int]graph.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) } if start < 1 { start = 1 } var src graph.Nodes for lineno := start; lineno <= end; lineno++ { line, ok := reader.line(file, lineno) if !ok { break } flat, cum := lineNodes[lineno].Sum() src = append(src, &graph.Node{ Info: graph.NodeInfo{ Name: strings.TrimRight(line, "\n"), Lineno: lineno, }, Flat: flat, Cum: cum, }) } if err := reader.fileError(file); err != nil { return nil, file, err } return src, file, nil } // sourceReader provides access to source code with caching of file contents. type sourceReader struct { // searchPath is a filepath.ListSeparator-separated list of directories where // source files should be searched. searchPath string // trimPath is a filepath.ListSeparator-separated list of paths to trim. trimPath string // files maps from path name to a list of lines. // files[*][0] is unused since line numbering starts at 1. files map[string][]string // errors collects errors encountered per file. These errors are // consulted before returning out of these module. errors map[string]error } func newSourceReader(searchPath, trimPath string) *sourceReader { return &sourceReader{ searchPath, trimPath, make(map[string][]string), make(map[string]error), } } func (reader *sourceReader) fileError(path string) error { return reader.errors[path] } // line returns the line numbered "lineno" in path, or _,false if lineno is out of range. func (reader *sourceReader) line(path string, lineno int) (string, bool) { lines, ok := reader.files[path] if !ok { // Read and cache file contents. lines = []string{""} // Skip 0th line f, err := openSourceFile(path, reader.searchPath, reader.trimPath) if err != nil { reader.errors[path] = err } else { s := bufio.NewScanner(f) for s.Scan() { lines = append(lines, s.Text()) } f.Close() if s.Err() != nil { reader.errors[path] = err } } reader.files[path] = lines } if lineno <= 0 || lineno >= len(lines) { return "", false } return lines[lineno], true } // openSourceFile opens a source file from a name encoded in a profile. File // names in a profile after can be relative paths, so search them in each of // the paths in searchPath and their parents. In case the profile contains // absolute paths, additional paths may be configured to trim from the source // paths in the profile. This effectively turns the path into a relative path // searching it using searchPath as usual). func openSourceFile(path, searchPath, trim string) (*os.File, error) { path = trimPath(path, trim, searchPath) // If file is still absolute, require file to exist. if filepath.IsAbs(path) { f, err := os.Open(path) return f, err } // Scan each component of the path. for _, dir := range filepath.SplitList(searchPath) { // Search up for every parent of each possible path. for { filename := filepath.Join(dir, path) if f, err := os.Open(filename); err == nil { return f, nil } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } } return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath) } // trimPath cleans up a path by removing prefixes that are commonly // found on profiles plus configured prefixes. // TODO(aalexand): Consider optimizing out the redundant work done in this // function if it proves to matter. func trimPath(path, trimPath, searchPath string) string { // Keep path variable intact as it's used below to form the return value. sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath) if trimPath == "" { // If the trim path is not configured, try to guess it heuristically: // search for basename of each search path in the original path and, if // found, strip everything up to and including the basename. So, for // example, given original path "/some/remote/path/my-project/foo/bar.c" // and search path "/my/local/path/my-project" the heuristic will return // "/my/local/path/my-project/foo/bar.c". for _, dir := range filepath.SplitList(searchPath) { want := "/" + filepath.Base(dir) + "/" if found := strings.Index(sPath, want); found != -1 { return path[found+len(want):] } } } // Trim configured trim prefixes. trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/") for _, trimPath := range trimPaths { if !strings.HasSuffix(trimPath, "/") { trimPath += "/" } if strings.HasPrefix(sPath, trimPath) { return path[len(trimPath):] } } return path } func indentation(line string) int { column := 0 for _, c := range line { if c == ' ' { column++ } else if c == '\t' { column++ for column%8 != 0 { column++ } } else { break } } return column } // rightPad pads the input with spaces on the right-hand-side to make it have // at least width n. It treats tabs as enough spaces that lead to the next // 8-aligned tab-stop. func rightPad(s string, n int) string { var str strings.Builder // Convert tabs to spaces as we go so padding works regardless of what prefix // is placed before the result. column := 0 for _, c := range s { column++ if c == '\t' { str.WriteRune(' ') for column%8 != 0 { column++ str.WriteRune(' ') } } else { str.WriteRune(c) } } for column < n { column++ str.WriteRune(' ') } return str.String() } func canonicalizeFileName(fname string) string { fname = strings.TrimPrefix(fname, "/proc/self/cwd/") fname = strings.TrimPrefix(fname, "./") return filepath.Clean(fname) }