1// Copyright 2014 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package report
16
17// This file contains routines related to the generation of annotated
18// source listings.
19
20import (
21	"bufio"
22	"fmt"
23	"html/template"
24	"io"
25	"os"
26	"path/filepath"
27	"regexp"
28	"sort"
29	"strconv"
30	"strings"
31
32	"github.com/google/pprof/internal/graph"
33	"github.com/google/pprof/internal/measurement"
34	"github.com/google/pprof/internal/plugin"
35	"github.com/google/pprof/profile"
36)
37
38// printSource prints an annotated source listing, include all
39// functions with samples that match the regexp rpt.options.symbol.
40// The sources are sorted by function name and then by filename to
41// eliminate potential nondeterminism.
42func printSource(w io.Writer, rpt *Report) error {
43	o := rpt.options
44	g := rpt.newGraph(nil)
45
46	// Identify all the functions that match the regexp provided.
47	// Group nodes for each matching function.
48	var functions graph.Nodes
49	functionNodes := make(map[string]graph.Nodes)
50	for _, n := range g.Nodes {
51		if !o.Symbol.MatchString(n.Info.Name) {
52			continue
53		}
54		if functionNodes[n.Info.Name] == nil {
55			functions = append(functions, n)
56		}
57		functionNodes[n.Info.Name] = append(functionNodes[n.Info.Name], n)
58	}
59	functions.Sort(graph.NameOrder)
60
61	if len(functionNodes) == 0 {
62		return fmt.Errorf("no matches found for regexp: %s", o.Symbol)
63	}
64
65	sourcePath := o.SourcePath
66	if sourcePath == "" {
67		wd, err := os.Getwd()
68		if err != nil {
69			return fmt.Errorf("could not stat current dir: %v", err)
70		}
71		sourcePath = wd
72	}
73	reader := newSourceReader(sourcePath, o.TrimPath)
74
75	fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
76	for _, fn := range functions {
77		name := fn.Info.Name
78
79		// Identify all the source files associated to this function.
80		// Group nodes for each source file.
81		var sourceFiles graph.Nodes
82		fileNodes := make(map[string]graph.Nodes)
83		for _, n := range functionNodes[name] {
84			if n.Info.File == "" {
85				continue
86			}
87			if fileNodes[n.Info.File] == nil {
88				sourceFiles = append(sourceFiles, n)
89			}
90			fileNodes[n.Info.File] = append(fileNodes[n.Info.File], n)
91		}
92
93		if len(sourceFiles) == 0 {
94			fmt.Fprintf(w, "No source information for %s\n", name)
95			continue
96		}
97
98		sourceFiles.Sort(graph.FileOrder)
99
100		// Print each file associated with this function.
101		for _, fl := range sourceFiles {
102			filename := fl.Info.File
103			fns := fileNodes[filename]
104			flatSum, cumSum := fns.Sum()
105
106			fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0)
107			fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename)
108			fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
109				rpt.formatValue(flatSum), rpt.formatValue(cumSum),
110				measurement.Percentage(cumSum, rpt.total))
111
112			if err != nil {
113				fmt.Fprintf(w, " Error: %v\n", err)
114				continue
115			}
116
117			for _, fn := range fnodes {
118				fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), fn.Info.Lineno, fn.Info.Name)
119			}
120		}
121	}
122	return nil
123}
124
125// sourcePrinter holds state needed for generating source+asm HTML listing.
126type sourcePrinter struct {
127	reader     *sourceReader
128	synth      *synthCode
129	objectTool plugin.ObjTool
130	objects    map[string]plugin.ObjFile  // Opened object files
131	sym        *regexp.Regexp             // May be nil
132	files      map[string]*sourceFile     // Set of files to print.
133	insts      map[uint64]instructionInfo // Instructions of interest (keyed by address).
134
135	// Set of function names that we are interested in (because they had
136	// a sample and match sym).
137	interest map[string]bool
138
139	// Mapping from system function names to printable names.
140	prettyNames map[string]string
141}
142
143// addrInfo holds information for an address we are interested in.
144type addrInfo struct {
145	loc *profile.Location // Always non-nil
146	obj plugin.ObjFile    // May be nil
147}
148
149// instructionInfo holds collected information for an instruction.
150type instructionInfo struct {
151	objAddr   uint64 // Address in object file (with base subtracted out)
152	length    int    // Instruction length in bytes
153	disasm    string // Disassembly of instruction
154	file      string // For top-level function in which instruction occurs
155	line      int    // For top-level function in which instruction occurs
156	flat, cum int64  // Samples to report (divisor already applied)
157}
158
159// sourceFile contains collected information for files we will print.
160type sourceFile struct {
161	fname    string
162	cum      int64
163	flat     int64
164	lines    map[int][]sourceInst // Instructions to show per line
165	funcName map[int]string       // Function name per line
166}
167
168// sourceInst holds information for an instruction to be displayed.
169type sourceInst struct {
170	addr  uint64
171	stack []callID // Inlined call-stack
172}
173
174// sourceFunction contains information for a contiguous range of lines per function we
175// will print.
176type sourceFunction struct {
177	name       string
178	begin, end int // Line numbers (end is not included in the range)
179	flat, cum  int64
180}
181
182// addressRange is a range of addresses plus the object file that contains it.
183type addressRange struct {
184	begin, end uint64
185	obj        plugin.ObjFile
186	mapping    *profile.Mapping
187	score      int64 // Used to order ranges for processing
188}
189
190// WebListData holds the data needed to generate HTML source code listing.
191type WebListData struct {
192	Total string
193	Files []WebListFile
194}
195
196// WebListFile holds the per-file information for HTML source code listing.
197type WebListFile struct {
198	Funcs []WebListFunc
199}
200
201// WebListFunc holds the per-function information for HTML source code listing.
202type WebListFunc struct {
203	Name       string
204	File       string
205	Flat       string
206	Cumulative string
207	Percent    string
208	Lines      []WebListLine
209}
210
211// WebListLine holds the per-source-line information for HTML source code listing.
212type WebListLine struct {
213	SrcLine      string
214	HTMLClass    string
215	Line         int
216	Flat         string
217	Cumulative   string
218	Instructions []WebListInstruction
219}
220
221// WebListInstruction holds the per-instruction information for HTML source code listing.
222type WebListInstruction struct {
223	NewBlock     bool // Insert marker that indicates separation from previous block
224	Flat         string
225	Cumulative   string
226	Synthetic    bool
227	Address      uint64
228	Disasm       string
229	FileLine     string
230	InlinedCalls []WebListCall
231}
232
233// WebListCall holds the per-inlined-call information for HTML source code listing.
234type WebListCall struct {
235	SrcLine  string
236	FileBase string
237	Line     int
238}
239
240// MakeWebList returns an annotated source listing of rpt.
241// rpt.prof should contain inlined call info.
242func MakeWebList(rpt *Report, obj plugin.ObjTool, maxFiles int) (WebListData, error) {
243	sourcePath := rpt.options.SourcePath
244	if sourcePath == "" {
245		wd, err := os.Getwd()
246		if err != nil {
247			return WebListData{}, fmt.Errorf("could not stat current dir: %v", err)
248		}
249		sourcePath = wd
250	}
251	sp := newSourcePrinter(rpt, obj, sourcePath)
252	if len(sp.interest) == 0 {
253		return WebListData{}, fmt.Errorf("no matches found for regexp: %s", rpt.options.Symbol)
254	}
255	defer sp.close()
256	return sp.generate(maxFiles, rpt), nil
257}
258
259func newSourcePrinter(rpt *Report, obj plugin.ObjTool, sourcePath string) *sourcePrinter {
260	sp := &sourcePrinter{
261		reader:      newSourceReader(sourcePath, rpt.options.TrimPath),
262		synth:       newSynthCode(rpt.prof.Mapping),
263		objectTool:  obj,
264		objects:     map[string]plugin.ObjFile{},
265		sym:         rpt.options.Symbol,
266		files:       map[string]*sourceFile{},
267		insts:       map[uint64]instructionInfo{},
268		prettyNames: map[string]string{},
269		interest:    map[string]bool{},
270	}
271
272	// If the regexp source can be parsed as an address, also match
273	// functions that land on that address.
274	var address *uint64
275	if sp.sym != nil {
276		if hex, err := strconv.ParseUint(sp.sym.String(), 0, 64); err == nil {
277			address = &hex
278		}
279	}
280
281	addrs := map[uint64]addrInfo{}
282	flat := map[uint64]int64{}
283	cum := map[uint64]int64{}
284
285	// Record an interest in the function corresponding to lines[index].
286	markInterest := func(addr uint64, loc *profile.Location, index int) {
287		fn := loc.Line[index]
288		if fn.Function == nil {
289			return
290		}
291		sp.interest[fn.Function.Name] = true
292		sp.interest[fn.Function.SystemName] = true
293		if _, ok := addrs[addr]; !ok {
294			addrs[addr] = addrInfo{loc, sp.objectFile(loc.Mapping)}
295		}
296	}
297
298	// See if sp.sym matches line.
299	matches := func(line profile.Line) bool {
300		if line.Function == nil {
301			return false
302		}
303		return sp.sym.MatchString(line.Function.Name) ||
304			sp.sym.MatchString(line.Function.SystemName) ||
305			sp.sym.MatchString(line.Function.Filename)
306	}
307
308	// Extract sample counts and compute set of interesting functions.
309	for _, sample := range rpt.prof.Sample {
310		value := rpt.options.SampleValue(sample.Value)
311		if rpt.options.SampleMeanDivisor != nil {
312			div := rpt.options.SampleMeanDivisor(sample.Value)
313			if div != 0 {
314				value /= div
315			}
316		}
317
318		// Find call-sites matching sym.
319		for i := len(sample.Location) - 1; i >= 0; i-- {
320			loc := sample.Location[i]
321			for _, line := range loc.Line {
322				if line.Function == nil {
323					continue
324				}
325				sp.prettyNames[line.Function.SystemName] = line.Function.Name
326			}
327
328			addr := loc.Address
329			if addr == 0 {
330				// Some profiles are missing valid addresses.
331				addr = sp.synth.address(loc)
332			}
333
334			cum[addr] += value
335			if i == 0 {
336				flat[addr] += value
337			}
338
339			if sp.sym == nil || (address != nil && addr == *address) {
340				// Interested in top-level entry of stack.
341				if len(loc.Line) > 0 {
342					markInterest(addr, loc, len(loc.Line)-1)
343				}
344				continue
345			}
346
347			// Search in inlined stack for a match.
348			matchFile := (loc.Mapping != nil && sp.sym.MatchString(loc.Mapping.File))
349			for j, line := range loc.Line {
350				if (j == 0 && matchFile) || matches(line) {
351					markInterest(addr, loc, j)
352				}
353			}
354		}
355	}
356
357	sp.expandAddresses(rpt, addrs, flat)
358	sp.initSamples(flat, cum)
359	return sp
360}
361
362func (sp *sourcePrinter) close() {
363	for _, objFile := range sp.objects {
364		if objFile != nil {
365			objFile.Close()
366		}
367	}
368}
369
370func (sp *sourcePrinter) expandAddresses(rpt *Report, addrs map[uint64]addrInfo, flat map[uint64]int64) {
371	// We found interesting addresses (ones with non-zero samples) above.
372	// Get covering address ranges and disassemble the ranges.
373	ranges, unprocessed := sp.splitIntoRanges(rpt.prof, addrs, flat)
374	sp.handleUnprocessed(addrs, unprocessed)
375
376	// Trim ranges if there are too many.
377	const maxRanges = 25
378	sort.Slice(ranges, func(i, j int) bool {
379		return ranges[i].score > ranges[j].score
380	})
381	if len(ranges) > maxRanges {
382		ranges = ranges[:maxRanges]
383	}
384
385	for _, r := range ranges {
386		objBegin, err := r.obj.ObjAddr(r.begin)
387		if err != nil {
388			fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range start %x: %v\n", r.begin, err)
389			continue
390		}
391		objEnd, err := r.obj.ObjAddr(r.end)
392		if err != nil {
393			fmt.Fprintf(os.Stderr, "Failed to compute objdump address for range end %x: %v\n", r.end, err)
394			continue
395		}
396		base := r.begin - objBegin
397		insts, err := sp.objectTool.Disasm(r.mapping.File, objBegin, objEnd, rpt.options.IntelSyntax)
398		if err != nil {
399			// TODO(sanjay): Report that the covered addresses are missing.
400			continue
401		}
402
403		var lastFrames []plugin.Frame
404		var lastAddr, maxAddr uint64
405		for i, inst := range insts {
406			addr := inst.Addr + base
407
408			// Guard against duplicate output from Disasm.
409			if addr <= maxAddr {
410				continue
411			}
412			maxAddr = addr
413
414			length := 1
415			if i+1 < len(insts) && insts[i+1].Addr > inst.Addr {
416				// Extend to next instruction.
417				length = int(insts[i+1].Addr - inst.Addr)
418			}
419
420			// Get inlined-call-stack for address.
421			frames, err := r.obj.SourceLine(addr)
422			if err != nil {
423				// Construct a frame from disassembler output.
424				frames = []plugin.Frame{{Func: inst.Function, File: inst.File, Line: inst.Line}}
425			}
426
427			x := instructionInfo{objAddr: inst.Addr, length: length, disasm: inst.Text}
428			if len(frames) > 0 {
429				// We could consider using the outer-most caller's source
430				// location so we give the some hint as to where the
431				// inlining happened that led to this instruction. So for
432				// example, suppose we have the following (inlined) call
433				// chains for this instruction:
434				//   F1->G->H
435				//   F2->G->H
436				// We could tag the instructions from the first call with
437				// F1 and instructions from the second call with F2. But
438				// that leads to a somewhat confusing display. So for now,
439				// we stick with just the inner-most location (i.e., H).
440				// In the future we will consider changing the display to
441				// make caller info more visible.
442				index := 0 // Inner-most frame
443				x.file = frames[index].File
444				x.line = frames[index].Line
445			}
446			sp.insts[addr] = x
447
448			// We sometimes get instructions with a zero reported line number.
449			// Make such instructions have the same line info as the preceding
450			// instruction, if an earlier instruction is found close enough.
451			const neighborhood = 32
452			if len(frames) > 0 && frames[0].Line != 0 {
453				lastFrames = frames
454				lastAddr = addr
455			} else if (addr-lastAddr <= neighborhood) && lastFrames != nil {
456				frames = lastFrames
457			}
458
459			sp.addStack(addr, frames)
460		}
461	}
462}
463
464func (sp *sourcePrinter) addStack(addr uint64, frames []plugin.Frame) {
465	// See if the stack contains a function we are interested in.
466	for i, f := range frames {
467		if !sp.interest[f.Func] {
468			continue
469		}
470
471		// Record sub-stack under frame's file/line.
472		fname := canonicalizeFileName(f.File)
473		file := sp.files[fname]
474		if file == nil {
475			file = &sourceFile{
476				fname:    fname,
477				lines:    map[int][]sourceInst{},
478				funcName: map[int]string{},
479			}
480			sp.files[fname] = file
481		}
482		callees := frames[:i]
483		stack := make([]callID, 0, len(callees))
484		for j := len(callees) - 1; j >= 0; j-- { // Reverse so caller is first
485			stack = append(stack, callID{
486				file: callees[j].File,
487				line: callees[j].Line,
488			})
489		}
490		file.lines[f.Line] = append(file.lines[f.Line], sourceInst{addr, stack})
491
492		// Remember the first function name encountered per source line
493		// and assume that that line belongs to that function.
494		if _, ok := file.funcName[f.Line]; !ok {
495			file.funcName[f.Line] = f.Func
496		}
497	}
498}
499
500// synthAsm is the special disassembler value used for instructions without an object file.
501const synthAsm = ""
502
503// handleUnprocessed handles addresses that were skipped by splitIntoRanges because they
504// did not belong to a known object file.
505func (sp *sourcePrinter) handleUnprocessed(addrs map[uint64]addrInfo, unprocessed []uint64) {
506	// makeFrames synthesizes a []plugin.Frame list for the specified address.
507	// The result will typically have length 1, but may be longer if address corresponds
508	// to inlined calls.
509	makeFrames := func(addr uint64) []plugin.Frame {
510		loc := addrs[addr].loc
511		stack := make([]plugin.Frame, 0, len(loc.Line))
512		for _, line := range loc.Line {
513			fn := line.Function
514			if fn == nil {
515				continue
516			}
517			stack = append(stack, plugin.Frame{
518				Func: fn.Name,
519				File: fn.Filename,
520				Line: int(line.Line),
521			})
522		}
523		return stack
524	}
525
526	for _, addr := range unprocessed {
527		frames := makeFrames(addr)
528		x := instructionInfo{
529			objAddr: addr,
530			length:  1,
531			disasm:  synthAsm,
532		}
533		if len(frames) > 0 {
534			x.file = frames[0].File
535			x.line = frames[0].Line
536		}
537		sp.insts[addr] = x
538
539		sp.addStack(addr, frames)
540	}
541}
542
543// splitIntoRanges converts the set of addresses we are interested in into a set of address
544// ranges to disassemble. It also returns the set of addresses found that did not have an
545// associated object file and were therefore not added to an address range.
546func (sp *sourcePrinter) splitIntoRanges(prof *profile.Profile, addrMap map[uint64]addrInfo, flat map[uint64]int64) ([]addressRange, []uint64) {
547	// Partition addresses into two sets: ones with a known object file, and ones without.
548	var addrs, unprocessed []uint64
549	for addr, info := range addrMap {
550		if info.obj != nil {
551			addrs = append(addrs, addr)
552		} else {
553			unprocessed = append(unprocessed, addr)
554		}
555	}
556	sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] })
557
558	const expand = 500 // How much to expand range to pick up nearby addresses.
559	var result []addressRange
560	for i, n := 0, len(addrs); i < n; {
561		begin, end := addrs[i], addrs[i]
562		sum := flat[begin]
563		i++
564
565		info := addrMap[begin]
566		m := info.loc.Mapping
567		obj := info.obj // Non-nil because of the partitioning done above.
568
569		// Find following addresses that are close enough to addrs[i].
570		for i < n && addrs[i] <= end+2*expand && addrs[i] < m.Limit {
571			// When we expand ranges by "expand" on either side, the ranges
572			// for addrs[i] and addrs[i-1] will merge.
573			end = addrs[i]
574			sum += flat[end]
575			i++
576		}
577		if m.Start-begin >= expand {
578			begin -= expand
579		} else {
580			begin = m.Start
581		}
582		if m.Limit-end >= expand {
583			end += expand
584		} else {
585			end = m.Limit
586		}
587
588		result = append(result, addressRange{begin, end, obj, m, sum})
589	}
590	return result, unprocessed
591}
592
593func (sp *sourcePrinter) initSamples(flat, cum map[uint64]int64) {
594	for addr, inst := range sp.insts {
595		// Move all samples that were assigned to the middle of an instruction to the
596		// beginning of that instruction. This takes care of samples that were recorded
597		// against pc+1.
598		instEnd := addr + uint64(inst.length)
599		for p := addr; p < instEnd; p++ {
600			inst.flat += flat[p]
601			inst.cum += cum[p]
602		}
603		sp.insts[addr] = inst
604	}
605}
606
607func (sp *sourcePrinter) generate(maxFiles int, rpt *Report) WebListData {
608	// Finalize per-file counts.
609	for _, file := range sp.files {
610		seen := map[uint64]bool{}
611		for _, line := range file.lines {
612			for _, x := range line {
613				if seen[x.addr] {
614					// Same address can be displayed multiple times in a file
615					// (e.g., if we show multiple inlined functions).
616					// Avoid double-counting samples in this case.
617					continue
618				}
619				seen[x.addr] = true
620				inst := sp.insts[x.addr]
621				file.cum += inst.cum
622				file.flat += inst.flat
623			}
624		}
625	}
626
627	// Get sorted list of files to print.
628	var files []*sourceFile
629	for _, f := range sp.files {
630		files = append(files, f)
631	}
632	order := func(i, j int) bool { return files[i].flat > files[j].flat }
633	if maxFiles < 0 {
634		// Order by name for compatibility with old code.
635		order = func(i, j int) bool { return files[i].fname < files[j].fname }
636		maxFiles = len(files)
637	}
638	sort.Slice(files, order)
639	result := WebListData{
640		Total: rpt.formatValue(rpt.total),
641	}
642	for i, f := range files {
643		if i < maxFiles {
644			result.Files = append(result.Files, sp.generateFile(f, rpt))
645		}
646	}
647	return result
648}
649
650func (sp *sourcePrinter) generateFile(f *sourceFile, rpt *Report) WebListFile {
651	var result WebListFile
652	for _, fn := range sp.functions(f) {
653		if fn.cum == 0 {
654			continue
655		}
656
657		listfn := WebListFunc{
658			Name:       fn.name,
659			File:       f.fname,
660			Flat:       rpt.formatValue(fn.flat),
661			Cumulative: rpt.formatValue(fn.cum),
662			Percent:    measurement.Percentage(fn.cum, rpt.total),
663		}
664		var asm []assemblyInstruction
665		for l := fn.begin; l < fn.end; l++ {
666			lineContents, ok := sp.reader.line(f.fname, l)
667			if !ok {
668				if len(f.lines[l]) == 0 {
669					// Outside of range of valid lines and nothing to print.
670					continue
671				}
672				if l == 0 {
673					// Line number 0 shows up if line number is not known.
674					lineContents = "<instructions with unknown line numbers>"
675				} else {
676					// Past end of file, but have data to print.
677					lineContents = "???"
678				}
679			}
680
681			// Make list of assembly instructions.
682			asm = asm[:0]
683			var flatSum, cumSum int64
684			var lastAddr uint64
685			for _, inst := range f.lines[l] {
686				addr := inst.addr
687				x := sp.insts[addr]
688				flatSum += x.flat
689				cumSum += x.cum
690				startsBlock := (addr != lastAddr+uint64(sp.insts[lastAddr].length))
691				lastAddr = addr
692
693				// divisors already applied, so leave flatDiv,cumDiv as 0
694				asm = append(asm, assemblyInstruction{
695					address:     x.objAddr,
696					instruction: x.disasm,
697					function:    fn.name,
698					file:        x.file,
699					line:        x.line,
700					flat:        x.flat,
701					cum:         x.cum,
702					startsBlock: startsBlock,
703					inlineCalls: inst.stack,
704				})
705			}
706
707			listfn.Lines = append(listfn.Lines, makeWebListLine(l, flatSum, cumSum, lineContents, asm, sp.reader, rpt))
708		}
709
710		result.Funcs = append(result.Funcs, listfn)
711	}
712	return result
713}
714
715// functions splits apart the lines to show in a file into a list of per-function ranges.
716func (sp *sourcePrinter) functions(f *sourceFile) []sourceFunction {
717	var funcs []sourceFunction
718
719	// Get interesting lines in sorted order.
720	lines := make([]int, 0, len(f.lines))
721	for l := range f.lines {
722		lines = append(lines, l)
723	}
724	sort.Ints(lines)
725
726	// Merge adjacent lines that are in same function and not too far apart.
727	const mergeLimit = 20
728	for _, l := range lines {
729		name := f.funcName[l]
730		if pretty, ok := sp.prettyNames[name]; ok {
731			// Use demangled name if available.
732			name = pretty
733		}
734
735		fn := sourceFunction{name: name, begin: l, end: l + 1}
736		for _, x := range f.lines[l] {
737			inst := sp.insts[x.addr]
738			fn.flat += inst.flat
739			fn.cum += inst.cum
740		}
741
742		// See if we should merge into preceding function.
743		if len(funcs) > 0 {
744			last := funcs[len(funcs)-1]
745			if l-last.end < mergeLimit && last.name == name {
746				last.end = l + 1
747				last.flat += fn.flat
748				last.cum += fn.cum
749				funcs[len(funcs)-1] = last
750				continue
751			}
752		}
753
754		// Add new function.
755		funcs = append(funcs, fn)
756	}
757
758	// Expand function boundaries to show neighborhood.
759	const expand = 5
760	for i, f := range funcs {
761		if i == 0 {
762			// Extend backwards, stopping at line number 1, but do not disturb 0
763			// since that is a special line number that can show up when addr2line
764			// cannot determine the real line number.
765			if f.begin > expand {
766				f.begin -= expand
767			} else if f.begin > 1 {
768				f.begin = 1
769			}
770		} else {
771			// Find gap from predecessor and divide between predecessor and f.
772			halfGap := (f.begin - funcs[i-1].end) / 2
773			if halfGap > expand {
774				halfGap = expand
775			}
776			funcs[i-1].end += halfGap
777			f.begin -= halfGap
778		}
779		funcs[i] = f
780	}
781
782	// Also extend the ending point of the last function.
783	if len(funcs) > 0 {
784		funcs[len(funcs)-1].end += expand
785	}
786
787	return funcs
788}
789
790// objectFile return the object for the specified mapping, opening it if necessary.
791// It returns nil on error.
792func (sp *sourcePrinter) objectFile(m *profile.Mapping) plugin.ObjFile {
793	if m == nil {
794		return nil
795	}
796	if object, ok := sp.objects[m.File]; ok {
797		return object // May be nil if we detected an error earlier.
798	}
799	object, err := sp.objectTool.Open(m.File, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol)
800	if err != nil {
801		object = nil
802	}
803	sp.objects[m.File] = object // Cache even on error.
804	return object
805}
806
807// makeWebListLine returns the contents of a single line in a web listing. This includes
808// the source line and the corresponding assembly.
809func makeWebListLine(lineNo int, flat, cum int64, lineContents string,
810	assembly []assemblyInstruction, reader *sourceReader, rpt *Report) WebListLine {
811	line := WebListLine{
812		SrcLine:    lineContents,
813		Line:       lineNo,
814		Flat:       valueOrDot(flat, rpt),
815		Cumulative: valueOrDot(cum, rpt),
816	}
817
818	if len(assembly) == 0 {
819		line.HTMLClass = "nop"
820		return line
821	}
822
823	nestedInfo := false
824	line.HTMLClass = "deadsrc"
825	for _, an := range assembly {
826		if len(an.inlineCalls) > 0 || an.instruction != synthAsm {
827			nestedInfo = true
828			line.HTMLClass = "livesrc"
829		}
830	}
831
832	if nestedInfo {
833		srcIndent := indentation(lineContents)
834		line.Instructions = makeWebListInstructions(srcIndent, assembly, reader, rpt)
835	}
836	return line
837}
838
839func makeWebListInstructions(srcIndent int, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) []WebListInstruction {
840	var result []WebListInstruction
841	var curCalls []callID
842	for i, an := range assembly {
843		var fileline string
844		if an.file != "" {
845			fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(filepath.Base(an.file)), an.line)
846		}
847		text := strings.Repeat(" ", srcIndent+4+4*len(an.inlineCalls)) + an.instruction
848		inst := WebListInstruction{
849			NewBlock:   (an.startsBlock && i != 0),
850			Flat:       valueOrDot(an.flat, rpt),
851			Cumulative: valueOrDot(an.cum, rpt),
852			Synthetic:  (an.instruction == synthAsm),
853			Address:    an.address,
854			Disasm:     rightPad(text, 80),
855			FileLine:   fileline,
856		}
857
858		// Add inlined call context.
859		for j, c := range an.inlineCalls {
860			if j < len(curCalls) && curCalls[j] == c {
861				// Skip if same as previous instruction.
862				continue
863			}
864			curCalls = nil
865			fline, ok := reader.line(c.file, c.line)
866			if !ok {
867				fline = ""
868			}
869			srcCode := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline)
870			inst.InlinedCalls = append(inst.InlinedCalls, WebListCall{
871				SrcLine:  rightPad(srcCode, 80),
872				FileBase: filepath.Base(c.file),
873				Line:     c.line,
874			})
875		}
876		curCalls = an.inlineCalls
877
878		result = append(result, inst)
879	}
880	return result
881}
882
883// getSourceFromFile collects the sources of a function from a source
884// file and annotates it with the samples in fns. Returns the sources
885// as nodes, using the info.name field to hold the source code.
886func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
887	lineNodes := make(map[int]graph.Nodes)
888
889	// Collect source coordinates from profile.
890	const margin = 5 // Lines before first/after last sample.
891	if start == 0 {
892		if fns[0].Info.StartLine != 0 {
893			start = fns[0].Info.StartLine
894		} else {
895			start = fns[0].Info.Lineno - margin
896		}
897	} else {
898		start -= margin
899	}
900	if end == 0 {
901		end = fns[0].Info.Lineno
902	}
903	end += margin
904	for _, n := range fns {
905		lineno := n.Info.Lineno
906		nodeStart := n.Info.StartLine
907		if nodeStart == 0 {
908			nodeStart = lineno - margin
909		}
910		nodeEnd := lineno + margin
911		if nodeStart < start {
912			start = nodeStart
913		} else if nodeEnd > end {
914			end = nodeEnd
915		}
916		lineNodes[lineno] = append(lineNodes[lineno], n)
917	}
918	if start < 1 {
919		start = 1
920	}
921
922	var src graph.Nodes
923	for lineno := start; lineno <= end; lineno++ {
924		line, ok := reader.line(file, lineno)
925		if !ok {
926			break
927		}
928		flat, cum := lineNodes[lineno].Sum()
929		src = append(src, &graph.Node{
930			Info: graph.NodeInfo{
931				Name:   strings.TrimRight(line, "\n"),
932				Lineno: lineno,
933			},
934			Flat: flat,
935			Cum:  cum,
936		})
937	}
938	if err := reader.fileError(file); err != nil {
939		return nil, file, err
940	}
941	return src, file, nil
942}
943
944// sourceReader provides access to source code with caching of file contents.
945type sourceReader struct {
946	// searchPath is a filepath.ListSeparator-separated list of directories where
947	// source files should be searched.
948	searchPath string
949
950	// trimPath is a filepath.ListSeparator-separated list of paths to trim.
951	trimPath string
952
953	// files maps from path name to a list of lines.
954	// files[*][0] is unused since line numbering starts at 1.
955	files map[string][]string
956
957	// errors collects errors encountered per file. These errors are
958	// consulted before returning out of these module.
959	errors map[string]error
960}
961
962func newSourceReader(searchPath, trimPath string) *sourceReader {
963	return &sourceReader{
964		searchPath,
965		trimPath,
966		make(map[string][]string),
967		make(map[string]error),
968	}
969}
970
971func (reader *sourceReader) fileError(path string) error {
972	return reader.errors[path]
973}
974
975// line returns the line numbered "lineno" in path, or _,false if lineno is out of range.
976func (reader *sourceReader) line(path string, lineno int) (string, bool) {
977	lines, ok := reader.files[path]
978	if !ok {
979		// Read and cache file contents.
980		lines = []string{""} // Skip 0th line
981		f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
982		if err != nil {
983			reader.errors[path] = err
984		} else {
985			s := bufio.NewScanner(f)
986			for s.Scan() {
987				lines = append(lines, s.Text())
988			}
989			f.Close()
990			if s.Err() != nil {
991				reader.errors[path] = err
992			}
993		}
994		reader.files[path] = lines
995	}
996	if lineno <= 0 || lineno >= len(lines) {
997		return "", false
998	}
999	return lines[lineno], true
1000}
1001
1002// openSourceFile opens a source file from a name encoded in a profile. File
1003// names in a profile after can be relative paths, so search them in each of
1004// the paths in searchPath and their parents. In case the profile contains
1005// absolute paths, additional paths may be configured to trim from the source
1006// paths in the profile. This effectively turns the path into a relative path
1007// searching it using searchPath as usual).
1008func openSourceFile(path, searchPath, trim string) (*os.File, error) {
1009	path = trimPath(path, trim, searchPath)
1010	// If file is still absolute, require file to exist.
1011	if filepath.IsAbs(path) {
1012		f, err := os.Open(path)
1013		return f, err
1014	}
1015	// Scan each component of the path.
1016	for _, dir := range filepath.SplitList(searchPath) {
1017		// Search up for every parent of each possible path.
1018		for {
1019			filename := filepath.Join(dir, path)
1020			if f, err := os.Open(filename); err == nil {
1021				return f, nil
1022			}
1023			parent := filepath.Dir(dir)
1024			if parent == dir {
1025				break
1026			}
1027			dir = parent
1028		}
1029	}
1030
1031	return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath)
1032}
1033
1034// trimPath cleans up a path by removing prefixes that are commonly
1035// found on profiles plus configured prefixes.
1036// TODO(aalexand): Consider optimizing out the redundant work done in this
1037// function if it proves to matter.
1038func trimPath(path, trimPath, searchPath string) string {
1039	// Keep path variable intact as it's used below to form the return value.
1040	sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath)
1041	if trimPath == "" {
1042		// If the trim path is not configured, try to guess it heuristically:
1043		// search for basename of each search path in the original path and, if
1044		// found, strip everything up to and including the basename. So, for
1045		// example, given original path "/some/remote/path/my-project/foo/bar.c"
1046		// and search path "/my/local/path/my-project" the heuristic will return
1047		// "/my/local/path/my-project/foo/bar.c".
1048		for _, dir := range filepath.SplitList(searchPath) {
1049			want := "/" + filepath.Base(dir) + "/"
1050			if found := strings.Index(sPath, want); found != -1 {
1051				return path[found+len(want):]
1052			}
1053		}
1054	}
1055	// Trim configured trim prefixes.
1056	trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/")
1057	for _, trimPath := range trimPaths {
1058		if !strings.HasSuffix(trimPath, "/") {
1059			trimPath += "/"
1060		}
1061		if strings.HasPrefix(sPath, trimPath) {
1062			return path[len(trimPath):]
1063		}
1064	}
1065	return path
1066}
1067
1068func indentation(line string) int {
1069	column := 0
1070	for _, c := range line {
1071		if c == ' ' {
1072			column++
1073		} else if c == '\t' {
1074			column++
1075			for column%8 != 0 {
1076				column++
1077			}
1078		} else {
1079			break
1080		}
1081	}
1082	return column
1083}
1084
1085// rightPad pads the input with spaces on the right-hand-side to make it have
1086// at least width n. It treats tabs as enough spaces that lead to the next
1087// 8-aligned tab-stop.
1088func rightPad(s string, n int) string {
1089	var str strings.Builder
1090
1091	// Convert tabs to spaces as we go so padding works regardless of what prefix
1092	// is placed before the result.
1093	column := 0
1094	for _, c := range s {
1095		column++
1096		if c == '\t' {
1097			str.WriteRune(' ')
1098			for column%8 != 0 {
1099				column++
1100				str.WriteRune(' ')
1101			}
1102		} else {
1103			str.WriteRune(c)
1104		}
1105	}
1106	for column < n {
1107		column++
1108		str.WriteRune(' ')
1109	}
1110	return str.String()
1111}
1112
1113func canonicalizeFileName(fname string) string {
1114	fname = strings.TrimPrefix(fname, "/proc/self/cwd/")
1115	fname = strings.TrimPrefix(fname, "./")
1116	return filepath.Clean(fname)
1117}
1118