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 if len(syms) == 0 { 436 return fmt.Errorf("no matches found for regexp: %s", o.Symbol) 437 } 438 439 // Correlate the symbols from the binary with the profile samples. 440 for _, s := range syms { 441 sns := symNodes[s] 442 443 // Gather samples for this symbol. 444 flatSum, cumSum := sns.Sum() 445 446 // Get the function assembly. 447 insts, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End, o.IntelSyntax) 448 if err != nil { 449 return err 450 } 451 452 ns := annotateAssembly(insts, sns, s.file) 453 454 fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0]) 455 for _, name := range s.sym.Name[1:] { 456 fmt.Fprintf(w, " AKA ======================== %s\n", name) 457 } 458 fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", 459 rpt.formatValue(flatSum), rpt.formatValue(cumSum), 460 measurement.Percentage(cumSum, rpt.total)) 461 462 function, file, line := "", "", 0 463 for _, n := range ns { 464 locStr := "" 465 // Skip loc information if it hasn't changed from previous instruction. 466 if n.function != function || n.file != file || n.line != line { 467 function, file, line = n.function, n.file, n.line 468 if n.function != "" { 469 locStr = n.function + " " 470 } 471 if n.file != "" { 472 locStr += n.file 473 if n.line != 0 { 474 locStr += fmt.Sprintf(":%d", n.line) 475 } 476 } 477 } 478 switch { 479 case locStr == "": 480 // No location info, just print the instruction. 481 fmt.Fprintf(w, "%10s %10s %10x: %s\n", 482 valueOrDot(n.flatValue(), rpt), 483 valueOrDot(n.cumValue(), rpt), 484 n.address, n.instruction, 485 ) 486 case len(n.instruction) < 40: 487 // Short instruction, print loc on the same line. 488 fmt.Fprintf(w, "%10s %10s %10x: %-40s;%s\n", 489 valueOrDot(n.flatValue(), rpt), 490 valueOrDot(n.cumValue(), rpt), 491 n.address, n.instruction, 492 locStr, 493 ) 494 default: 495 // Long instruction, print loc on a separate line. 496 fmt.Fprintf(w, "%74s;%s\n", "", locStr) 497 fmt.Fprintf(w, "%10s %10s %10x: %s\n", 498 valueOrDot(n.flatValue(), rpt), 499 valueOrDot(n.cumValue(), rpt), 500 n.address, n.instruction, 501 ) 502 } 503 } 504 } 505 return nil 506} 507 508// symbolsFromBinaries examines the binaries listed on the profile 509// that have associated samples, and identifies symbols matching rx. 510func symbolsFromBinaries(prof *profile.Profile, g *graph.Graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol { 511 hasSamples := make(map[string]bool) 512 // Only examine mappings that have samples that match the 513 // regexp. This is an optimization to speed up pprof. 514 for _, n := range g.Nodes { 515 if name := n.Info.PrintableName(); rx.MatchString(name) && n.Info.Objfile != "" { 516 hasSamples[n.Info.Objfile] = true 517 } 518 } 519 520 // Walk all mappings looking for matching functions with samples. 521 var objSyms []*objSymbol 522 for _, m := range prof.Mapping { 523 if !hasSamples[m.File] { 524 if address == nil || !(m.Start <= *address && *address <= m.Limit) { 525 continue 526 } 527 } 528 529 f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset) 530 if err != nil { 531 fmt.Printf("%v\n", err) 532 continue 533 } 534 535 // Find symbols in this binary matching the user regexp. 536 var addr uint64 537 if address != nil { 538 addr = *address 539 } 540 msyms, err := f.Symbols(rx, addr) 541 f.Close() 542 if err != nil { 543 continue 544 } 545 for _, ms := range msyms { 546 objSyms = append(objSyms, 547 &objSymbol{ 548 sym: ms, 549 file: f, 550 }, 551 ) 552 } 553 } 554 555 return objSyms 556} 557 558// objSym represents a symbol identified from a binary. It includes 559// the SymbolInfo from the disasm package and the base that must be 560// added to correspond to sample addresses 561type objSymbol struct { 562 sym *plugin.Sym 563 file plugin.ObjFile 564} 565 566// orderSyms is a wrapper type to sort []*objSymbol by a supplied comparator. 567type orderSyms struct { 568 v []*objSymbol 569 less func(a, b *objSymbol) bool 570} 571 572func (o orderSyms) Len() int { return len(o.v) } 573func (o orderSyms) Less(i, j int) bool { return o.less(o.v[i], o.v[j]) } 574func (o orderSyms) Swap(i, j int) { o.v[i], o.v[j] = o.v[j], o.v[i] } 575 576// nodesPerSymbol classifies nodes into a group of symbols. 577func nodesPerSymbol(ns graph.Nodes, symbols []*objSymbol) map[*objSymbol]graph.Nodes { 578 symNodes := make(map[*objSymbol]graph.Nodes) 579 for _, s := range symbols { 580 // Gather samples for this symbol. 581 for _, n := range ns { 582 if address, err := s.file.ObjAddr(n.Info.Address); err == nil && 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, file plugin.ObjFile) []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); s++ { 649 if addr, err := file.ObjAddr(samples[s].Info.Address); err != nil || addr >= next { 650 break 651 } 652 sample := samples[s] 653 n.flatDiv += sample.FlatDiv 654 n.flat += sample.Flat 655 n.cumDiv += sample.CumDiv 656 n.cum += sample.Cum 657 if f := sample.Info.File; f != "" && n.file == "" { 658 n.file = filepath.Base(f) 659 } 660 if ln := sample.Info.Lineno; ln != 0 && n.line == 0 { 661 n.line = ln 662 } 663 if f := sample.Info.Name; f != "" && n.function == "" { 664 n.function = f 665 } 666 } 667 asm = append(asm, n) 668 } 669 670 return asm 671} 672 673// valueOrDot formats a value according to a report, intercepting zero 674// values. 675func valueOrDot(value int64, rpt *Report) string { 676 if value == 0 { 677 return "." 678 } 679 return rpt.formatValue(value) 680} 681 682// printTags collects all tags referenced in the profile and prints 683// them in a sorted table. 684func printTags(w io.Writer, rpt *Report) error { 685 p := rpt.prof 686 687 o := rpt.options 688 formatTag := func(v int64, key string) string { 689 return measurement.ScaledLabel(v, key, o.OutputUnit) 690 } 691 692 // Hashtable to keep accumulate tags as key,value,count. 693 tagMap := make(map[string]map[string]int64) 694 for _, s := range p.Sample { 695 for key, vals := range s.Label { 696 for _, val := range vals { 697 valueMap, ok := tagMap[key] 698 if !ok { 699 valueMap = make(map[string]int64) 700 tagMap[key] = valueMap 701 } 702 valueMap[val] += o.SampleValue(s.Value) 703 } 704 } 705 for key, vals := range s.NumLabel { 706 unit := o.NumLabelUnits[key] 707 for _, nval := range vals { 708 val := formatTag(nval, unit) 709 valueMap, ok := tagMap[key] 710 if !ok { 711 valueMap = make(map[string]int64) 712 tagMap[key] = valueMap 713 } 714 valueMap[val] += o.SampleValue(s.Value) 715 } 716 } 717 } 718 719 tagKeys := make([]*graph.Tag, 0, len(tagMap)) 720 for key := range tagMap { 721 tagKeys = append(tagKeys, &graph.Tag{Name: key}) 722 } 723 tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight) 724 for _, tagKey := range graph.SortTags(tagKeys, true) { 725 var total int64 726 key := tagKey.Name 727 tags := make([]*graph.Tag, 0, len(tagMap[key])) 728 for t, c := range tagMap[key] { 729 total += c 730 tags = append(tags, &graph.Tag{Name: t, Flat: c}) 731 } 732 733 f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit) 734 fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u) 735 for _, t := range graph.SortTags(tags, true) { 736 f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit) 737 if total > 0 { 738 fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name) 739 } else { 740 fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name) 741 } 742 } 743 fmt.Fprintln(tabw) 744 } 745 return tabw.Flush() 746} 747 748// printComments prints all freeform comments in the profile. 749func printComments(w io.Writer, rpt *Report) error { 750 p := rpt.prof 751 752 for _, c := range p.Comments { 753 fmt.Fprintln(w, c) 754 } 755 return nil 756} 757 758// TextItem holds a single text report entry. 759type TextItem struct { 760 Name string 761 InlineLabel string // Not empty if inlined 762 Flat, Cum int64 // Raw values 763 FlatFormat, CumFormat string // Formatted values 764} 765 766// TextItems returns a list of text items from the report and a list 767// of labels that describe the report. 768func TextItems(rpt *Report) ([]TextItem, []string) { 769 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 770 rpt.selectOutputUnit(g) 771 labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false) 772 773 var items []TextItem 774 var flatSum int64 775 for _, n := range g.Nodes { 776 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 777 778 var inline, noinline bool 779 for _, e := range n.In { 780 if e.Inline { 781 inline = true 782 } else { 783 noinline = true 784 } 785 } 786 787 var inl string 788 if inline { 789 if noinline { 790 inl = "(partial-inline)" 791 } else { 792 inl = "(inline)" 793 } 794 } 795 796 flatSum += flat 797 items = append(items, TextItem{ 798 Name: name, 799 InlineLabel: inl, 800 Flat: flat, 801 Cum: cum, 802 FlatFormat: rpt.formatValue(flat), 803 CumFormat: rpt.formatValue(cum), 804 }) 805 } 806 return items, labels 807} 808 809// printText prints a flat text report for a profile. 810func printText(w io.Writer, rpt *Report) error { 811 items, labels := TextItems(rpt) 812 fmt.Fprintln(w, strings.Join(labels, "\n")) 813 fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n", 814 "flat", "flat", "sum", "cum", "cum") 815 var flatSum int64 816 for _, item := range items { 817 inl := item.InlineLabel 818 if inl != "" { 819 inl = " " + inl 820 } 821 flatSum += item.Flat 822 fmt.Fprintf(w, "%10s %s %s %10s %s %s%s\n", 823 item.FlatFormat, measurement.Percentage(item.Flat, rpt.total), 824 measurement.Percentage(flatSum, rpt.total), 825 item.CumFormat, measurement.Percentage(item.Cum, rpt.total), 826 item.Name, inl) 827 } 828 return nil 829} 830 831// printTraces prints all traces from a profile. 832func printTraces(w io.Writer, rpt *Report) error { 833 fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n")) 834 835 prof := rpt.prof 836 o := rpt.options 837 838 const separator = "-----------+-------------------------------------------------------" 839 840 _, locations := graph.CreateNodes(prof, &graph.Options{}) 841 for _, sample := range prof.Sample { 842 type stk struct { 843 *graph.NodeInfo 844 inline bool 845 } 846 var stack []stk 847 for _, loc := range sample.Location { 848 nodes := locations[loc.ID] 849 for i, n := range nodes { 850 // The inline flag may be inaccurate if 'show' or 'hide' filter is 851 // used. See https://github.com/google/pprof/issues/511. 852 inline := i != len(nodes)-1 853 stack = append(stack, stk{&n.Info, inline}) 854 } 855 } 856 857 if len(stack) == 0 { 858 continue 859 } 860 861 fmt.Fprintln(w, separator) 862 // Print any text labels for the sample. 863 var labels []string 864 for s, vs := range sample.Label { 865 labels = append(labels, fmt.Sprintf("%10s: %s\n", s, strings.Join(vs, " "))) 866 } 867 sort.Strings(labels) 868 fmt.Fprint(w, strings.Join(labels, "")) 869 870 // Print any numeric labels for the sample 871 var numLabels []string 872 for key, vals := range sample.NumLabel { 873 unit := o.NumLabelUnits[key] 874 numValues := make([]string, len(vals)) 875 for i, vv := range vals { 876 numValues[i] = measurement.Label(vv, unit) 877 } 878 numLabels = append(numLabels, fmt.Sprintf("%10s: %s\n", key, strings.Join(numValues, " "))) 879 } 880 sort.Strings(numLabels) 881 fmt.Fprint(w, strings.Join(numLabels, "")) 882 883 var d, v int64 884 v = o.SampleValue(sample.Value) 885 if o.SampleMeanDivisor != nil { 886 d = o.SampleMeanDivisor(sample.Value) 887 } 888 // Print call stack. 889 if d != 0 { 890 v = v / d 891 } 892 for i, s := range stack { 893 var vs, inline string 894 if i == 0 { 895 vs = rpt.formatValue(v) 896 } 897 if s.inline { 898 inline = " (inline)" 899 } 900 fmt.Fprintf(w, "%10s %s%s\n", vs, s.PrintableName(), inline) 901 } 902 } 903 fmt.Fprintln(w, separator) 904 return nil 905} 906 907// printCallgrind prints a graph for a profile on callgrind format. 908func printCallgrind(w io.Writer, rpt *Report) error { 909 o := rpt.options 910 rpt.options.NodeFraction = 0 911 rpt.options.EdgeFraction = 0 912 rpt.options.NodeCount = 0 913 914 g, _, _, _ := rpt.newTrimmedGraph() 915 rpt.selectOutputUnit(g) 916 917 nodeNames := getDisambiguatedNames(g) 918 919 fmt.Fprintln(w, "positions: instr line") 920 fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")") 921 922 objfiles := make(map[string]int) 923 files := make(map[string]int) 924 names := make(map[string]int) 925 926 // prevInfo points to the previous NodeInfo. 927 // It is used to group cost lines together as much as possible. 928 var prevInfo *graph.NodeInfo 929 for _, n := range g.Nodes { 930 if prevInfo == nil || n.Info.Objfile != prevInfo.Objfile || n.Info.File != prevInfo.File || n.Info.Name != prevInfo.Name { 931 fmt.Fprintln(w) 932 fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.Info.Objfile)) 933 fmt.Fprintln(w, "fl="+callgrindName(files, n.Info.File)) 934 fmt.Fprintln(w, "fn="+callgrindName(names, n.Info.Name)) 935 } 936 937 addr := callgrindAddress(prevInfo, n.Info.Address) 938 sv, _ := measurement.Scale(n.FlatValue(), o.SampleUnit, o.OutputUnit) 939 fmt.Fprintf(w, "%s %d %d\n", addr, n.Info.Lineno, int64(sv)) 940 941 // Print outgoing edges. 942 for _, out := range n.Out.Sort() { 943 c, _ := measurement.Scale(out.Weight, o.SampleUnit, o.OutputUnit) 944 callee := out.Dest 945 fmt.Fprintln(w, "cfl="+callgrindName(files, callee.Info.File)) 946 fmt.Fprintln(w, "cfn="+callgrindName(names, nodeNames[callee])) 947 // pprof doesn't have a flat weight for a call, leave as 0. 948 fmt.Fprintf(w, "calls=0 %s %d\n", callgrindAddress(prevInfo, callee.Info.Address), callee.Info.Lineno) 949 // TODO: This address may be in the middle of a call 950 // instruction. It would be best to find the beginning 951 // of the instruction, but the tools seem to handle 952 // this OK. 953 fmt.Fprintf(w, "* * %d\n", int64(c)) 954 } 955 956 prevInfo = &n.Info 957 } 958 959 return nil 960} 961 962// getDisambiguatedNames returns a map from each node in the graph to 963// the name to use in the callgrind output. Callgrind merges all 964// functions with the same [file name, function name]. Add a [%d/n] 965// suffix to disambiguate nodes with different values of 966// node.Function, which we want to keep separate. In particular, this 967// affects graphs created with --call_tree, where nodes from different 968// contexts are associated to different Functions. 969func getDisambiguatedNames(g *graph.Graph) map[*graph.Node]string { 970 nodeName := make(map[*graph.Node]string, len(g.Nodes)) 971 972 type names struct { 973 file, function string 974 } 975 976 // nameFunctionIndex maps the callgrind names (filename, function) 977 // to the node.Function values found for that name, and each 978 // node.Function value to a sequential index to be used on the 979 // disambiguated name. 980 nameFunctionIndex := make(map[names]map[*graph.Node]int) 981 for _, n := range g.Nodes { 982 nm := names{n.Info.File, n.Info.Name} 983 p, ok := nameFunctionIndex[nm] 984 if !ok { 985 p = make(map[*graph.Node]int) 986 nameFunctionIndex[nm] = p 987 } 988 if _, ok := p[n.Function]; !ok { 989 p[n.Function] = len(p) 990 } 991 } 992 993 for _, n := range g.Nodes { 994 nm := names{n.Info.File, n.Info.Name} 995 nodeName[n] = n.Info.Name 996 if p := nameFunctionIndex[nm]; len(p) > 1 { 997 // If there is more than one function, add suffix to disambiguate. 998 nodeName[n] += fmt.Sprintf(" [%d/%d]", p[n.Function]+1, len(p)) 999 } 1000 } 1001 return nodeName 1002} 1003 1004// callgrindName implements the callgrind naming compression scheme. 1005// For names not previously seen returns "(N) name", where N is a 1006// unique index. For names previously seen returns "(N)" where N is 1007// the index returned the first time. 1008func callgrindName(names map[string]int, name string) string { 1009 if name == "" { 1010 return "" 1011 } 1012 if id, ok := names[name]; ok { 1013 return fmt.Sprintf("(%d)", id) 1014 } 1015 id := len(names) + 1 1016 names[name] = id 1017 return fmt.Sprintf("(%d) %s", id, name) 1018} 1019 1020// callgrindAddress implements the callgrind subposition compression scheme if 1021// possible. If prevInfo != nil, it contains the previous address. The current 1022// address can be given relative to the previous address, with an explicit +/- 1023// to indicate it is relative, or * for the same address. 1024func callgrindAddress(prevInfo *graph.NodeInfo, curr uint64) string { 1025 abs := fmt.Sprintf("%#x", curr) 1026 if prevInfo == nil { 1027 return abs 1028 } 1029 1030 prev := prevInfo.Address 1031 if prev == curr { 1032 return "*" 1033 } 1034 1035 diff := int64(curr - prev) 1036 relative := fmt.Sprintf("%+d", diff) 1037 1038 // Only bother to use the relative address if it is actually shorter. 1039 if len(relative) < len(abs) { 1040 return relative 1041 } 1042 1043 return abs 1044} 1045 1046// printTree prints a tree-based report in text form. 1047func printTree(w io.Writer, rpt *Report) error { 1048 const separator = "----------------------------------------------------------+-------------" 1049 const legend = " flat flat% sum% cum cum% calls calls% + context " 1050 1051 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 1052 rpt.selectOutputUnit(g) 1053 1054 fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n")) 1055 1056 fmt.Fprintln(w, separator) 1057 fmt.Fprintln(w, legend) 1058 var flatSum int64 1059 1060 rx := rpt.options.Symbol 1061 matched := 0 1062 for _, n := range g.Nodes { 1063 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 1064 1065 // Skip any entries that do not match the regexp (for the "peek" command). 1066 if rx != nil && !rx.MatchString(name) { 1067 continue 1068 } 1069 matched++ 1070 1071 fmt.Fprintln(w, separator) 1072 // Print incoming edges. 1073 inEdges := n.In.Sort() 1074 for _, in := range inEdges { 1075 var inline string 1076 if in.Inline { 1077 inline = " (inline)" 1078 } 1079 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(in.Weight), 1080 measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline) 1081 } 1082 1083 // Print current node. 1084 flatSum += flat 1085 fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", 1086 rpt.formatValue(flat), 1087 measurement.Percentage(flat, rpt.total), 1088 measurement.Percentage(flatSum, rpt.total), 1089 rpt.formatValue(cum), 1090 measurement.Percentage(cum, rpt.total), 1091 name) 1092 1093 // Print outgoing edges. 1094 outEdges := n.Out.Sort() 1095 for _, out := range outEdges { 1096 var inline string 1097 if out.Inline { 1098 inline = " (inline)" 1099 } 1100 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(out.Weight), 1101 measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline) 1102 } 1103 } 1104 if len(g.Nodes) > 0 { 1105 fmt.Fprintln(w, separator) 1106 } 1107 if rx != nil && matched == 0 { 1108 return fmt.Errorf("no matches found for regexp: %s", rx) 1109 } 1110 return nil 1111} 1112 1113// GetDOT returns a graph suitable for dot processing along with some 1114// configuration information. 1115func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) { 1116 g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph() 1117 rpt.selectOutputUnit(g) 1118 labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true) 1119 1120 c := &graph.DotConfig{ 1121 Title: rpt.options.Title, 1122 Labels: labels, 1123 FormatValue: rpt.formatValue, 1124 Total: rpt.total, 1125 } 1126 return g, c 1127} 1128 1129// printDOT prints an annotated callgraph in DOT format. 1130func printDOT(w io.Writer, rpt *Report) error { 1131 g, c := GetDOT(rpt) 1132 graph.ComposeDot(w, g, &graph.DotAttributes{}, c) 1133 return nil 1134} 1135 1136// ProfileLabels returns printable labels for a profile. 1137func ProfileLabels(rpt *Report) []string { 1138 label := []string{} 1139 prof := rpt.prof 1140 o := rpt.options 1141 if len(prof.Mapping) > 0 { 1142 if prof.Mapping[0].File != "" { 1143 label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) 1144 } 1145 if prof.Mapping[0].BuildID != "" { 1146 label = append(label, "Build ID: "+prof.Mapping[0].BuildID) 1147 } 1148 } 1149 // Only include comments that do not start with '#'. 1150 for _, c := range prof.Comments { 1151 if !strings.HasPrefix(c, "#") { 1152 label = append(label, c) 1153 } 1154 } 1155 if o.SampleType != "" { 1156 label = append(label, "Type: "+o.SampleType) 1157 } 1158 if prof.TimeNanos != 0 { 1159 const layout = "Jan 2, 2006 at 3:04pm (MST)" 1160 label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) 1161 } 1162 if prof.DurationNanos != 0 { 1163 duration := measurement.Label(prof.DurationNanos, "nanoseconds") 1164 totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds") 1165 var ratio string 1166 if totalUnit == "ns" && totalNanos != 0 { 1167 ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")" 1168 } 1169 label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio)) 1170 } 1171 return label 1172} 1173 1174// reportLabels returns printable labels for a report. Includes 1175// profileLabels. 1176func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedEdges int, fullHeaders bool) []string { 1177 nodeFraction := rpt.options.NodeFraction 1178 edgeFraction := rpt.options.EdgeFraction 1179 nodeCount := len(g.Nodes) 1180 1181 var label []string 1182 if len(rpt.options.ProfileLabels) > 0 { 1183 label = append(label, rpt.options.ProfileLabels...) 1184 } else if fullHeaders || !rpt.options.CompactLabels { 1185 label = ProfileLabels(rpt) 1186 } 1187 1188 var flatSum int64 1189 for _, n := range g.Nodes { 1190 flatSum = flatSum + n.FlatValue() 1191 } 1192 1193 if len(rpt.options.ActiveFilters) > 0 { 1194 activeFilters := legendActiveFilters(rpt.options.ActiveFilters) 1195 label = append(label, activeFilters...) 1196 } 1197 1198 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))) 1199 1200 if rpt.total != 0 { 1201 if droppedNodes > 0 { 1202 label = append(label, genLabel(droppedNodes, "node", "cum", 1203 rpt.formatValue(abs64(int64(float64(rpt.total)*nodeFraction))))) 1204 } 1205 if droppedEdges > 0 { 1206 label = append(label, genLabel(droppedEdges, "edge", "freq", 1207 rpt.formatValue(abs64(int64(float64(rpt.total)*edgeFraction))))) 1208 } 1209 if nodeCount > 0 && nodeCount < origCount { 1210 label = append(label, fmt.Sprintf("Showing top %d nodes out of %d", 1211 nodeCount, origCount)) 1212 } 1213 } 1214 1215 // Help new users understand the graph. 1216 // A new line is intentionally added here to better show this message. 1217 if fullHeaders { 1218 label = append(label, "\nSee https://git.io/JfYMW for how to read the graph") 1219 } 1220 1221 return label 1222} 1223 1224func legendActiveFilters(activeFilters []string) []string { 1225 legendActiveFilters := make([]string, len(activeFilters)+1) 1226 legendActiveFilters[0] = "Active filters:" 1227 for i, s := range activeFilters { 1228 if len(s) > 80 { 1229 s = s[:80] + "…" 1230 } 1231 legendActiveFilters[i+1] = " " + s 1232 } 1233 return legendActiveFilters 1234} 1235 1236func genLabel(d int, n, l, f string) string { 1237 if d > 1 { 1238 n = n + "s" 1239 } 1240 return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) 1241} 1242 1243// New builds a new report indexing the sample values interpreting the 1244// samples with the provided function. 1245func New(prof *profile.Profile, o *Options) *Report { 1246 format := func(v int64) string { 1247 if r := o.Ratio; r > 0 && r != 1 { 1248 fv := float64(v) * r 1249 v = int64(fv) 1250 } 1251 return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit) 1252 } 1253 return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor), 1254 o, format} 1255} 1256 1257// NewDefault builds a new report indexing the last sample value 1258// available. 1259func NewDefault(prof *profile.Profile, options Options) *Report { 1260 index := len(prof.SampleType) - 1 1261 o := &options 1262 if o.Title == "" && len(prof.Mapping) > 0 && prof.Mapping[0].File != "" { 1263 o.Title = filepath.Base(prof.Mapping[0].File) 1264 } 1265 o.SampleType = prof.SampleType[index].Type 1266 o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) 1267 o.SampleValue = func(v []int64) int64 { 1268 return v[index] 1269 } 1270 return New(prof, o) 1271} 1272 1273// computeTotal computes the sum of the absolute value of all sample values. 1274// If any samples have label indicating they belong to the diff base, then the 1275// total will only include samples with that label. 1276func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 { 1277 var div, total, diffDiv, diffTotal int64 1278 for _, sample := range prof.Sample { 1279 var d, v int64 1280 v = value(sample.Value) 1281 if meanDiv != nil { 1282 d = meanDiv(sample.Value) 1283 } 1284 if v < 0 { 1285 v = -v 1286 } 1287 total += v 1288 div += d 1289 if sample.DiffBaseSample() { 1290 diffTotal += v 1291 diffDiv += d 1292 } 1293 } 1294 if diffTotal > 0 { 1295 total = diffTotal 1296 div = diffDiv 1297 } 1298 if div != 0 { 1299 return total / div 1300 } 1301 return total 1302} 1303 1304// Report contains the data and associated routines to extract a 1305// report from a profile. 1306type Report struct { 1307 prof *profile.Profile 1308 total int64 1309 options *Options 1310 formatValue func(int64) string 1311} 1312 1313// Total returns the total number of samples in a report. 1314func (rpt *Report) Total() int64 { return rpt.total } 1315 1316func abs64(i int64) int64 { 1317 if i < 0 { 1318 return -i 1319 } 1320 return i 1321} 1322