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