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