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