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 15 package report 16 17 // This file contains routines related to the generation of annotated 18 // source listings. 19 20 import ( 21 "bufio" 22 "fmt" 23 "html/template" 24 "io" 25 "os" 26 "path/filepath" 27 "strconv" 28 "strings" 29 30 "github.com/google/pprof/internal/graph" 31 "github.com/google/pprof/internal/plugin" 32 ) 33 34 // printSource prints an annotated source listing, include all 35 // functions with samples that match the regexp rpt.options.symbol. 36 // The sources are sorted by function name and then by filename to 37 // eliminate potential nondeterminism. 38 func printSource(w io.Writer, rpt *Report) error { 39 o := rpt.options 40 g := rpt.newGraph(nil) 41 42 // Identify all the functions that match the regexp provided. 43 // Group nodes for each matching function. 44 var functions graph.Nodes 45 functionNodes := make(map[string]graph.Nodes) 46 for _, n := range g.Nodes { 47 if !o.Symbol.MatchString(n.Info.Name) { 48 continue 49 } 50 if functionNodes[n.Info.Name] == nil { 51 functions = append(functions, n) 52 } 53 functionNodes[n.Info.Name] = append(functionNodes[n.Info.Name], n) 54 } 55 functions.Sort(graph.NameOrder) 56 57 sourcePath := o.SourcePath 58 if sourcePath == "" { 59 wd, err := os.Getwd() 60 if err != nil { 61 return fmt.Errorf("Could not stat current dir: %v", err) 62 } 63 sourcePath = wd 64 } 65 reader := newSourceReader(sourcePath) 66 67 fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) 68 for _, fn := range functions { 69 name := fn.Info.Name 70 71 // Identify all the source files associated to this function. 72 // Group nodes for each source file. 73 var sourceFiles graph.Nodes 74 fileNodes := make(map[string]graph.Nodes) 75 for _, n := range functionNodes[name] { 76 if n.Info.File == "" { 77 continue 78 } 79 if fileNodes[n.Info.File] == nil { 80 sourceFiles = append(sourceFiles, n) 81 } 82 fileNodes[n.Info.File] = append(fileNodes[n.Info.File], n) 83 } 84 85 if len(sourceFiles) == 0 { 86 fmt.Fprintf(w, "No source information for %s\n", name) 87 continue 88 } 89 90 sourceFiles.Sort(graph.FileOrder) 91 92 // Print each file associated with this function. 93 for _, fl := range sourceFiles { 94 filename := fl.Info.File 95 fns := fileNodes[filename] 96 flatSum, cumSum := fns.Sum() 97 98 fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0) 99 fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename) 100 fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", 101 rpt.formatValue(flatSum), rpt.formatValue(cumSum), 102 percentage(cumSum, rpt.total)) 103 104 if err != nil { 105 fmt.Fprintf(w, " Error: %v\n", err) 106 continue 107 } 108 109 for _, fn := range fnodes { 110 fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), fn.Info.Lineno, fn.Info.Name) 111 } 112 } 113 } 114 return nil 115 } 116 117 // printWebSource prints an annotated source listing, include all 118 // functions with samples that match the regexp rpt.options.symbol. 119 func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 120 printHeader(w, rpt) 121 if err := PrintWebList(w, rpt, obj, -1); err != nil { 122 return err 123 } 124 printPageClosing(w) 125 return nil 126 } 127 128 // PrintWebList prints annotated source listing of rpt to w. 129 func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error { 130 o := rpt.options 131 g := rpt.newGraph(nil) 132 133 // If the regexp source can be parsed as an address, also match 134 // functions that land on that address. 135 var address *uint64 136 if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { 137 address = &hex 138 } 139 140 sourcePath := o.SourcePath 141 if sourcePath == "" { 142 wd, err := os.Getwd() 143 if err != nil { 144 return fmt.Errorf("Could not stat current dir: %v", err) 145 } 146 sourcePath = wd 147 } 148 reader := newSourceReader(sourcePath) 149 150 type fileFunction struct { 151 fileName, functionName string 152 } 153 154 // Extract interesting symbols from binary files in the profile and 155 // classify samples per symbol. 156 symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj) 157 symNodes := nodesPerSymbol(g.Nodes, symbols) 158 159 // Identify sources associated to a symbol by examining 160 // symbol samples. Classify samples per source file. 161 fileNodes := make(map[fileFunction]graph.Nodes) 162 if len(symNodes) == 0 { 163 for _, n := range g.Nodes { 164 if n.Info.File == "" || !o.Symbol.MatchString(n.Info.Name) { 165 continue 166 } 167 ff := fileFunction{n.Info.File, n.Info.Name} 168 fileNodes[ff] = append(fileNodes[ff], n) 169 } 170 } else { 171 for _, nodes := range symNodes { 172 for _, n := range nodes { 173 if n.Info.File != "" { 174 ff := fileFunction{n.Info.File, n.Info.Name} 175 fileNodes[ff] = append(fileNodes[ff], n) 176 } 177 } 178 } 179 } 180 181 if len(fileNodes) == 0 { 182 return fmt.Errorf("No source information for %s", o.Symbol.String()) 183 } 184 185 sourceFiles := make(graph.Nodes, 0, len(fileNodes)) 186 for _, nodes := range fileNodes { 187 sNode := *nodes[0] 188 sNode.Flat, sNode.Cum = nodes.Sum() 189 sourceFiles = append(sourceFiles, &sNode) 190 } 191 192 // Limit number of files printed? 193 if maxFiles < 0 { 194 sourceFiles.Sort(graph.FileOrder) 195 } else { 196 sourceFiles.Sort(graph.FlatNameOrder) 197 if maxFiles < len(sourceFiles) { 198 sourceFiles = sourceFiles[:maxFiles] 199 } 200 } 201 202 // Print each file associated with this function. 203 for _, n := range sourceFiles { 204 ff := fileFunction{n.Info.File, n.Info.Name} 205 fns := fileNodes[ff] 206 207 asm := assemblyPerSourceLine(symbols, fns, ff.fileName, obj) 208 start, end := sourceCoordinates(asm) 209 210 fnodes, path, err := getSourceFromFile(ff.fileName, reader, fns, start, end) 211 if err != nil { 212 fnodes, path = getMissingFunctionSource(ff.fileName, asm, start, end) 213 } 214 215 printFunctionHeader(w, ff.functionName, path, n.Flat, n.Cum, rpt) 216 for _, fn := range fnodes { 217 printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], reader, rpt) 218 } 219 printFunctionClosing(w) 220 } 221 return nil 222 } 223 224 // sourceCoordinates returns the lowest and highest line numbers from 225 // a set of assembly statements. 226 func sourceCoordinates(asm map[int][]assemblyInstruction) (start, end int) { 227 for l := range asm { 228 if start == 0 || l < start { 229 start = l 230 } 231 if end == 0 || l > end { 232 end = l 233 } 234 } 235 return start, end 236 } 237 238 // assemblyPerSourceLine disassembles the binary containing a symbol 239 // and classifies the assembly instructions according to its 240 // corresponding source line, annotating them with a set of samples. 241 func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj plugin.ObjTool) map[int][]assemblyInstruction { 242 assembly := make(map[int][]assemblyInstruction) 243 // Identify symbol to use for this collection of samples. 244 o := findMatchingSymbol(objSyms, rs) 245 if o == nil { 246 return assembly 247 } 248 249 // Extract assembly for matched symbol 250 insts, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End) 251 if err != nil { 252 return assembly 253 } 254 255 srcBase := filepath.Base(src) 256 anodes := annotateAssembly(insts, rs, o.base) 257 var lineno = 0 258 var prevline = 0 259 for _, an := range anodes { 260 // Do not rely solely on the line number produced by Disasm 261 // since it is not what we want in the presence of inlining. 262 // 263 // E.g., suppose we are printing source code for F and this 264 // instruction is from H where F called G called H and both 265 // of those calls were inlined. We want to use the line 266 // number from F, not from H (which is what Disasm gives us). 267 // 268 // So find the outer-most linenumber in the source file. 269 found := false 270 if frames, err := o.file.SourceLine(an.address + o.base); err == nil { 271 for i := len(frames) - 1; i >= 0; i-- { 272 if filepath.Base(frames[i].File) == srcBase { 273 for j := i - 1; j >= 0; j-- { 274 an.inlineCalls = append(an.inlineCalls, callID{frames[j].File, frames[j].Line}) 275 } 276 lineno = frames[i].Line 277 found = true 278 break 279 } 280 } 281 } 282 if !found && filepath.Base(an.file) == srcBase { 283 lineno = an.line 284 } 285 286 if lineno != 0 { 287 if lineno != prevline { 288 // This instruction starts a new block 289 // of contiguous instructions on this line. 290 an.startsBlock = true 291 } 292 prevline = lineno 293 assembly[lineno] = append(assembly[lineno], an) 294 } 295 } 296 297 return assembly 298 } 299 300 // findMatchingSymbol looks for the symbol that corresponds to a set 301 // of samples, by comparing their addresses. 302 func findMatchingSymbol(objSyms []*objSymbol, ns graph.Nodes) *objSymbol { 303 for _, n := range ns { 304 for _, o := range objSyms { 305 if filepath.Base(o.sym.File) == filepath.Base(n.Info.Objfile) && 306 o.sym.Start <= n.Info.Address-o.base && 307 n.Info.Address-o.base <= o.sym.End { 308 return o 309 } 310 } 311 } 312 return nil 313 } 314 315 // printHeader prints the page header for a weblist report. 316 func printHeader(w io.Writer, rpt *Report) { 317 fmt.Fprintln(w, ` 318 <!DOCTYPE html> 319 <html> 320 <head> 321 <meta charset="UTF-8"> 322 <title>Pprof listing</title>`) 323 fmt.Fprintln(w, weblistPageCSS) 324 fmt.Fprintln(w, weblistPageScript) 325 fmt.Fprint(w, "</head>\n<body>\n\n") 326 327 var labels []string 328 for _, l := range ProfileLabels(rpt) { 329 labels = append(labels, template.HTMLEscapeString(l)) 330 } 331 332 fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`, 333 strings.Join(labels, "<br>\n"), 334 rpt.formatValue(rpt.total), 335 ) 336 } 337 338 // printFunctionHeader prints a function header for a weblist report. 339 func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) { 340 fmt.Fprintf(w, `<h1>%s</h1>%s 341 <pre onClick="pprof_toggle_asm(event)"> 342 Total: %10s %10s (flat, cum) %s 343 `, 344 template.HTMLEscapeString(name), template.HTMLEscapeString(path), 345 rpt.formatValue(flatSum), rpt.formatValue(cumSum), 346 percentage(cumSum, rpt.total)) 347 } 348 349 // printFunctionSourceLine prints a source line and the corresponding assembly. 350 func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) { 351 if len(assembly) == 0 { 352 fmt.Fprintf(w, 353 "<span class=line> %6d</span> <span class=nop> %10s %10s %8s %s </span>\n", 354 fn.Info.Lineno, 355 valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), 356 "", template.HTMLEscapeString(fn.Info.Name)) 357 return 358 } 359 360 fmt.Fprintf(w, 361 "<span class=line> %6d</span> <span class=deadsrc> %10s %10s %8s %s </span>", 362 fn.Info.Lineno, 363 valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), 364 "", template.HTMLEscapeString(fn.Info.Name)) 365 srcIndent := indentation(fn.Info.Name) 366 fmt.Fprint(w, "<span class=asm>") 367 var curCalls []callID 368 for i, an := range assembly { 369 if an.startsBlock && i != 0 { 370 // Insert a separator between discontiguous blocks. 371 fmt.Fprintf(w, " %8s %28s\n", "", "") 372 } 373 374 var fileline string 375 if an.file != "" { 376 fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.file), an.line) 377 } 378 flat, cum := an.flat, an.cum 379 if an.flatDiv != 0 { 380 flat = flat / an.flatDiv 381 } 382 if an.cumDiv != 0 { 383 cum = cum / an.cumDiv 384 } 385 386 // Print inlined call context. 387 for j, c := range an.inlineCalls { 388 if j < len(curCalls) && curCalls[j] == c { 389 // Skip if same as previous instruction. 390 continue 391 } 392 curCalls = nil 393 fname := trimPath(c.file) 394 fline, ok := reader.line(fname, c.line) 395 if !ok { 396 fline = "" 397 } 398 text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline) 399 fmt.Fprintf(w, " %8s %10s %10s %8s <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n", 400 "", "", "", "", 401 template.HTMLEscapeString(fmt.Sprintf("%-80s", text)), 402 template.HTMLEscapeString(filepath.Base(fname)), c.line) 403 } 404 curCalls = an.inlineCalls 405 text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction 406 fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n", 407 "", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address, 408 template.HTMLEscapeString(fmt.Sprintf("%-80s", text)), 409 template.HTMLEscapeString(fileline)) 410 } 411 fmt.Fprintln(w, "</span>") 412 } 413 414 // printFunctionClosing prints the end of a function in a weblist report. 415 func printFunctionClosing(w io.Writer) { 416 fmt.Fprintln(w, "</pre>") 417 } 418 419 // printPageClosing prints the end of the page in a weblist report. 420 func printPageClosing(w io.Writer) { 421 fmt.Fprintln(w, weblistPageClosing) 422 } 423 424 // getSourceFromFile collects the sources of a function from a source 425 // file and annotates it with the samples in fns. Returns the sources 426 // as nodes, using the info.name field to hold the source code. 427 func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) { 428 file = trimPath(file) 429 lineNodes := make(map[int]graph.Nodes) 430 431 // Collect source coordinates from profile. 432 const margin = 5 // Lines before first/after last sample. 433 if start == 0 { 434 if fns[0].Info.StartLine != 0 { 435 start = fns[0].Info.StartLine 436 } else { 437 start = fns[0].Info.Lineno - margin 438 } 439 } else { 440 start -= margin 441 } 442 if end == 0 { 443 end = fns[0].Info.Lineno 444 } 445 end += margin 446 for _, n := range fns { 447 lineno := n.Info.Lineno 448 nodeStart := n.Info.StartLine 449 if nodeStart == 0 { 450 nodeStart = lineno - margin 451 } 452 nodeEnd := lineno + margin 453 if nodeStart < start { 454 start = nodeStart 455 } else if nodeEnd > end { 456 end = nodeEnd 457 } 458 lineNodes[lineno] = append(lineNodes[lineno], n) 459 } 460 if start < 1 { 461 start = 1 462 } 463 464 var src graph.Nodes 465 for lineno := start; lineno <= end; lineno++ { 466 line, ok := reader.line(file, lineno) 467 if !ok { 468 break 469 } 470 flat, cum := lineNodes[lineno].Sum() 471 src = append(src, &graph.Node{ 472 Info: graph.NodeInfo{ 473 Name: strings.TrimRight(line, "\n"), 474 Lineno: lineno, 475 }, 476 Flat: flat, 477 Cum: cum, 478 }) 479 } 480 if err := reader.fileError(file); err != nil { 481 return nil, file, err 482 } 483 return src, file, nil 484 } 485 486 // getMissingFunctionSource creates a dummy function body to point to 487 // the source file and annotates it with the samples in asm. 488 func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction, start, end int) (graph.Nodes, string) { 489 var fnodes graph.Nodes 490 for i := start; i <= end; i++ { 491 insts := asm[i] 492 if len(insts) == 0 { 493 continue 494 } 495 var group assemblyInstruction 496 for _, insn := range insts { 497 group.flat += insn.flat 498 group.cum += insn.cum 499 group.flatDiv += insn.flatDiv 500 group.cumDiv += insn.cumDiv 501 } 502 flat := group.flatValue() 503 cum := group.cumValue() 504 fnodes = append(fnodes, &graph.Node{ 505 Info: graph.NodeInfo{ 506 Name: "???", 507 Lineno: i, 508 }, 509 Flat: flat, 510 Cum: cum, 511 }) 512 } 513 return fnodes, filename 514 } 515 516 // sourceReader provides access to source code with caching of file contents. 517 type sourceReader struct { 518 searchPath string 519 520 // files maps from path name to a list of lines. 521 // files[*][0] is unused since line numbering starts at 1. 522 files map[string][]string 523 524 // errors collects errors encountered per file. These errors are 525 // consulted before returning out of these module. 526 errors map[string]error 527 } 528 529 func newSourceReader(searchPath string) *sourceReader { 530 return &sourceReader{ 531 searchPath, 532 make(map[string][]string), 533 make(map[string]error), 534 } 535 } 536 537 func (reader *sourceReader) fileError(path string) error { 538 return reader.errors[path] 539 } 540 541 func (reader *sourceReader) line(path string, lineno int) (string, bool) { 542 lines, ok := reader.files[path] 543 if !ok { 544 // Read and cache file contents. 545 lines = []string{""} // Skip 0th line 546 f, err := openSourceFile(path, reader.searchPath) 547 if err != nil { 548 reader.errors[path] = err 549 } else { 550 s := bufio.NewScanner(f) 551 for s.Scan() { 552 lines = append(lines, s.Text()) 553 } 554 f.Close() 555 if s.Err() != nil { 556 reader.errors[path] = err 557 } 558 } 559 reader.files[path] = lines 560 } 561 if lineno <= 0 || lineno >= len(lines) { 562 return "", false 563 } 564 return lines[lineno], true 565 } 566 567 // openSourceFile opens a source file from a name encoded in a 568 // profile. File names in a profile after often relative paths, so 569 // search them in each of the paths in searchPath (or CWD by default), 570 // and their parents. 571 func openSourceFile(path, searchPath string) (*os.File, error) { 572 if filepath.IsAbs(path) { 573 f, err := os.Open(path) 574 return f, err 575 } 576 577 // Scan each component of the path 578 for _, dir := range strings.Split(searchPath, ":") { 579 // Search up for every parent of each possible path. 580 for { 581 filename := filepath.Join(dir, path) 582 if f, err := os.Open(filename); err == nil { 583 return f, nil 584 } 585 parent := filepath.Dir(dir) 586 if parent == dir { 587 break 588 } 589 dir = parent 590 } 591 } 592 593 return nil, fmt.Errorf("Could not find file %s on path %s", path, searchPath) 594 } 595 596 // trimPath cleans up a path by removing prefixes that are commonly 597 // found on profiles. 598 func trimPath(path string) string { 599 basePaths := []string{ 600 "/proc/self/cwd/./", 601 "/proc/self/cwd/", 602 } 603 604 sPath := filepath.ToSlash(path) 605 606 for _, base := range basePaths { 607 if strings.HasPrefix(sPath, base) { 608 return filepath.FromSlash(sPath[len(base):]) 609 } 610 } 611 return path 612 } 613 614 func indentation(line string) int { 615 column := 0 616 for _, c := range line { 617 if c == ' ' { 618 column++ 619 } else if c == '\t' { 620 column++ 621 for column%8 != 0 { 622 column++ 623 } 624 } else { 625 break 626 } 627 } 628 return column 629 } 630