| // 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 = "<instructions with unknown line numbers>" |
| } 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, ` |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Pprof listing</title>`) |
| fmt.Fprintln(w, weblistPageCSS) |
| fmt.Fprintln(w, weblistPageScript) |
| fmt.Fprint(w, "</head>\n<body>\n\n") |
| |
| var labels []string |
| for _, l := range ProfileLabels(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, `<h2>%s</h2><p class="filename">%s</p> |
| <pre onClick="pprof_toggle_asm(event)"> |
| 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, |
| "<span class=line> %6d</span> <span class=nop> %10s %10s %8s %s </span>\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, |
| "<span class=line> %6d</span> <span class=%s> %10s %10s %8s %s </span>", |
| 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, "<span class=asm>") |
| 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 <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\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 <span class=unimportant>%s</span>\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, "</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) |
| } |
| |
| // 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) |
| } |