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 summarizes a performance profile into a 16// human-readable report. 17package report 18 19import ( 20 "fmt" 21 "io" 22 "path/filepath" 23 "regexp" 24 "sort" 25 "strconv" 26 "strings" 27 "text/tabwriter" 28 "time" 29 30 "github.com/google/pprof/internal/graph" 31 "github.com/google/pprof/internal/measurement" 32 "github.com/google/pprof/internal/plugin" 33 "github.com/google/pprof/profile" 34) 35 36// Output formats. 37const ( 38 Callgrind = iota 39 Comments 40 Dis 41 Dot 42 List 43 Proto 44 Raw 45 Tags 46 Text 47 TopProto 48 Traces 49 Tree 50 WebList 51) 52 53// Options are the formatting and filtering options used to generate a 54// profile. 55type Options struct { 56 OutputFormat int 57 58 CumSort bool 59 CallTree bool 60 DropNegative bool 61 CompactLabels bool 62 Ratio float64 63 Title string 64 ProfileLabels []string 65 ActiveFilters []string 66 NumLabelUnits map[string]string 67 68 NodeCount int 69 NodeFraction float64 70 EdgeFraction float64 71 72 SampleValue func(s []int64) int64 73 SampleMeanDivisor func(s []int64) int64 74 SampleType string 75 SampleUnit string // Unit for the sample data from the profile. 76 77 OutputUnit string // Units for data formatting in report. 78 79 Symbol *regexp.Regexp // Symbols to include on disassembly report. 80 SourcePath string // Search path for source files. 81 TrimPath string // Paths to trim from source file paths. 82 83 IntelSyntax bool // Whether or not to print assembly in Intel syntax. 84} 85 86// Generate generates a report as directed by the Report. 87func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 88 o := rpt.options 89 90 switch o.OutputFormat { 91 case Comments: 92 return printComments(w, rpt) 93 case Dot: 94 return printDOT(w, rpt) 95 case Tree: 96 return printTree(w, rpt) 97 case Text: 98 return printText(w, rpt) 99 case Traces: 100 return printTraces(w, rpt) 101 case Raw: 102 fmt.Fprint(w, rpt.prof.String()) 103 return nil 104 case Tags: 105 return printTags(w, rpt) 106 case Proto: 107 return printProto(w, rpt) 108 case TopProto: 109 return printTopProto(w, rpt) 110 case Dis: 111 return printAssembly(w, rpt, obj) 112 case List: 113 return printSource(w, rpt) 114 case Callgrind: 115 return printCallgrind(w, rpt) 116 } 117 // Note: WebList handling is in driver package. 118 return fmt.Errorf("unexpected output format %v", o.OutputFormat) 119} 120 121// newTrimmedGraph creates a graph for this report, trimmed according 122// to the report options. 123func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, droppedEdges int) { 124 o := rpt.options 125 126 // Build a graph and refine it. On each refinement step we must rebuild the graph from the samples, 127 // as the graph itself doesn't contain enough information to preserve full precision. 128 visualMode := o.OutputFormat == Dot 129 cumSort := o.CumSort 130 131 // The call_tree option is only honored when generating visual representations of the callgraph. 132 callTree := o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind) 133 134 // First step: Build complete graph to identify low frequency nodes, based on their cum weight. 135 g = rpt.newGraph(nil) 136 totalValue, _ := g.Nodes.Sum() 137 nodeCutoff := abs64(int64(float64(totalValue) * o.NodeFraction)) 138 edgeCutoff := abs64(int64(float64(totalValue) * o.EdgeFraction)) 139 140 // Filter out nodes with cum value below nodeCutoff. 141 if nodeCutoff > 0 { 142 if callTree { 143 if nodesKept := g.DiscardLowFrequencyNodePtrs(nodeCutoff); len(g.Nodes) != len(nodesKept) { 144 droppedNodes = len(g.Nodes) - len(nodesKept) 145 g.TrimTree(nodesKept) 146 } 147 } else { 148 if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept) { 149 droppedNodes = len(g.Nodes) - len(nodesKept) 150 g = rpt.newGraph(nodesKept) 151 } 152 } 153 } 154 origCount = len(g.Nodes) 155 156 // Second step: Limit the total number of nodes. Apply specialized heuristics to improve 157 // visualization when generating dot output. 158 g.SortNodes(cumSort, visualMode) 159 if nodeCount := o.NodeCount; nodeCount > 0 { 160 // Remove low frequency tags and edges as they affect selection. 161 g.TrimLowFrequencyTags(nodeCutoff) 162 g.TrimLowFrequencyEdges(edgeCutoff) 163 if callTree { 164 if nodesKept := g.SelectTopNodePtrs(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) { 165 g.TrimTree(nodesKept) 166 g.SortNodes(cumSort, visualMode) 167 } 168 } else { 169 if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) { 170 g = rpt.newGraph(nodesKept) 171 g.SortNodes(cumSort, visualMode) 172 } 173 } 174 } 175 176 // Final step: Filter out low frequency tags and edges, and remove redundant edges that clutter 177 // the graph. 178 g.TrimLowFrequencyTags(nodeCutoff) 179 droppedEdges = g.TrimLowFrequencyEdges(edgeCutoff) 180 if visualMode { 181 g.RemoveRedundantEdges() 182 } 183 return 184} 185 186func (rpt *Report) selectOutputUnit(g *graph.Graph) { 187 o := rpt.options 188 189 // Select best unit for profile output. 190 // Find the appropriate units for the smallest non-zero sample 191 if o.OutputUnit != "minimum" || len(g.Nodes) == 0 { 192 return 193 } 194 var minValue int64 195 196 for _, n := range g.Nodes { 197 nodeMin := abs64(n.FlatValue()) 198 if nodeMin == 0 { 199 nodeMin = abs64(n.CumValue()) 200 } 201 if nodeMin > 0 && (minValue == 0 || nodeMin < minValue) { 202 minValue = nodeMin 203 } 204 } 205 maxValue := rpt.total 206 if minValue == 0 { 207 minValue = maxValue 208 } 209 210 if r := o.Ratio; r > 0 && r != 1 { 211 minValue = int64(float64(minValue) * r) 212 maxValue = int64(float64(maxValue) * r) 213 } 214 215 _, minUnit := measurement.Scale(minValue, o.SampleUnit, "minimum") 216 _, maxUnit := measurement.Scale(maxValue, o.SampleUnit, "minimum") 217 218 unit := minUnit 219 if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind { 220 // Minimum and maximum values have different units. Scale 221 // minimum by 100 to use larger units, allowing minimum value to 222 // be scaled down to 0.01, except for callgrind reports since 223 // they can only represent integer values. 224 _, unit = measurement.Scale(100*minValue, o.SampleUnit, "minimum") 225 } 226 227 if unit != "" { 228 o.OutputUnit = unit 229 } else { 230 o.OutputUnit = o.SampleUnit 231 } 232} 233 234// newGraph creates a new graph for this report. If nodes is non-nil, 235// only nodes whose info matches are included. Otherwise, all nodes 236// are included, without trimming. 237func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { 238 o := rpt.options 239 240 // Clean up file paths using heuristics. 241 prof := rpt.prof 242 for _, f := range prof.Function { 243 f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath) 244 } 245 // Removes all numeric tags except for the bytes tag prior 246 // to making graph. 247 // TODO: modify to select first numeric tag if no bytes tag 248 for _, s := range prof.Sample { 249 numLabels := make(map[string][]int64, len(s.NumLabel)) 250 numUnits := make(map[string][]string, len(s.NumLabel)) 251 for k, vs := range s.NumLabel { 252 if k == "bytes" { 253 unit := o.NumLabelUnits[k] 254 numValues := make([]int64, len(vs)) 255 numUnit := make([]string, len(vs)) 256 for i, v := range vs { 257 numValues[i] = v 258 numUnit[i] = unit 259 } 260 numLabels[k] = append(numLabels[k], numValues...) 261 numUnits[k] = append(numUnits[k], numUnit...) 262 } 263 } 264 s.NumLabel = numLabels 265 s.NumUnit = numUnits 266 } 267 268 // Remove label marking samples from the base profiles, so it does not appear 269 // as a nodelet in the graph view. 270 prof.RemoveLabel("pprof::base") 271 272 formatTag := func(v int64, key string) string { 273 return measurement.ScaledLabel(v, key, o.OutputUnit) 274 } 275 276 gopt := &graph.Options{ 277 SampleValue: o.SampleValue, 278 SampleMeanDivisor: o.SampleMeanDivisor, 279 FormatTag: formatTag, 280 CallTree: o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind), 281 DropNegative: o.DropNegative, 282 KeptNodes: nodes, 283 } 284 285 // Only keep binary names for disassembly-based reports, otherwise 286 // remove it to allow merging of functions across binaries. 287 switch o.OutputFormat { 288 case Raw, List, WebList, Dis, Callgrind: 289 gopt.ObjNames = true 290 } 291 292 return graph.New(rpt.prof, gopt) 293} 294 295// printProto writes the incoming proto via the writer w. 296// If the divide_by option has been specified, samples are scaled appropriately. 297func printProto(w io.Writer, rpt *Report) error { 298 p, o := rpt.prof, rpt.options 299 300 // Apply the sample ratio to all samples before saving the profile. 301 if r := o.Ratio; r > 0 && r != 1 { 302 for _, sample := range p.Sample { 303 for i, v := range sample.Value { 304 sample.Value[i] = int64(float64(v) * r) 305 } 306 } 307 } 308 return p.Write(w) 309} 310 311// printTopProto writes a list of the hottest routines in a profile as a profile.proto. 312func printTopProto(w io.Writer, rpt *Report) error { 313 p := rpt.prof 314 o := rpt.options 315 g, _, _, _ := rpt.newTrimmedGraph() 316 rpt.selectOutputUnit(g) 317 318 out := profile.Profile{ 319 SampleType: []*profile.ValueType{ 320 {Type: "cum", Unit: o.OutputUnit}, 321 {Type: "flat", Unit: o.OutputUnit}, 322 }, 323 TimeNanos: p.TimeNanos, 324 DurationNanos: p.DurationNanos, 325 PeriodType: p.PeriodType, 326 Period: p.Period, 327 } 328 functionMap := make(functionMap) 329 for i, n := range g.Nodes { 330 f, added := functionMap.findOrAdd(n.Info) 331 if added { 332 out.Function = append(out.Function, f) 333 } 334 flat, cum := n.FlatValue(), n.CumValue() 335 l := &profile.Location{ 336 ID: uint64(i + 1), 337 Address: n.Info.Address, 338 Line: []profile.Line{ 339 { 340 Line: int64(n.Info.Lineno), 341 Column: int64(n.Info.Columnno), 342 Function: f, 343 }, 344 }, 345 } 346 347 fv, _ := measurement.Scale(flat, o.SampleUnit, o.OutputUnit) 348 cv, _ := measurement.Scale(cum, o.SampleUnit, o.OutputUnit) 349 s := &profile.Sample{ 350 Location: []*profile.Location{l}, 351 Value: []int64{int64(cv), int64(fv)}, 352 } 353 out.Location = append(out.Location, l) 354 out.Sample = append(out.Sample, s) 355 } 356 357 return out.Write(w) 358} 359 360type functionMap map[string]*profile.Function 361 362// findOrAdd takes a node representing a function, adds the function 363// represented by the node to the map if the function is not already present, 364// and returns the function the node represents. This also returns a boolean, 365// which is true if the function was added and false otherwise. 366func (fm functionMap) findOrAdd(ni graph.NodeInfo) (*profile.Function, bool) { 367 fName := fmt.Sprintf("%q%q%q%d", ni.Name, ni.OrigName, ni.File, ni.StartLine) 368 369 if f := fm[fName]; f != nil { 370 return f, false 371 } 372 373 f := &profile.Function{ 374 ID: uint64(len(fm) + 1), 375 Name: ni.Name, 376 SystemName: ni.OrigName, 377 Filename: ni.File, 378 StartLine: int64(ni.StartLine), 379 } 380 fm[fName] = f 381 return f, true 382} 383 384// printAssembly prints an annotated assembly listing. 385func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 386 return PrintAssembly(w, rpt, obj, -1) 387} 388 389// PrintAssembly prints annotated disassembly of rpt to w. 390func PrintAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFuncs int) error { 391 o := rpt.options 392 prof := rpt.prof 393 394 g := rpt.newGraph(nil) 395 396 // If the regexp source can be parsed as an address, also match 397 // functions that land on that address. 398 var address *uint64 399 if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { 400 address = &hex 401 } 402 403 fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total)) 404 symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj) 405 symNodes := nodesPerSymbol(g.Nodes, symbols) 406 407 // Sort for printing. 408 var syms []*objSymbol 409 for s := range symNodes { 410 syms = append(syms, s) 411 } 412 byName := func(a, b *objSymbol) bool { 413 if na, nb := a.sym.Name[0], b.sym.Name[0]; na != nb { 414 return na < nb 415 } 416 return a.sym.Start < b.sym.Start 417 } 418 if maxFuncs < 0 { 419 sort.Sort(orderSyms{syms, byName}) 420 } else { 421 byFlatSum := func(a, b *objSymbol) bool { 422 suma, _ := symNodes[a].Sum() 423 sumb, _ := symNodes[b].Sum() 424 if suma != sumb { 425 return suma > sumb 426 } 427 return byName(a, b) 428 } 429 sort.Sort(orderSyms{syms, byFlatSum}) 430 if len(syms) > maxFuncs { 431 syms = syms[:maxFuncs] 432 } 433 } 434 435 if len(syms) == 0 { 436 // The symbol regexp case 437 if address == nil { 438 return fmt.Errorf("no matches found for regexp %s", o.Symbol) 439 } 440 441 // The address case 442 if len(symbols) == 0 { 443 return fmt.Errorf("no matches found for address 0x%x", *address) 444 } 445 return fmt.Errorf("address 0x%x found in binary, but the corresponding symbols do not have samples in the profile", *address) 446 } 447 448 // Correlate the symbols from the binary with the profile samples. 449 for _, s := range syms { 450 sns := symNodes[s] 451 452 // Gather samples for this symbol. 453 flatSum, cumSum := sns.Sum() 454 455 // Get the function assembly. 456 insts, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End, o.IntelSyntax) 457 if err != nil { 458 return err 459 } 460 461 ns := annotateAssembly(insts, sns, s.file) 462 463 fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0]) 464 for _, name := range s.sym.Name[1:] { 465 fmt.Fprintf(w, " AKA ======================== %s\n", name) 466 } 467 fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", 468 rpt.formatValue(flatSum), rpt.formatValue(cumSum), 469 measurement.Percentage(cumSum, rpt.total)) 470 471 function, file, line := "", "", 0 472 for _, n := range ns { 473 locStr := "" 474 // Skip loc information if it hasn't changed from previous instruction. 475 if n.function != function || n.file != file || n.line != line { 476 function, file, line = n.function, n.file, n.line 477 if n.function != "" { 478 locStr = n.function + " " 479 } 480 if n.file != "" { 481 locStr += n.file 482 if n.line != 0 { 483 locStr += fmt.Sprintf(":%d", n.line) 484 } 485 } 486 } 487 switch { 488 case locStr == "": 489 // No location info, just print the instruction. 490 fmt.Fprintf(w, "%10s %10s %10x: %s\n", 491 valueOrDot(n.flatValue(), rpt), 492 valueOrDot(n.cumValue(), rpt), 493 n.address, n.instruction, 494 ) 495 case len(n.instruction) < 40: 496 // Short instruction, print loc on the same line. 497 fmt.Fprintf(w, "%10s %10s %10x: %-40s;%s\n", 498 valueOrDot(n.flatValue(), rpt), 499 valueOrDot(n.cumValue(), rpt), 500 n.address, n.instruction, 501 locStr, 502 ) 503 default: 504 // Long instruction, print loc on a separate line. 505 fmt.Fprintf(w, "%74s;%s\n", "", locStr) 506 fmt.Fprintf(w, "%10s %10s %10x: %s\n", 507 valueOrDot(n.flatValue(), rpt), 508 valueOrDot(n.cumValue(), rpt), 509 n.address, n.instruction, 510 ) 511 } 512 } 513 } 514 return nil 515} 516 517// symbolsFromBinaries examines the binaries listed on the profile that have 518// associated samples, and returns the identified symbols matching rx. 519func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol { 520 // fileHasSamplesAndMatched is for optimization to speed up pprof: when later 521 // walking through the profile mappings, it will only examine the ones that have 522 // samples and are matched to the regexp. 523 fileHasSamplesAndMatched := make(map[string]bool) 524 for _, n := range g.Nodes { 525 if name := n.Info.PrintableName(); rx.MatchString(name) && n.Info.Objfile != "" { 526 fileHasSamplesAndMatched[n.Info.Objfile] = true 527 } 528 } 529 530 // Walk all mappings looking for matching functions with samples. 531 var objSyms []*objSymbol 532 for _, m := range prof.Mapping { 533 // Skip the mapping if its file does not have samples or is not matched to 534 // the regexp (unless the regexp is an address and the mapping's range covers 535 // the address) 536 if !fileHasSamplesAndMatched[m.File] { 537 if address == nil || !(m.Start <= *address && *address <= m.Limit) { 538 continue 539 } 540 } 541 542 f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol) 543 if err != nil { 544 fmt.Printf("%v\n", err) 545 continue 546 } 547 548 // Find symbols in this binary matching the user regexp. 549 var addr uint64 550 if address != nil { 551 addr = *address 552 } 553 msyms, err := f.Symbols(rx, addr) 554 f.Close() 555 if err != nil { 556 continue 557 } 558 for _, ms := range msyms { 559 objSyms = append(objSyms, 560 &objSymbol{ 561 sym: ms, 562 file: f, 563 }, 564 ) 565 } 566 } 567 568 return objSyms 569} 570 571// objSym represents a symbol identified from a binary. It includes 572// the SymbolInfo from the disasm package and the base that must be 573// added to correspond to sample addresses 574type objSymbol struct { 575 sym *plugin.Sym 576 file plugin.ObjFile 577} 578 579// orderSyms is a wrapper type to sort []*objSymbol by a supplied comparator. 580type orderSyms struct { 581 v []*objSymbol 582 less func(a, b *objSymbol) bool 583} 584 585func (o orderSyms) Len() int { return len(o.v) } 586func (o orderSyms) Less(i, j int) bool { return o.less(o.v[i], o.v[j]) } 587func (o orderSyms) Swap(i, j int) { o.v[i], o.v[j] = o.v[j], o.v[i] } 588 589// nodesPerSymbol classifies nodes into a group of symbols. 590func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.Nodes { 591 symNodes := make(map[*objSymbol]graph.Nodes) 592 for _, s := range symbols { 593 // Gather samples for this symbol. 594 for _, n := range ns { 595 if address, err := s.file.ObjAddr(n.Info.Address); err == nil && address >= s.sym.Start && address < s.sym.End { 596 symNodes[s] = append(symNodes[s], n) 597 } 598 } 599 } 600 return symNodes 601} 602 603type assemblyInstruction struct { 604 address uint64 605 instruction string 606 function string 607 file string 608 line int 609 flat, cum int64 610 flatDiv, cumDiv int64 611 startsBlock bool 612 inlineCalls []callID 613} 614 615type callID struct { 616 file string 617 line int 618} 619 620func (a *assemblyInstruction) flatValue() int64 { 621 if a.flatDiv != 0 { 622 return a.flat / a.flatDiv 623 } 624 return a.flat 625} 626 627func (a *assemblyInstruction) cumValue() int64 { 628 if a.cumDiv != 0 { 629 return a.cum / a.cumDiv 630 } 631 return a.cum 632} 633 634// annotateAssembly annotates a set of assembly instructions with a 635// set of samples. It returns a set of nodes to display. base is an 636// offset to adjust the sample addresses. 637func annotateAssembly(insts []plugin.Inst, samples graph.Nodes, file plugin.ObjFile) []assemblyInstruction { 638 // Add end marker to simplify printing loop. 639 insts = append(insts, plugin.Inst{ 640 Addr: ^uint64(0), 641 }) 642 643 // Ensure samples are sorted by address. 644 samples.Sort(graph.AddressOrder) 645 646 s := 0 647 asm := make([]assemblyInstruction, 0, len(insts)) 648 for ix, in := range insts[:len(insts)-1] { 649 n := assemblyInstruction{ 650 address: in.Addr, 651 instruction: in.Text, 652 function: in.Function, 653 line: in.Line, 654 } 655 if in.File != "" { 656 n.file = filepath.Base(in.File) 657 } 658 659 // Sum all the samples until the next instruction (to account 660 // for samples attributed to the middle of an instruction). 661 for next := insts[ix+1].Addr; s < len(samples); s++ { 662 if addr, err := file.ObjAddr(samples[s].Info.Address); err != nil || addr >= next { 663 break 664 } 665 sample := samples[s] 666 n.flatDiv += sample.FlatDiv 667 n.flat += sample.Flat 668 n.cumDiv += sample.CumDiv 669 n.cum += sample.Cum 670 if f := sample.Info.File; f != "" && n.file == "" { 671 n.file = filepath.Base(f) 672 } 673 if ln := sample.Info.Lineno; ln != 0 && n.line == 0 { 674 n.line = ln 675 } 676 if f := sample.Info.Name; f != "" && n.function == "" { 677 n.function = f 678 } 679 } 680 asm = append(asm, n) 681 } 682 683 return asm 684} 685 686// valueOrDot formats a value according to a report, intercepting zero 687// values. 688func valueOrDot(value int64, rpt *Report) string { 689 if value == 0 { 690 return "." 691 } 692 return rpt.formatValue(value) 693} 694 695// printTags collects all tags referenced in the profile and prints 696// them in a sorted table. 697func printTags(w io.Writer, rpt *Report) error { 698 p := rpt.prof 699 700 o := rpt.options 701 formatTag := func(v int64, key string) string { 702 return measurement.ScaledLabel(v, key, o.OutputUnit) 703 } 704 705 // Hashtable to keep accumulate tags as key,value,count. 706 tagMap := make(map[string]map[string]int64) 707 for _, s := range p.Sample { 708 for key, vals := range s.Label { 709 for _, val := range vals { 710 valueMap, ok := tagMap[key] 711 if !ok { 712 valueMap = make(map[string]int64) 713 tagMap[key] = valueMap 714 } 715 valueMap[val] += o.SampleValue(s.Value) 716 } 717 } 718 for key, vals := range s.NumLabel { 719 unit := o.NumLabelUnits[key] 720 for _, nval := range vals { 721 val := formatTag(nval, unit) 722 valueMap, ok := tagMap[key] 723 if !ok { 724 valueMap = make(map[string]int64) 725 tagMap[key] = valueMap 726 } 727 valueMap[val] += o.SampleValue(s.Value) 728 } 729 } 730 } 731 732 tagKeys := make([]*graph.Tag, 0, len(tagMap)) 733 for key := range tagMap { 734 tagKeys = append(tagKeys, &graph.Tag{Name: key}) 735 } 736 tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight) 737 for _, tagKey := range graph.SortTags(tagKeys, true) { 738 var total int64 739 key := tagKey.Name 740 tags := make([]*graph.Tag, 0, len(tagMap[key])) 741 for t, c := range tagMap[key] { 742 total += c 743 tags = append(tags, &graph.Tag{Name: t, Flat: c}) 744 } 745 746 f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit) 747 fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u) 748 for _, t := range graph.SortTags(tags, true) { 749 f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit) 750 if total > 0 { 751 fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name) 752 } else { 753 fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name) 754 } 755 } 756 fmt.Fprintln(tabw) 757 } 758 return tabw.Flush() 759} 760 761// printComments prints all freeform comments in the profile. 762func printComments(w io.Writer, rpt *Report) error { 763 p := rpt.prof 764 765 for _, c := range p.Comments { 766 fmt.Fprintln(w, c) 767 } 768 return nil 769} 770 771// TextItem holds a single text report entry. 772type TextItem struct { 773 Name string 774 InlineLabel string // Not empty if inlined 775 Flat, Cum int64 // Raw values 776 FlatFormat, CumFormat string // Formatted values 777} 778 779// TextItems returns a list of text items from the report and a list 780// of labels that describe the report. 781func TextItems(rpt *Report) ([]TextItem, []string) { 782 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 783 rpt.selectOutputUnit(g) 784 labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false) 785 786 var items []TextItem 787 var flatSum int64 788 for _, n := range g.Nodes { 789 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 790 791 var inline, noinline bool 792 for _, e := range n.In { 793 if e.Inline { 794 inline = true 795 } else { 796 noinline = true 797 } 798 } 799 800 var inl string 801 if inline { 802 if noinline { 803 inl = "(partial-inline)" 804 } else { 805 inl = "(inline)" 806 } 807 } 808 809 flatSum += flat 810 items = append(items, TextItem{ 811 Name: name, 812 InlineLabel: inl, 813 Flat: flat, 814 Cum: cum, 815 FlatFormat: rpt.formatValue(flat), 816 CumFormat: rpt.formatValue(cum), 817 }) 818 } 819 return items, labels 820} 821 822// printText prints a flat text report for a profile. 823func printText(w io.Writer, rpt *Report) error { 824 items, labels := TextItems(rpt) 825 fmt.Fprintln(w, strings.Join(labels, "\n")) 826 fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n", 827 "flat", "flat", "sum", "cum", "cum") 828 var flatSum int64 829 for _, item := range items { 830 inl := item.InlineLabel 831 if inl != "" { 832 inl = " " + inl 833 } 834 flatSum += item.Flat 835 fmt.Fprintf(w, "%10s %s %s %10s %s %s%s\n", 836 item.FlatFormat, measurement.Percentage(item.Flat, rpt.total), 837 measurement.Percentage(flatSum, rpt.total), 838 item.CumFormat, measurement.Percentage(item.Cum, rpt.total), 839 item.Name, inl) 840 } 841 return nil 842} 843 844// printTraces prints all traces from a profile. 845func printTraces(w io.Writer, rpt *Report) error { 846 fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n")) 847 848 prof := rpt.prof 849 o := rpt.options 850 851 const separator = "-----------+-------------------------------------------------------" 852 853 _, locations := graph.CreateNodes(prof, &graph.Options{}) 854 for _, sample := range prof.Sample { 855 type stk struct { 856 *graph.NodeInfo 857 inline bool 858 } 859 var stack []stk 860 for _, loc := range sample.Location { 861 nodes := locations[loc.ID] 862 for i, n := range nodes { 863 // The inline flag may be inaccurate if 'show' or 'hide' filter is 864 // used. See https://github.com/google/pprof/issues/511. 865 inline := i != len(nodes)-1 866 stack = append(stack, stk{&n.Info, inline}) 867 } 868 } 869 870 if len(stack) == 0 { 871 continue 872 } 873 874 fmt.Fprintln(w, separator) 875 // Print any text labels for the sample. 876 var labels []string 877 for s, vs := range sample.Label { 878 labels = append(labels, fmt.Sprintf("%10s: %s\n", s, strings.Join(vs, " "))) 879 } 880 sort.Strings(labels) 881 fmt.Fprint(w, strings.Join(labels, "")) 882 883 // Print any numeric labels for the sample 884 var numLabels []string 885 for key, vals := range sample.NumLabel { 886 unit := o.NumLabelUnits[key] 887 numValues := make([]string, len(vals)) 888 for i, vv := range vals { 889 numValues[i] = measurement.Label(vv, unit) 890 } 891 numLabels = append(numLabels, fmt.Sprintf("%10s: %s\n", key, strings.Join(numValues, " "))) 892 } 893 sort.Strings(numLabels) 894 fmt.Fprint(w, strings.Join(numLabels, "")) 895 896 var d, v int64 897 v = o.SampleValue(sample.Value) 898 if o.SampleMeanDivisor != nil { 899 d = o.SampleMeanDivisor(sample.Value) 900 } 901 // Print call stack. 902 if d != 0 { 903 v = v / d 904 } 905 for i, s := range stack { 906 var vs, inline string 907 if i == 0 { 908 vs = rpt.formatValue(v) 909 } 910 if s.inline { 911 inline = " (inline)" 912 } 913 fmt.Fprintf(w, "%10s %s%s\n", vs, s.PrintableName(), inline) 914 } 915 } 916 fmt.Fprintln(w, separator) 917 return nil 918} 919 920// printCallgrind prints a graph for a profile on callgrind format. 921func printCallgrind(w io.Writer, rpt *Report) error { 922 o := rpt.options 923 rpt.options.NodeFraction = 0 924 rpt.options.EdgeFraction = 0 925 rpt.options.NodeCount = 0 926 927 g, _, _, _ := rpt.newTrimmedGraph() 928 rpt.selectOutputUnit(g) 929 930 nodeNames := getDisambiguatedNames(g) 931 932 fmt.Fprintln(w, "positions: instr line") 933 fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")") 934 935 objfiles := make(map[string]int) 936 files := make(map[string]int) 937 names := make(map[string]int) 938 939 // prevInfo points to the previous NodeInfo. 940 // It is used to group cost lines together as much as possible. 941 var prevInfo *graph.NodeInfo 942 for _, n := range g.Nodes { 943 if prevInfo == nil || n.Info.Objfile != prevInfo.Objfile || n.Info.File != prevInfo.File || n.Info.Name != prevInfo.Name { 944 fmt.Fprintln(w) 945 fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.Info.Objfile)) 946 fmt.Fprintln(w, "fl="+callgrindName(files, n.Info.File)) 947 fmt.Fprintln(w, "fn="+callgrindName(names, n.Info.Name)) 948 } 949 950 addr := callgrindAddress(prevInfo, n.Info.Address) 951 sv, _ := measurement.Scale(n.FlatValue(), o.SampleUnit, o.OutputUnit) 952 fmt.Fprintf(w, "%s %d %d\n", addr, n.Info.Lineno, int64(sv)) 953 954 // Print outgoing edges. 955 for _, out := range n.Out.Sort() { 956 c, _ := measurement.Scale(out.Weight, o.SampleUnit, o.OutputUnit) 957 callee := out.Dest 958 fmt.Fprintln(w, "cfl="+callgrindName(files, callee.Info.File)) 959 fmt.Fprintln(w, "cfn="+callgrindName(names, nodeNames[callee])) 960 // pprof doesn't have a flat weight for a call, leave as 0. 961 fmt.Fprintf(w, "calls=0 %s %d\n", callgrindAddress(prevInfo, callee.Info.Address), callee.Info.Lineno) 962 // TODO: This address may be in the middle of a call 963 // instruction. It would be best to find the beginning 964 // of the instruction, but the tools seem to handle 965 // this OK. 966 fmt.Fprintf(w, "* * %d\n", int64(c)) 967 } 968 969 prevInfo = &n.Info 970 } 971 972 return nil 973} 974 975// getDisambiguatedNames returns a map from each node in the graph to 976// the name to use in the callgrind output. Callgrind merges all 977// functions with the same [file name, function name]. Add a [%d/n] 978// suffix to disambiguate nodes with different values of 979// node.Function, which we want to keep separate. In particular, this 980// affects graphs created with --call_tree, where nodes from different 981// contexts are associated to different Functions. 982func getDisambiguatedNames(g *graph.Graph) map[*graph.Node]string { 983 nodeName := make(map[*graph.Node]string, len(g.Nodes)) 984 985 type names struct { 986 file, function string 987 } 988 989 // nameFunctionIndex maps the callgrind names (filename, function) 990 // to the node.Function values found for that name, and each 991 // node.Function value to a sequential index to be used on the 992 // disambiguated name. 993 nameFunctionIndex := make(map[names]map[*graph.Node]int) 994 for _, n := range g.Nodes { 995 nm := names{n.Info.File, n.Info.Name} 996 p, ok := nameFunctionIndex[nm] 997 if !ok { 998 p = make(map[*graph.Node]int) 999 nameFunctionIndex[nm] = p 1000 } 1001 if _, ok := p[n.Function]; !ok { 1002 p[n.Function] = len(p) 1003 } 1004 } 1005 1006 for _, n := range g.Nodes { 1007 nm := names{n.Info.File, n.Info.Name} 1008 nodeName[n] = n.Info.Name 1009 if p := nameFunctionIndex[nm]; len(p) > 1 { 1010 // If there is more than one function, add suffix to disambiguate. 1011 nodeName[n] += fmt.Sprintf(" [%d/%d]", p[n.Function]+1, len(p)) 1012 } 1013 } 1014 return nodeName 1015} 1016 1017// callgrindName implements the callgrind naming compression scheme. 1018// For names not previously seen returns "(N) name", where N is a 1019// unique index. For names previously seen returns "(N)" where N is 1020// the index returned the first time. 1021func callgrindName(names map[string]int, name string) string { 1022 if name == "" { 1023 return "" 1024 } 1025 if id, ok := names[name]; ok { 1026 return fmt.Sprintf("(%d)", id) 1027 } 1028 id := len(names) + 1 1029 names[name] = id 1030 return fmt.Sprintf("(%d) %s", id, name) 1031} 1032 1033// callgrindAddress implements the callgrind subposition compression scheme if 1034// possible. If prevInfo != nil, it contains the previous address. The current 1035// address can be given relative to the previous address, with an explicit +/- 1036// to indicate it is relative, or * for the same address. 1037func callgrindAddress(prevInfo *graph.NodeInfo, curr uint64) string { 1038 abs := fmt.Sprintf("%#x", curr) 1039 if prevInfo == nil { 1040 return abs 1041 } 1042 1043 prev := prevInfo.Address 1044 if prev == curr { 1045 return "*" 1046 } 1047 1048 diff := int64(curr - prev) 1049 relative := fmt.Sprintf("%+d", diff) 1050 1051 // Only bother to use the relative address if it is actually shorter. 1052 if len(relative) < len(abs) { 1053 return relative 1054 } 1055 1056 return abs 1057} 1058 1059// printTree prints a tree-based report in text form. 1060func printTree(w io.Writer, rpt *Report) error { 1061 const separator = "----------------------------------------------------------+-------------" 1062 const legend = " flat flat% sum% cum cum% calls calls% + context " 1063 1064 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 1065 rpt.selectOutputUnit(g) 1066 1067 fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n")) 1068 1069 fmt.Fprintln(w, separator) 1070 fmt.Fprintln(w, legend) 1071 var flatSum int64 1072 1073 rx := rpt.options.Symbol 1074 matched := 0 1075 for _, n := range g.Nodes { 1076 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 1077 1078 // Skip any entries that do not match the regexp (for the "peek" command). 1079 if rx != nil && !rx.MatchString(name) { 1080 continue 1081 } 1082 matched++ 1083 1084 fmt.Fprintln(w, separator) 1085 // Print incoming edges. 1086 inEdges := n.In.Sort() 1087 for _, in := range inEdges { 1088 var inline string 1089 if in.Inline { 1090 inline = " (inline)" 1091 } 1092 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(in.Weight), 1093 measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline) 1094 } 1095 1096 // Print current node. 1097 flatSum += flat 1098 fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", 1099 rpt.formatValue(flat), 1100 measurement.Percentage(flat, rpt.total), 1101 measurement.Percentage(flatSum, rpt.total), 1102 rpt.formatValue(cum), 1103 measurement.Percentage(cum, rpt.total), 1104 name) 1105 1106 // Print outgoing edges. 1107 outEdges := n.Out.Sort() 1108 for _, out := range outEdges { 1109 var inline string 1110 if out.Inline { 1111 inline = " (inline)" 1112 } 1113 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(out.Weight), 1114 measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline) 1115 } 1116 } 1117 if len(g.Nodes) > 0 { 1118 fmt.Fprintln(w, separator) 1119 } 1120 if rx != nil && matched == 0 { 1121 return fmt.Errorf("no matches found for regexp: %s", rx) 1122 } 1123 return nil 1124} 1125 1126// GetDOT returns a graph suitable for dot processing along with some 1127// configuration information. 1128func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) { 1129 g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph() 1130 rpt.selectOutputUnit(g) 1131 labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true) 1132 1133 c := &graph.DotConfig{ 1134 Title: rpt.options.Title, 1135 Labels: labels, 1136 FormatValue: rpt.formatValue, 1137 Total: rpt.total, 1138 } 1139 return g, c 1140} 1141 1142// printDOT prints an annotated callgraph in DOT format. 1143func printDOT(w io.Writer, rpt *Report) error { 1144 g, c := GetDOT(rpt) 1145 graph.ComposeDot(w, g, &graph.DotAttributes{}, c) 1146 return nil 1147} 1148 1149// ProfileLabels returns printable labels for a profile. 1150func ProfileLabels(rpt *Report) []string { 1151 label := []string{} 1152 prof := rpt.prof 1153 o := rpt.options 1154 if len(prof.Mapping) > 0 { 1155 if prof.Mapping[0].File != "" { 1156 label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) 1157 } 1158 if prof.Mapping[0].BuildID != "" { 1159 label = append(label, "Build ID: "+prof.Mapping[0].BuildID) 1160 } 1161 } 1162 // Only include comments that do not start with '#'. 1163 for _, c := range prof.Comments { 1164 if !strings.HasPrefix(c, "#") { 1165 label = append(label, c) 1166 } 1167 } 1168 if o.SampleType != "" { 1169 label = append(label, "Type: "+o.SampleType) 1170 } 1171 if prof.TimeNanos != 0 { 1172 const layout = "Jan 2, 2006 at 3:04pm (MST)" 1173 label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) 1174 } 1175 if prof.DurationNanos != 0 { 1176 duration := measurement.Label(prof.DurationNanos, "nanoseconds") 1177 totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds") 1178 var ratio string 1179 if totalUnit == "ns" && totalNanos != 0 { 1180 ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")" 1181 } 1182 label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio)) 1183 } 1184 return label 1185} 1186 1187// reportLabels returns printable labels for a report. Includes 1188// profileLabels. 1189func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedEdges int, fullHeaders bool) []string { 1190 nodeFraction := rpt.options.NodeFraction 1191 edgeFraction := rpt.options.EdgeFraction 1192 nodeCount := len(g.Nodes) 1193 1194 var label []string 1195 if len(rpt.options.ProfileLabels) > 0 { 1196 label = append(label, rpt.options.ProfileLabels...) 1197 } else if fullHeaders || !rpt.options.CompactLabels { 1198 label = ProfileLabels(rpt) 1199 } 1200 1201 var flatSum int64 1202 for _, n := range g.Nodes { 1203 flatSum = flatSum + n.FlatValue() 1204 } 1205 1206 if len(rpt.options.ActiveFilters) > 0 { 1207 activeFilters := legendActiveFilters(rpt.options.ActiveFilters) 1208 label = append(label, activeFilters...) 1209 } 1210 1211 label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(measurement.Percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total))) 1212 1213 if rpt.total != 0 { 1214 if droppedNodes > 0 { 1215 label = append(label, genLabel(droppedNodes, "node", "cum", 1216 rpt.formatValue(abs64(int64(float64(rpt.total)*nodeFraction))))) 1217 } 1218 if droppedEdges > 0 { 1219 label = append(label, genLabel(droppedEdges, "edge", "freq", 1220 rpt.formatValue(abs64(int64(float64(rpt.total)*edgeFraction))))) 1221 } 1222 if nodeCount > 0 && nodeCount < origCount { 1223 label = append(label, fmt.Sprintf("Showing top %d nodes out of %d", 1224 nodeCount, origCount)) 1225 } 1226 } 1227 1228 // Help new users understand the graph. 1229 // A new line is intentionally added here to better show this message. 1230 if fullHeaders { 1231 label = append(label, "\nSee https://git.io/JfYMW for how to read the graph") 1232 } 1233 1234 return label 1235} 1236 1237func legendActiveFilters(activeFilters []string) []string { 1238 legendActiveFilters := make([]string, len(activeFilters)+1) 1239 legendActiveFilters[0] = "Active filters:" 1240 for i, s := range activeFilters { 1241 if len(s) > 80 { 1242 s = s[:80] + "…" 1243 } 1244 legendActiveFilters[i+1] = " " + s 1245 } 1246 return legendActiveFilters 1247} 1248 1249func genLabel(d int, n, l, f string) string { 1250 if d > 1 { 1251 n = n + "s" 1252 } 1253 return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) 1254} 1255 1256// New builds a new report indexing the sample values interpreting the 1257// samples with the provided function. 1258func New(prof *profile.Profile, o *Options) *Report { 1259 format := func(v int64) string { 1260 if r := o.Ratio; r > 0 && r != 1 { 1261 fv := float64(v) * r 1262 v = int64(fv) 1263 } 1264 return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit) 1265 } 1266 return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor), 1267 o, format} 1268} 1269 1270// NewDefault builds a new report indexing the last sample value 1271// available. 1272func NewDefault(prof *profile.Profile, options Options) *Report { 1273 index := len(prof.SampleType) - 1 1274 o := &options 1275 if o.Title == "" && len(prof.Mapping) > 0 && prof.Mapping[0].File != "" { 1276 o.Title = filepath.Base(prof.Mapping[0].File) 1277 } 1278 o.SampleType = prof.SampleType[index].Type 1279 o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) 1280 o.SampleValue = func(v []int64) int64 { 1281 return v[index] 1282 } 1283 return New(prof, o) 1284} 1285 1286// computeTotal computes the sum of the absolute value of all sample values. 1287// If any samples have label indicating they belong to the diff base, then the 1288// total will only include samples with that label. 1289func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 { 1290 var div, total, diffDiv, diffTotal int64 1291 for _, sample := range prof.Sample { 1292 var d, v int64 1293 v = value(sample.Value) 1294 if meanDiv != nil { 1295 d = meanDiv(sample.Value) 1296 } 1297 if v < 0 { 1298 v = -v 1299 } 1300 total += v 1301 div += d 1302 if sample.DiffBaseSample() { 1303 diffTotal += v 1304 diffDiv += d 1305 } 1306 } 1307 if diffTotal > 0 { 1308 total = diffTotal 1309 div = diffDiv 1310 } 1311 if div != 0 { 1312 return total / div 1313 } 1314 return total 1315} 1316 1317// Report contains the data and associated routines to extract a 1318// report from a profile. 1319type Report struct { 1320 prof *profile.Profile 1321 total int64 1322 options *Options 1323 formatValue func(int64) string 1324} 1325 1326// Total returns the total number of samples in a report. 1327func (rpt *Report) Total() int64 { return rpt.total } 1328 1329// OutputFormat returns the output format for the report. 1330func (rpt *Report) OutputFormat() int { return rpt.options.OutputFormat } 1331 1332func abs64(i int64) int64 { 1333 if i < 0 { 1334 return -i 1335 } 1336 return i 1337} 1338