1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package report summarizes a performance profile into a 6 // human-readable report. 7 package report 8 9 import ( 10 "fmt" 11 "io" 12 "math" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 "time" 19 20 "cmd/pprof/internal/plugin" 21 "internal/pprof/profile" 22 ) 23 24 // Generate generates a report as directed by the Report. 25 func Generate(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 26 o := rpt.options 27 28 switch o.OutputFormat { 29 case Dot: 30 return printDOT(w, rpt) 31 case Tree: 32 return printTree(w, rpt) 33 case Text: 34 return printText(w, rpt) 35 case Raw: 36 fmt.Fprint(w, rpt.prof.String()) 37 return nil 38 case Tags: 39 return printTags(w, rpt) 40 case Proto: 41 return rpt.prof.Write(w) 42 case Dis: 43 return printAssembly(w, rpt, obj) 44 case List: 45 return printSource(w, rpt) 46 case WebList: 47 return printWebSource(w, rpt, obj) 48 case Callgrind: 49 return printCallgrind(w, rpt) 50 } 51 return fmt.Errorf("unexpected output format") 52 } 53 54 // printAssembly prints an annotated assembly listing. 55 func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 56 g, err := newGraph(rpt) 57 if err != nil { 58 return err 59 } 60 61 o := rpt.options 62 prof := rpt.prof 63 64 // If the regexp source can be parsed as an address, also match 65 // functions that land on that address. 66 var address *uint64 67 if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { 68 address = &hex 69 } 70 71 fmt.Fprintln(w, "Total:", rpt.formatValue(rpt.total)) 72 symbols := symbolsFromBinaries(prof, g, o.Symbol, address, obj) 73 symNodes := nodesPerSymbol(g.ns, symbols) 74 // Sort function names for printing. 75 var syms objSymbols 76 for s := range symNodes { 77 syms = append(syms, s) 78 } 79 sort.Sort(syms) 80 81 // Correlate the symbols from the binary with the profile samples. 82 for _, s := range syms { 83 sns := symNodes[s] 84 85 // Gather samples for this symbol. 86 flatSum, cumSum := sumNodes(sns) 87 88 // Get the function assembly. 89 insns, err := obj.Disasm(s.sym.File, s.sym.Start, s.sym.End) 90 if err != nil { 91 return err 92 } 93 94 ns := annotateAssembly(insns, sns, s.base) 95 96 fmt.Fprintf(w, "ROUTINE ======================== %s\n", s.sym.Name[0]) 97 for _, name := range s.sym.Name[1:] { 98 fmt.Fprintf(w, " AKA ======================== %s\n", name) 99 } 100 fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", 101 rpt.formatValue(flatSum), rpt.formatValue(cumSum), 102 percentage(cumSum, rpt.total)) 103 104 for _, n := range ns { 105 fmt.Fprintf(w, "%10s %10s %10x: %s\n", valueOrDot(n.flat, rpt), valueOrDot(n.cum, rpt), n.info.address, n.info.name) 106 } 107 } 108 return nil 109 } 110 111 // symbolsFromBinaries examines the binaries listed on the profile 112 // that have associated samples, and identifies symbols matching rx. 113 func symbolsFromBinaries(prof *profile.Profile, g graph, rx *regexp.Regexp, address *uint64, obj plugin.ObjTool) []*objSymbol { 114 hasSamples := make(map[string]bool) 115 // Only examine mappings that have samples that match the 116 // regexp. This is an optimization to speed up pprof. 117 for _, n := range g.ns { 118 if name := n.info.prettyName(); rx.MatchString(name) && n.info.objfile != "" { 119 hasSamples[n.info.objfile] = true 120 } 121 } 122 123 // Walk all mappings looking for matching functions with samples. 124 var objSyms []*objSymbol 125 for _, m := range prof.Mapping { 126 if !hasSamples[m.File] { 127 if address == nil || !(m.Start <= *address && *address <= m.Limit) { 128 continue 129 } 130 } 131 132 f, err := obj.Open(m.File, m.Start) 133 if err != nil { 134 fmt.Printf("%v\n", err) 135 continue 136 } 137 138 // Find symbols in this binary matching the user regexp. 139 var addr uint64 140 if address != nil { 141 addr = *address 142 } 143 msyms, err := f.Symbols(rx, addr) 144 base := f.Base() 145 f.Close() 146 if err != nil { 147 continue 148 } 149 for _, ms := range msyms { 150 objSyms = append(objSyms, 151 &objSymbol{ 152 sym: ms, 153 base: base, 154 }, 155 ) 156 } 157 } 158 159 return objSyms 160 } 161 162 // objSym represents a symbol identified from a binary. It includes 163 // the SymbolInfo from the disasm package and the base that must be 164 // added to correspond to sample addresses 165 type objSymbol struct { 166 sym *plugin.Sym 167 base uint64 168 } 169 170 // objSymbols is a wrapper type to enable sorting of []*objSymbol. 171 type objSymbols []*objSymbol 172 173 func (o objSymbols) Len() int { 174 return len(o) 175 } 176 177 func (o objSymbols) Less(i, j int) bool { 178 if namei, namej := o[i].sym.Name[0], o[j].sym.Name[0]; namei != namej { 179 return namei < namej 180 } 181 return o[i].sym.Start < o[j].sym.Start 182 } 183 184 func (o objSymbols) Swap(i, j int) { 185 o[i], o[j] = o[j], o[i] 186 } 187 188 // nodesPerSymbol classifies nodes into a group of symbols. 189 func nodesPerSymbol(ns nodes, symbols []*objSymbol) map[*objSymbol]nodes { 190 symNodes := make(map[*objSymbol]nodes) 191 for _, s := range symbols { 192 // Gather samples for this symbol. 193 for _, n := range ns { 194 address := n.info.address - s.base 195 if address >= s.sym.Start && address < s.sym.End { 196 symNodes[s] = append(symNodes[s], n) 197 } 198 } 199 } 200 return symNodes 201 } 202 203 // annotateAssembly annotates a set of assembly instructions with a 204 // set of samples. It returns a set of nodes to display. base is an 205 // offset to adjust the sample addresses. 206 func annotateAssembly(insns []plugin.Inst, samples nodes, base uint64) nodes { 207 // Add end marker to simplify printing loop. 208 insns = append(insns, plugin.Inst{ 209 Addr: ^uint64(0), 210 }) 211 212 // Ensure samples are sorted by address. 213 samples.sort(addressOrder) 214 215 var s int 216 var asm nodes 217 for ix, in := range insns[:len(insns)-1] { 218 n := node{ 219 info: nodeInfo{ 220 address: in.Addr, 221 name: in.Text, 222 file: trimPath(in.File), 223 lineno: in.Line, 224 }, 225 } 226 227 // Sum all the samples until the next instruction (to account 228 // for samples attributed to the middle of an instruction). 229 for next := insns[ix+1].Addr; s < len(samples) && samples[s].info.address-base < next; s++ { 230 n.flat += samples[s].flat 231 n.cum += samples[s].cum 232 if samples[s].info.file != "" { 233 n.info.file = trimPath(samples[s].info.file) 234 n.info.lineno = samples[s].info.lineno 235 } 236 } 237 asm = append(asm, &n) 238 } 239 240 return asm 241 } 242 243 // valueOrDot formats a value according to a report, intercepting zero 244 // values. 245 func valueOrDot(value int64, rpt *Report) string { 246 if value == 0 { 247 return "." 248 } 249 return rpt.formatValue(value) 250 } 251 252 // printTags collects all tags referenced in the profile and prints 253 // them in a sorted table. 254 func printTags(w io.Writer, rpt *Report) error { 255 p := rpt.prof 256 257 // Hashtable to keep accumulate tags as key,value,count. 258 tagMap := make(map[string]map[string]int64) 259 for _, s := range p.Sample { 260 for key, vals := range s.Label { 261 for _, val := range vals { 262 if valueMap, ok := tagMap[key]; ok { 263 valueMap[val] = valueMap[val] + s.Value[0] 264 continue 265 } 266 valueMap := make(map[string]int64) 267 valueMap[val] = s.Value[0] 268 tagMap[key] = valueMap 269 } 270 } 271 for key, vals := range s.NumLabel { 272 for _, nval := range vals { 273 val := scaledValueLabel(nval, key, "auto") 274 if valueMap, ok := tagMap[key]; ok { 275 valueMap[val] = valueMap[val] + s.Value[0] 276 continue 277 } 278 valueMap := make(map[string]int64) 279 valueMap[val] = s.Value[0] 280 tagMap[key] = valueMap 281 } 282 } 283 } 284 285 tagKeys := make(tags, 0, len(tagMap)) 286 for key := range tagMap { 287 tagKeys = append(tagKeys, &tag{name: key}) 288 } 289 sort.Sort(tagKeys) 290 291 for _, tagKey := range tagKeys { 292 var total int64 293 key := tagKey.name 294 tags := make(tags, 0, len(tagMap[key])) 295 for t, c := range tagMap[key] { 296 total += c 297 tags = append(tags, &tag{name: t, weight: c}) 298 } 299 300 sort.Sort(tags) 301 fmt.Fprintf(w, "%s: Total %d\n", key, total) 302 for _, t := range tags { 303 if total > 0 { 304 fmt.Fprintf(w, " %8d (%s): %s\n", t.weight, 305 percentage(t.weight, total), t.name) 306 } else { 307 fmt.Fprintf(w, " %8d: %s\n", t.weight, t.name) 308 } 309 } 310 fmt.Fprintln(w) 311 } 312 return nil 313 } 314 315 // printText prints a flat text report for a profile. 316 func printText(w io.Writer, rpt *Report) error { 317 g, err := newGraph(rpt) 318 if err != nil { 319 return err 320 } 321 322 origCount, droppedNodes, _ := g.preprocess(rpt) 323 fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n")) 324 325 fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n", 326 "flat", "flat", "sum", "cum", "cum") 327 328 var flatSum int64 329 for _, n := range g.ns { 330 name, flat, cum := n.info.prettyName(), n.flat, n.cum 331 332 flatSum += flat 333 fmt.Fprintf(w, "%10s %s %s %10s %s %s\n", 334 rpt.formatValue(flat), 335 percentage(flat, rpt.total), 336 percentage(flatSum, rpt.total), 337 rpt.formatValue(cum), 338 percentage(cum, rpt.total), 339 name) 340 } 341 return nil 342 } 343 344 // printCallgrind prints a graph for a profile on callgrind format. 345 func printCallgrind(w io.Writer, rpt *Report) error { 346 g, err := newGraph(rpt) 347 if err != nil { 348 return err 349 } 350 351 o := rpt.options 352 rpt.options.NodeFraction = 0 353 rpt.options.EdgeFraction = 0 354 rpt.options.NodeCount = 0 355 356 g.preprocess(rpt) 357 358 fmt.Fprintln(w, "positions: instr line") 359 fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")") 360 361 objfiles := make(map[string]int) 362 files := make(map[string]int) 363 names := make(map[string]int) 364 365 // prevInfo points to the previous nodeInfo. 366 // It is used to group cost lines together as much as possible. 367 var prevInfo *nodeInfo 368 for _, n := range g.ns { 369 if prevInfo == nil || n.info.objfile != prevInfo.objfile || n.info.file != prevInfo.file || n.info.name != prevInfo.name { 370 fmt.Fprintln(w) 371 fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.info.objfile)) 372 fmt.Fprintln(w, "fl="+callgrindName(files, n.info.file)) 373 fmt.Fprintln(w, "fn="+callgrindName(names, n.info.name)) 374 } 375 376 addr := callgrindAddress(prevInfo, n.info.address) 377 sv, _ := ScaleValue(n.flat, o.SampleUnit, o.OutputUnit) 378 fmt.Fprintf(w, "%s %d %d\n", addr, n.info.lineno, int(sv)) 379 380 // Print outgoing edges. 381 for _, out := range sortedEdges(n.out) { 382 c, _ := ScaleValue(out.weight, o.SampleUnit, o.OutputUnit) 383 callee := out.dest 384 fmt.Fprintln(w, "cfl="+callgrindName(files, callee.info.file)) 385 fmt.Fprintln(w, "cfn="+callgrindName(names, callee.info.name)) 386 fmt.Fprintf(w, "calls=%d %s %d\n", int(c), callgrindAddress(prevInfo, callee.info.address), callee.info.lineno) 387 // TODO: This address may be in the middle of a call 388 // instruction. It would be best to find the beginning 389 // of the instruction, but the tools seem to handle 390 // this OK. 391 fmt.Fprintf(w, "* * %d\n", int(c)) 392 } 393 394 prevInfo = &n.info 395 } 396 397 return nil 398 } 399 400 // callgrindName implements the callgrind naming compression scheme. 401 // For names not previously seen returns "(N) name", where N is a 402 // unique index. For names previously seen returns "(N)" where N is 403 // the index returned the first time. 404 func callgrindName(names map[string]int, name string) string { 405 if name == "" { 406 return "" 407 } 408 if id, ok := names[name]; ok { 409 return fmt.Sprintf("(%d)", id) 410 } 411 id := len(names) + 1 412 names[name] = id 413 return fmt.Sprintf("(%d) %s", id, name) 414 } 415 416 // callgrindAddress implements the callgrind subposition compression scheme if 417 // possible. If prevInfo != nil, it contains the previous address. The current 418 // address can be given relative to the previous address, with an explicit +/- 419 // to indicate it is relative, or * for the same address. 420 func callgrindAddress(prevInfo *nodeInfo, curr uint64) string { 421 abs := fmt.Sprintf("%#x", curr) 422 if prevInfo == nil { 423 return abs 424 } 425 426 prev := prevInfo.address 427 if prev == curr { 428 return "*" 429 } 430 431 diff := int64(curr - prev) 432 relative := fmt.Sprintf("%+d", diff) 433 434 // Only bother to use the relative address if it is actually shorter. 435 if len(relative) < len(abs) { 436 return relative 437 } 438 439 return abs 440 } 441 442 // printTree prints a tree-based report in text form. 443 func printTree(w io.Writer, rpt *Report) error { 444 const separator = "----------------------------------------------------------+-------------" 445 const legend = " flat flat% sum% cum cum% calls calls% + context " 446 447 g, err := newGraph(rpt) 448 if err != nil { 449 return err 450 } 451 452 origCount, droppedNodes, _ := g.preprocess(rpt) 453 fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n")) 454 455 fmt.Fprintln(w, separator) 456 fmt.Fprintln(w, legend) 457 var flatSum int64 458 459 rx := rpt.options.Symbol 460 for _, n := range g.ns { 461 name, flat, cum := n.info.prettyName(), n.flat, n.cum 462 463 // Skip any entries that do not match the regexp (for the "peek" command). 464 if rx != nil && !rx.MatchString(name) { 465 continue 466 } 467 468 fmt.Fprintln(w, separator) 469 // Print incoming edges. 470 inEdges := sortedEdges(n.in) 471 inSum := inEdges.sum() 472 for _, in := range inEdges { 473 fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(in.weight), 474 percentage(in.weight, inSum), in.src.info.prettyName()) 475 } 476 477 // Print current node. 478 flatSum += flat 479 fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", 480 rpt.formatValue(flat), 481 percentage(flat, rpt.total), 482 percentage(flatSum, rpt.total), 483 rpt.formatValue(cum), 484 percentage(cum, rpt.total), 485 name) 486 487 // Print outgoing edges. 488 outEdges := sortedEdges(n.out) 489 outSum := outEdges.sum() 490 for _, out := range outEdges { 491 fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(out.weight), 492 percentage(out.weight, outSum), out.dest.info.prettyName()) 493 } 494 } 495 if len(g.ns) > 0 { 496 fmt.Fprintln(w, separator) 497 } 498 return nil 499 } 500 501 // printDOT prints an annotated callgraph in DOT format. 502 func printDOT(w io.Writer, rpt *Report) error { 503 g, err := newGraph(rpt) 504 if err != nil { 505 return err 506 } 507 508 origCount, droppedNodes, droppedEdges := g.preprocess(rpt) 509 510 prof := rpt.prof 511 graphname := "unnamed" 512 if len(prof.Mapping) > 0 { 513 graphname = filepath.Base(prof.Mapping[0].File) 514 } 515 fmt.Fprintln(w, `digraph "`+graphname+`" {`) 516 fmt.Fprintln(w, `node [style=filled fillcolor="#f8f8f8"]`) 517 fmt.Fprintln(w, dotLegend(rpt, g, origCount, droppedNodes, droppedEdges)) 518 519 if len(g.ns) == 0 { 520 fmt.Fprintln(w, "}") 521 return nil 522 } 523 524 // Make sure nodes have a unique consistent id. 525 nodeIndex := make(map[*node]int) 526 maxFlat := float64(g.ns[0].flat) 527 for i, n := range g.ns { 528 nodeIndex[n] = i + 1 529 if float64(n.flat) > maxFlat { 530 maxFlat = float64(n.flat) 531 } 532 } 533 var edges edgeList 534 for _, n := range g.ns { 535 node := dotNode(rpt, maxFlat, nodeIndex[n], n) 536 fmt.Fprintln(w, node) 537 if nodelets := dotNodelets(rpt, nodeIndex[n], n); nodelets != "" { 538 fmt.Fprint(w, nodelets) 539 } 540 541 // Collect outgoing edges. 542 for _, e := range n.out { 543 edges = append(edges, e) 544 } 545 } 546 // Sort edges by frequency as a hint to the graph layout engine. 547 sort.Sort(edges) 548 for _, e := range edges { 549 fmt.Fprintln(w, dotEdge(rpt, nodeIndex[e.src], nodeIndex[e.dest], e)) 550 } 551 fmt.Fprintln(w, "}") 552 return nil 553 } 554 555 // percentage computes the percentage of total of a value, and encodes 556 // it as a string. At least two digits of precision are printed. 557 func percentage(value, total int64) string { 558 var ratio float64 559 if total != 0 { 560 ratio = float64(value) / float64(total) * 100 561 } 562 switch { 563 case ratio >= 99.95: 564 return " 100%" 565 case ratio >= 1.0: 566 return fmt.Sprintf("%5.2f%%", ratio) 567 default: 568 return fmt.Sprintf("%5.2g%%", ratio) 569 } 570 } 571 572 // dotLegend generates the overall graph label for a report in DOT format. 573 func dotLegend(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) string { 574 label := legendLabels(rpt) 575 label = append(label, legendDetailLabels(rpt, g, origCount, droppedNodes, droppedEdges)...) 576 return fmt.Sprintf(`subgraph cluster_L { L [shape=box fontsize=32 label="%s\l"] }`, strings.Join(label, `\l`)) 577 } 578 579 // legendLabels generates labels exclusive to graph visualization. 580 func legendLabels(rpt *Report) []string { 581 prof := rpt.prof 582 o := rpt.options 583 var label []string 584 if len(prof.Mapping) > 0 { 585 if prof.Mapping[0].File != "" { 586 label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) 587 } 588 if prof.Mapping[0].BuildID != "" { 589 label = append(label, "Build ID: "+prof.Mapping[0].BuildID) 590 } 591 } 592 if o.SampleType != "" { 593 label = append(label, "Type: "+o.SampleType) 594 } 595 if prof.TimeNanos != 0 { 596 const layout = "Jan 2, 2006 at 3:04pm (MST)" 597 label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) 598 } 599 if prof.DurationNanos != 0 { 600 label = append(label, fmt.Sprintf("Duration: %v", time.Duration(prof.DurationNanos))) 601 } 602 return label 603 } 604 605 // legendDetailLabels generates labels common to graph and text visualization. 606 func legendDetailLabels(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) []string { 607 nodeFraction := rpt.options.NodeFraction 608 edgeFraction := rpt.options.EdgeFraction 609 nodeCount := rpt.options.NodeCount 610 611 label := []string{} 612 613 var flatSum int64 614 for _, n := range g.ns { 615 flatSum = flatSum + n.flat 616 } 617 618 label = append(label, fmt.Sprintf("%s of %s total (%s)", rpt.formatValue(flatSum), rpt.formatValue(rpt.total), percentage(flatSum, rpt.total))) 619 620 if rpt.total > 0 { 621 if droppedNodes > 0 { 622 label = append(label, genLabel(droppedNodes, "node", "cum", 623 rpt.formatValue(int64(float64(rpt.total)*nodeFraction)))) 624 } 625 if droppedEdges > 0 { 626 label = append(label, genLabel(droppedEdges, "edge", "freq", 627 rpt.formatValue(int64(float64(rpt.total)*edgeFraction)))) 628 } 629 if nodeCount > 0 && nodeCount < origCount { 630 label = append(label, fmt.Sprintf("Showing top %d nodes out of %d (cum >= %s)", 631 nodeCount, origCount, 632 rpt.formatValue(g.ns[len(g.ns)-1].cum))) 633 } 634 } 635 return label 636 } 637 638 func genLabel(d int, n, l, f string) string { 639 if d > 1 { 640 n = n + "s" 641 } 642 return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) 643 } 644 645 // dotNode generates a graph node in DOT format. 646 func dotNode(rpt *Report, maxFlat float64, rIndex int, n *node) string { 647 flat, cum := n.flat, n.cum 648 649 labels := strings.Split(n.info.prettyName(), "::") 650 label := strings.Join(labels, `\n`) + `\n` 651 652 flatValue := rpt.formatValue(flat) 653 if flat > 0 { 654 label = label + fmt.Sprintf(`%s(%s)`, 655 flatValue, 656 strings.TrimSpace(percentage(flat, rpt.total))) 657 } else { 658 label = label + "0" 659 } 660 cumValue := flatValue 661 if cum != flat { 662 if flat > 0 { 663 label = label + `\n` 664 } else { 665 label = label + " " 666 } 667 cumValue = rpt.formatValue(cum) 668 label = label + fmt.Sprintf(`of %s(%s)`, 669 cumValue, 670 strings.TrimSpace(percentage(cum, rpt.total))) 671 } 672 673 // Scale font sizes from 8 to 24 based on percentage of flat frequency. 674 // Use non linear growth to emphasize the size difference. 675 baseFontSize, maxFontGrowth := 8, 16.0 676 fontSize := baseFontSize 677 if maxFlat > 0 && flat > 0 && float64(flat) <= maxFlat { 678 fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(flat)/maxFlat))) 679 } 680 return fmt.Sprintf(`N%d [label="%s" fontsize=%d shape=box tooltip="%s (%s)"]`, 681 rIndex, 682 label, 683 fontSize, n.info.prettyName(), cumValue) 684 } 685 686 // dotEdge generates a graph edge in DOT format. 687 func dotEdge(rpt *Report, from, to int, e *edgeInfo) string { 688 w := rpt.formatValue(e.weight) 689 attr := fmt.Sprintf(`label=" %s"`, w) 690 if rpt.total > 0 { 691 if weight := 1 + int(e.weight*100/rpt.total); weight > 1 { 692 attr = fmt.Sprintf(`%s weight=%d`, attr, weight) 693 } 694 if width := 1 + int(e.weight*5/rpt.total); width > 1 { 695 attr = fmt.Sprintf(`%s penwidth=%d`, attr, width) 696 } 697 } 698 arrow := "->" 699 if e.residual { 700 arrow = "..." 701 } 702 tooltip := fmt.Sprintf(`"%s %s %s (%s)"`, 703 e.src.info.prettyName(), arrow, e.dest.info.prettyName(), w) 704 attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, 705 attr, tooltip, tooltip) 706 707 if e.residual { 708 attr = attr + ` style="dotted"` 709 } 710 711 if len(e.src.tags) > 0 { 712 // Separate children further if source has tags. 713 attr = attr + " minlen=2" 714 } 715 return fmt.Sprintf("N%d -> N%d [%s]", from, to, attr) 716 } 717 718 // dotNodelets generates the DOT boxes for the node tags. 719 func dotNodelets(rpt *Report, rIndex int, n *node) (dot string) { 720 const maxNodelets = 4 // Number of nodelets for alphanumeric labels 721 const maxNumNodelets = 4 // Number of nodelets for numeric labels 722 723 var ts, nts tags 724 for _, t := range n.tags { 725 if t.unit == "" { 726 ts = append(ts, t) 727 } else { 728 nts = append(nts, t) 729 } 730 } 731 732 // Select the top maxNodelets alphanumeric labels by weight 733 sort.Sort(ts) 734 if len(ts) > maxNodelets { 735 ts = ts[:maxNodelets] 736 } 737 for i, t := range ts { 738 weight := rpt.formatValue(t.weight) 739 dot += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) 740 dot += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) 741 } 742 743 // Collapse numeric labels into maxNumNodelets buckets, of the form: 744 // 1MB..2MB, 3MB..5MB, ... 745 nts = collapseTags(nts, maxNumNodelets) 746 sort.Sort(nts) 747 for i, t := range nts { 748 weight := rpt.formatValue(t.weight) 749 dot += fmt.Sprintf(`NN%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) 750 dot += fmt.Sprintf(`N%d -> NN%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) 751 } 752 753 return dot 754 } 755 756 // graph summarizes a performance profile into a format that is 757 // suitable for visualization. 758 type graph struct { 759 ns nodes 760 } 761 762 // nodes is an ordered collection of graph nodes. 763 type nodes []*node 764 765 // tags represent sample annotations 766 type tags []*tag 767 type tagMap map[string]*tag 768 769 type tag struct { 770 name string 771 unit string // Describe the value, "" for non-numeric tags 772 value int64 773 weight int64 774 } 775 776 func (t tags) Len() int { return len(t) } 777 func (t tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 778 func (t tags) Less(i, j int) bool { 779 if t[i].weight == t[j].weight { 780 return t[i].name < t[j].name 781 } 782 return t[i].weight > t[j].weight 783 } 784 785 // node is an entry on a profiling report. It represents a unique 786 // program location. It can include multiple names to represent 787 // inlined functions. 788 type node struct { 789 info nodeInfo // Information associated to this entry. 790 791 // values associated to this node. 792 // flat is exclusive to this node, cum includes all descendents. 793 flat, cum int64 794 795 // in and out contains the nodes immediately reaching or reached by this nodes. 796 in, out edgeMap 797 798 // tags provide additional information about subsets of a sample. 799 tags tagMap 800 } 801 802 type nodeInfo struct { 803 name string 804 origName string 805 address uint64 806 file string 807 startLine, lineno int 808 inline bool 809 lowPriority bool 810 objfile string 811 parent *node // Used only if creating a calltree 812 } 813 814 func (n *node) addTags(s *profile.Sample, weight int64) { 815 // Add a tag with all string labels 816 var labels []string 817 for key, vals := range s.Label { 818 for _, v := range vals { 819 labels = append(labels, key+":"+v) 820 } 821 } 822 if len(labels) > 0 { 823 sort.Strings(labels) 824 l := n.tags.findOrAddTag(strings.Join(labels, `\n`), "", 0) 825 l.weight += weight 826 } 827 828 for key, nvals := range s.NumLabel { 829 for _, v := range nvals { 830 label := scaledValueLabel(v, key, "auto") 831 l := n.tags.findOrAddTag(label, key, v) 832 l.weight += weight 833 } 834 } 835 } 836 837 func (m tagMap) findOrAddTag(label, unit string, value int64) *tag { 838 if l := m[label]; l != nil { 839 return l 840 } 841 l := &tag{ 842 name: label, 843 unit: unit, 844 value: value, 845 } 846 m[label] = l 847 return l 848 } 849 850 // collapseTags reduces the number of entries in a tagMap by merging 851 // adjacent nodes into ranges. It uses a greedy approach to merge 852 // starting with the entries with the lowest weight. 853 func collapseTags(ts tags, count int) tags { 854 if len(ts) <= count { 855 return ts 856 } 857 858 sort.Sort(ts) 859 tagGroups := make([]tags, count) 860 for i, t := range ts[:count] { 861 tagGroups[i] = tags{t} 862 } 863 for _, t := range ts[count:] { 864 g, d := 0, tagDistance(t, tagGroups[0][0]) 865 for i := 1; i < count; i++ { 866 if nd := tagDistance(t, tagGroups[i][0]); nd < d { 867 g, d = i, nd 868 } 869 } 870 tagGroups[g] = append(tagGroups[g], t) 871 } 872 873 var nts tags 874 for _, g := range tagGroups { 875 l, w := tagGroupLabel(g) 876 nts = append(nts, &tag{ 877 name: l, 878 weight: w, 879 }) 880 } 881 return nts 882 } 883 884 func tagDistance(t, u *tag) float64 { 885 v, _ := ScaleValue(u.value, u.unit, t.unit) 886 if v < float64(t.value) { 887 return float64(t.value) - v 888 } 889 return v - float64(t.value) 890 } 891 892 func tagGroupLabel(g tags) (string, int64) { 893 if len(g) == 1 { 894 t := g[0] 895 return scaledValueLabel(t.value, t.unit, "auto"), t.weight 896 } 897 min := g[0] 898 max := g[0] 899 w := min.weight 900 for _, t := range g[1:] { 901 if v, _ := ScaleValue(t.value, t.unit, min.unit); int64(v) < min.value { 902 min = t 903 } 904 if v, _ := ScaleValue(t.value, t.unit, max.unit); int64(v) > max.value { 905 max = t 906 } 907 w += t.weight 908 } 909 return scaledValueLabel(min.value, min.unit, "auto") + ".." + 910 scaledValueLabel(max.value, max.unit, "auto"), w 911 } 912 913 // sumNodes adds the flat and sum values on a report. 914 func sumNodes(ns nodes) (flat int64, cum int64) { 915 for _, n := range ns { 916 flat += n.flat 917 cum += n.cum 918 } 919 return 920 } 921 922 type edgeMap map[*node]*edgeInfo 923 924 // edgeInfo contains any attributes to be represented about edges in a graph/ 925 type edgeInfo struct { 926 src, dest *node 927 // The summary weight of the edge 928 weight int64 929 // residual edges connect nodes that were connected through a 930 // separate node, which has been removed from the report. 931 residual bool 932 } 933 934 // bumpWeight increases the weight of an edge. If there isn't such an 935 // edge in the map one is created. 936 func bumpWeight(from, to *node, w int64, residual bool) { 937 if from.out[to] != to.in[from] { 938 panic(fmt.Errorf("asymmetric edges %v %v", *from, *to)) 939 } 940 941 if n := from.out[to]; n != nil { 942 n.weight += w 943 if n.residual && !residual { 944 n.residual = false 945 } 946 return 947 } 948 949 info := &edgeInfo{src: from, dest: to, weight: w, residual: residual} 950 from.out[to] = info 951 to.in[from] = info 952 } 953 954 // Output formats. 955 const ( 956 Proto = iota 957 Dot 958 Tags 959 Tree 960 Text 961 Raw 962 Dis 963 List 964 WebList 965 Callgrind 966 ) 967 968 // Options are the formatting and filtering options used to generate a 969 // profile. 970 type Options struct { 971 OutputFormat int 972 973 CumSort bool 974 CallTree bool 975 PrintAddresses bool 976 DropNegative bool 977 Ratio float64 978 979 NodeCount int 980 NodeFraction float64 981 EdgeFraction float64 982 983 SampleType string 984 SampleUnit string // Unit for the sample data from the profile. 985 OutputUnit string // Units for data formatting in report. 986 987 Symbol *regexp.Regexp // Symbols to include on disassembly report. 988 } 989 990 // newGraph summarizes performance data from a profile into a graph. 991 func newGraph(rpt *Report) (g graph, err error) { 992 prof := rpt.prof 993 o := rpt.options 994 995 // Generate a tree for graphical output if requested. 996 buildTree := o.CallTree && o.OutputFormat == Dot 997 998 locations := make(map[uint64][]nodeInfo) 999 for _, l := range prof.Location { 1000 locations[l.ID] = newLocInfo(l) 1001 } 1002 1003 nm := make(nodeMap) 1004 for _, sample := range prof.Sample { 1005 if sample.Location == nil { 1006 continue 1007 } 1008 1009 // Construct list of node names for sample. 1010 var stack []nodeInfo 1011 for _, loc := range sample.Location { 1012 id := loc.ID 1013 stack = append(stack, locations[id]...) 1014 } 1015 1016 // Upfront pass to update the parent chains, to prevent the 1017 // merging of nodes with different parents. 1018 if buildTree { 1019 var nn *node 1020 for i := len(stack); i > 0; i-- { 1021 n := &stack[i-1] 1022 n.parent = nn 1023 nn = nm.findOrInsertNode(*n) 1024 } 1025 } 1026 1027 leaf := nm.findOrInsertNode(stack[0]) 1028 weight := rpt.sampleValue(sample) 1029 leaf.addTags(sample, weight) 1030 1031 // Aggregate counter data. 1032 leaf.flat += weight 1033 seen := make(map[*node]bool) 1034 var nn *node 1035 for _, s := range stack { 1036 n := nm.findOrInsertNode(s) 1037 if !seen[n] { 1038 seen[n] = true 1039 n.cum += weight 1040 1041 if nn != nil { 1042 bumpWeight(n, nn, weight, false) 1043 } 1044 } 1045 nn = n 1046 } 1047 } 1048 1049 // Collect new nodes into a report. 1050 ns := make(nodes, 0, len(nm)) 1051 for _, n := range nm { 1052 if rpt.options.DropNegative && n.flat < 0 { 1053 continue 1054 } 1055 ns = append(ns, n) 1056 } 1057 1058 return graph{ns}, nil 1059 } 1060 1061 // Create a slice of formatted names for a location. 1062 func newLocInfo(l *profile.Location) []nodeInfo { 1063 var objfile string 1064 1065 if m := l.Mapping; m != nil { 1066 objfile = m.File 1067 } 1068 1069 if len(l.Line) == 0 { 1070 return []nodeInfo{ 1071 { 1072 address: l.Address, 1073 objfile: objfile, 1074 }, 1075 } 1076 } 1077 var info []nodeInfo 1078 numInlineFrames := len(l.Line) - 1 1079 for li, line := range l.Line { 1080 ni := nodeInfo{ 1081 address: l.Address, 1082 lineno: int(line.Line), 1083 inline: li < numInlineFrames, 1084 objfile: objfile, 1085 } 1086 1087 if line.Function != nil { 1088 ni.name = line.Function.Name 1089 ni.origName = line.Function.SystemName 1090 ni.file = line.Function.Filename 1091 ni.startLine = int(line.Function.StartLine) 1092 } 1093 1094 info = append(info, ni) 1095 } 1096 return info 1097 } 1098 1099 // nodeMap maps from a node info struct to a node. It is used to merge 1100 // report entries with the same info. 1101 type nodeMap map[nodeInfo]*node 1102 1103 func (m nodeMap) findOrInsertNode(info nodeInfo) *node { 1104 rr := m[info] 1105 if rr == nil { 1106 rr = &node{ 1107 info: info, 1108 in: make(edgeMap), 1109 out: make(edgeMap), 1110 tags: make(map[string]*tag), 1111 } 1112 m[info] = rr 1113 } 1114 return rr 1115 } 1116 1117 // preprocess does any required filtering/sorting according to the 1118 // report options. Returns the mapping from each node to any nodes 1119 // removed by path compression and statistics on the nodes/edges removed. 1120 func (g *graph) preprocess(rpt *Report) (origCount, droppedNodes, droppedEdges int) { 1121 o := rpt.options 1122 1123 // Compute total weight of current set of nodes. 1124 // This is <= rpt.total because of node filtering. 1125 var totalValue int64 1126 for _, n := range g.ns { 1127 totalValue += n.flat 1128 } 1129 1130 // Remove nodes with value <= total*nodeFraction 1131 if nodeFraction := o.NodeFraction; nodeFraction > 0 { 1132 var removed nodes 1133 minValue := int64(float64(totalValue) * nodeFraction) 1134 kept := make(nodes, 0, len(g.ns)) 1135 for _, n := range g.ns { 1136 if n.cum < minValue { 1137 removed = append(removed, n) 1138 } else { 1139 kept = append(kept, n) 1140 tagsKept := make(map[string]*tag) 1141 for s, t := range n.tags { 1142 if t.weight >= minValue { 1143 tagsKept[s] = t 1144 } 1145 } 1146 n.tags = tagsKept 1147 } 1148 } 1149 droppedNodes = len(removed) 1150 removeNodes(removed, false, false) 1151 g.ns = kept 1152 } 1153 1154 // Remove edges below minimum frequency. 1155 if edgeFraction := o.EdgeFraction; edgeFraction > 0 { 1156 minEdge := int64(float64(totalValue) * edgeFraction) 1157 for _, n := range g.ns { 1158 for src, e := range n.in { 1159 if e.weight < minEdge { 1160 delete(n.in, src) 1161 delete(src.out, n) 1162 droppedEdges++ 1163 } 1164 } 1165 } 1166 } 1167 1168 sortOrder := flatName 1169 if o.CumSort { 1170 // Force cum sorting for graph output, to preserve connectivity. 1171 sortOrder = cumName 1172 } 1173 1174 // Nodes that have flat==0 and a single in/out do not provide much 1175 // information. Give them first chance to be removed. Do not consider edges 1176 // from/to nodes that are expected to be removed. 1177 maxNodes := o.NodeCount 1178 if o.OutputFormat == Dot { 1179 if maxNodes > 0 && maxNodes < len(g.ns) { 1180 sortOrder = cumName 1181 g.ns.sort(cumName) 1182 cumCutoff := g.ns[maxNodes].cum 1183 for _, n := range g.ns { 1184 if n.flat == 0 { 1185 if count := countEdges(n.out, cumCutoff); count > 1 { 1186 continue 1187 } 1188 if count := countEdges(n.in, cumCutoff); count != 1 { 1189 continue 1190 } 1191 n.info.lowPriority = true 1192 } 1193 } 1194 } 1195 } 1196 1197 g.ns.sort(sortOrder) 1198 if maxNodes > 0 { 1199 origCount = len(g.ns) 1200 for index, nodes := 0, 0; index < len(g.ns); index++ { 1201 nodes++ 1202 // For DOT output, count the tags as nodes since we will draw 1203 // boxes for them. 1204 if o.OutputFormat == Dot { 1205 nodes += len(g.ns[index].tags) 1206 } 1207 if nodes > maxNodes { 1208 // Trim to the top n nodes. Create dotted edges to bridge any 1209 // broken connections. 1210 removeNodes(g.ns[index:], true, true) 1211 g.ns = g.ns[:index] 1212 break 1213 } 1214 } 1215 } 1216 removeRedundantEdges(g.ns) 1217 1218 // Select best unit for profile output. 1219 // Find the appropriate units for the smallest non-zero sample 1220 if o.OutputUnit == "minimum" && len(g.ns) > 0 { 1221 var maxValue, minValue int64 1222 1223 for _, n := range g.ns { 1224 if n.flat > 0 && (minValue == 0 || n.flat < minValue) { 1225 minValue = n.flat 1226 } 1227 if n.cum > maxValue { 1228 maxValue = n.cum 1229 } 1230 } 1231 if r := o.Ratio; r > 0 && r != 1 { 1232 minValue = int64(float64(minValue) * r) 1233 maxValue = int64(float64(maxValue) * r) 1234 } 1235 1236 _, minUnit := ScaleValue(minValue, o.SampleUnit, "minimum") 1237 _, maxUnit := ScaleValue(maxValue, o.SampleUnit, "minimum") 1238 1239 unit := minUnit 1240 if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind { 1241 // Minimum and maximum values have different units. Scale 1242 // minimum by 100 to use larger units, allowing minimum value to 1243 // be scaled down to 0.01, except for callgrind reports since 1244 // they can only represent integer values. 1245 _, unit = ScaleValue(100*minValue, o.SampleUnit, "minimum") 1246 } 1247 1248 if unit != "" { 1249 o.OutputUnit = unit 1250 } else { 1251 o.OutputUnit = o.SampleUnit 1252 } 1253 } 1254 return 1255 } 1256 1257 // countEdges counts the number of edges below the specified cutoff. 1258 func countEdges(el edgeMap, cutoff int64) int { 1259 count := 0 1260 for _, e := range el { 1261 if e.weight > cutoff { 1262 count++ 1263 } 1264 } 1265 return count 1266 } 1267 1268 // removeNodes removes nodes from a report, optionally bridging 1269 // connections between in/out edges and spreading out their weights 1270 // proportionally. residual marks new bridge edges as residual 1271 // (dotted). 1272 func removeNodes(toRemove nodes, bridge, residual bool) { 1273 for _, n := range toRemove { 1274 for ei := range n.in { 1275 delete(ei.out, n) 1276 } 1277 if bridge { 1278 for ei, wi := range n.in { 1279 for eo, wo := range n.out { 1280 var weight int64 1281 if n.cum != 0 { 1282 weight = int64(float64(wo.weight) * (float64(wi.weight) / float64(n.cum))) 1283 } 1284 bumpWeight(ei, eo, weight, residual) 1285 } 1286 } 1287 } 1288 for eo := range n.out { 1289 delete(eo.in, n) 1290 } 1291 } 1292 } 1293 1294 // removeRedundantEdges removes residual edges if the destination can 1295 // be reached through another path. This is done to simplify the graph 1296 // while preserving connectivity. 1297 func removeRedundantEdges(ns nodes) { 1298 // Walk the nodes and outgoing edges in reverse order to prefer 1299 // removing edges with the lowest weight. 1300 for i := len(ns); i > 0; i-- { 1301 n := ns[i-1] 1302 in := sortedEdges(n.in) 1303 for j := len(in); j > 0; j-- { 1304 if e := in[j-1]; e.residual && isRedundant(e) { 1305 delete(e.src.out, e.dest) 1306 delete(e.dest.in, e.src) 1307 } 1308 } 1309 } 1310 } 1311 1312 // isRedundant determines if an edge can be removed without impacting 1313 // connectivity of the whole graph. This is implemented by checking if the 1314 // nodes have a common ancestor after removing the edge. 1315 func isRedundant(e *edgeInfo) bool { 1316 destPred := predecessors(e, e.dest) 1317 if len(destPred) == 1 { 1318 return false 1319 } 1320 srcPred := predecessors(e, e.src) 1321 1322 for n := range srcPred { 1323 if destPred[n] && n != e.dest { 1324 return true 1325 } 1326 } 1327 return false 1328 } 1329 1330 // predecessors collects all the predecessors to node n, excluding edge e. 1331 func predecessors(e *edgeInfo, n *node) map[*node]bool { 1332 seen := map[*node]bool{n: true} 1333 queue := []*node{n} 1334 for len(queue) > 0 { 1335 n := queue[0] 1336 queue = queue[1:] 1337 for _, ie := range n.in { 1338 if e == ie || seen[ie.src] { 1339 continue 1340 } 1341 seen[ie.src] = true 1342 queue = append(queue, ie.src) 1343 } 1344 } 1345 return seen 1346 } 1347 1348 // nodeSorter is a mechanism used to allow a report to be sorted 1349 // in different ways. 1350 type nodeSorter struct { 1351 rs nodes 1352 less func(i, j int) bool 1353 } 1354 1355 func (s nodeSorter) Len() int { return len(s.rs) } 1356 func (s nodeSorter) Swap(i, j int) { s.rs[i], s.rs[j] = s.rs[j], s.rs[i] } 1357 func (s nodeSorter) Less(i, j int) bool { return s.less(i, j) } 1358 1359 type nodeOrder int 1360 1361 const ( 1362 flatName nodeOrder = iota 1363 flatCumName 1364 cumName 1365 nameOrder 1366 fileOrder 1367 addressOrder 1368 ) 1369 1370 // sort reorders the entries in a report based on the specified 1371 // ordering criteria. The result is sorted in decreasing order for 1372 // numeric quantities, alphabetically for text, and increasing for 1373 // addresses. 1374 func (ns nodes) sort(o nodeOrder) error { 1375 var s nodeSorter 1376 1377 switch o { 1378 case flatName: 1379 s = nodeSorter{ns, 1380 func(i, j int) bool { 1381 if iv, jv := ns[i].flat, ns[j].flat; iv != jv { 1382 return iv > jv 1383 } 1384 if ns[i].info.prettyName() != ns[j].info.prettyName() { 1385 return ns[i].info.prettyName() < ns[j].info.prettyName() 1386 } 1387 iv, jv := ns[i].cum, ns[j].cum 1388 return iv > jv 1389 }, 1390 } 1391 case flatCumName: 1392 s = nodeSorter{ns, 1393 func(i, j int) bool { 1394 if iv, jv := ns[i].flat, ns[j].flat; iv != jv { 1395 return iv > jv 1396 } 1397 if iv, jv := ns[i].cum, ns[j].cum; iv != jv { 1398 return iv > jv 1399 } 1400 return ns[i].info.prettyName() < ns[j].info.prettyName() 1401 }, 1402 } 1403 case cumName: 1404 s = nodeSorter{ns, 1405 func(i, j int) bool { 1406 if ns[i].info.lowPriority != ns[j].info.lowPriority { 1407 return ns[j].info.lowPriority 1408 } 1409 if iv, jv := ns[i].cum, ns[j].cum; iv != jv { 1410 return iv > jv 1411 } 1412 if ns[i].info.prettyName() != ns[j].info.prettyName() { 1413 return ns[i].info.prettyName() < ns[j].info.prettyName() 1414 } 1415 iv, jv := ns[i].flat, ns[j].flat 1416 return iv > jv 1417 }, 1418 } 1419 case nameOrder: 1420 s = nodeSorter{ns, 1421 func(i, j int) bool { 1422 return ns[i].info.name < ns[j].info.name 1423 }, 1424 } 1425 case fileOrder: 1426 s = nodeSorter{ns, 1427 func(i, j int) bool { 1428 return ns[i].info.file < ns[j].info.file 1429 }, 1430 } 1431 case addressOrder: 1432 s = nodeSorter{ns, 1433 func(i, j int) bool { 1434 return ns[i].info.address < ns[j].info.address 1435 }, 1436 } 1437 default: 1438 return fmt.Errorf("report: unrecognized sort ordering: %d", o) 1439 } 1440 sort.Sort(s) 1441 return nil 1442 } 1443 1444 type edgeList []*edgeInfo 1445 1446 // sortedEdges return a slice of the edges in the map, sorted for 1447 // visualization. The sort order is first based on the edge weight 1448 // (higher-to-lower) and then by the node names to avoid flakiness. 1449 func sortedEdges(edges map[*node]*edgeInfo) edgeList { 1450 el := make(edgeList, 0, len(edges)) 1451 for _, w := range edges { 1452 el = append(el, w) 1453 } 1454 1455 sort.Sort(el) 1456 return el 1457 } 1458 1459 func (el edgeList) Len() int { 1460 return len(el) 1461 } 1462 1463 func (el edgeList) Less(i, j int) bool { 1464 if el[i].weight != el[j].weight { 1465 return el[i].weight > el[j].weight 1466 } 1467 1468 from1 := el[i].src.info.prettyName() 1469 from2 := el[j].src.info.prettyName() 1470 if from1 != from2 { 1471 return from1 < from2 1472 } 1473 1474 to1 := el[i].dest.info.prettyName() 1475 to2 := el[j].dest.info.prettyName() 1476 1477 return to1 < to2 1478 } 1479 1480 func (el edgeList) Swap(i, j int) { 1481 el[i], el[j] = el[j], el[i] 1482 } 1483 1484 func (el edgeList) sum() int64 { 1485 var ret int64 1486 for _, e := range el { 1487 ret += e.weight 1488 } 1489 return ret 1490 } 1491 1492 // ScaleValue reformats a value from a unit to a different unit. 1493 func ScaleValue(value int64, fromUnit, toUnit string) (sv float64, su string) { 1494 // Avoid infinite recursion on overflow. 1495 if value < 0 && -value > 0 { 1496 v, u := ScaleValue(-value, fromUnit, toUnit) 1497 return -v, u 1498 } 1499 if m, u, ok := memoryLabel(value, fromUnit, toUnit); ok { 1500 return m, u 1501 } 1502 if t, u, ok := timeLabel(value, fromUnit, toUnit); ok { 1503 return t, u 1504 } 1505 // Skip non-interesting units. 1506 switch toUnit { 1507 case "count", "sample", "unit", "minimum": 1508 return float64(value), "" 1509 default: 1510 return float64(value), toUnit 1511 } 1512 } 1513 1514 func scaledValueLabel(value int64, fromUnit, toUnit string) string { 1515 v, u := ScaleValue(value, fromUnit, toUnit) 1516 1517 sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00") 1518 if sv == "0" || sv == "-0" { 1519 return "0" 1520 } 1521 return sv + u 1522 } 1523 1524 func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { 1525 fromUnit = strings.TrimSuffix(strings.ToLower(fromUnit), "s") 1526 toUnit = strings.TrimSuffix(strings.ToLower(toUnit), "s") 1527 1528 switch fromUnit { 1529 case "byte", "b": 1530 case "kilobyte", "kb": 1531 value *= 1024 1532 case "megabyte", "mb": 1533 value *= 1024 * 1024 1534 case "gigabyte", "gb": 1535 value *= 1024 * 1024 * 1024 1536 default: 1537 return 0, "", false 1538 } 1539 1540 if toUnit == "minimum" || toUnit == "auto" { 1541 switch { 1542 case value < 1024: 1543 toUnit = "b" 1544 case value < 1024*1024: 1545 toUnit = "kb" 1546 case value < 1024*1024*1024: 1547 toUnit = "mb" 1548 default: 1549 toUnit = "gb" 1550 } 1551 } 1552 1553 var output float64 1554 switch toUnit { 1555 default: 1556 output, toUnit = float64(value), "B" 1557 case "kb", "kbyte", "kilobyte": 1558 output, toUnit = float64(value)/1024, "kB" 1559 case "mb", "mbyte", "megabyte": 1560 output, toUnit = float64(value)/(1024*1024), "MB" 1561 case "gb", "gbyte", "gigabyte": 1562 output, toUnit = float64(value)/(1024*1024*1024), "GB" 1563 } 1564 return output, toUnit, true 1565 } 1566 1567 func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { 1568 fromUnit = strings.ToLower(fromUnit) 1569 if len(fromUnit) > 2 { 1570 fromUnit = strings.TrimSuffix(fromUnit, "s") 1571 } 1572 1573 toUnit = strings.ToLower(toUnit) 1574 if len(toUnit) > 2 { 1575 toUnit = strings.TrimSuffix(toUnit, "s") 1576 } 1577 1578 var d time.Duration 1579 switch fromUnit { 1580 case "nanosecond", "ns": 1581 d = time.Duration(value) * time.Nanosecond 1582 case "microsecond": 1583 d = time.Duration(value) * time.Microsecond 1584 case "millisecond", "ms": 1585 d = time.Duration(value) * time.Millisecond 1586 case "second", "sec": 1587 d = time.Duration(value) * time.Second 1588 case "cycle": 1589 return float64(value), "", true 1590 default: 1591 return 0, "", false 1592 } 1593 1594 if toUnit == "minimum" || toUnit == "auto" { 1595 switch { 1596 case d < 1*time.Microsecond: 1597 toUnit = "ns" 1598 case d < 1*time.Millisecond: 1599 toUnit = "us" 1600 case d < 1*time.Second: 1601 toUnit = "ms" 1602 case d < 1*time.Minute: 1603 toUnit = "sec" 1604 case d < 1*time.Hour: 1605 toUnit = "min" 1606 case d < 24*time.Hour: 1607 toUnit = "hour" 1608 case d < 15*24*time.Hour: 1609 toUnit = "day" 1610 case d < 120*24*time.Hour: 1611 toUnit = "week" 1612 default: 1613 toUnit = "year" 1614 } 1615 } 1616 1617 var output float64 1618 dd := float64(d) 1619 switch toUnit { 1620 case "ns", "nanosecond": 1621 output, toUnit = dd/float64(time.Nanosecond), "ns" 1622 case "us", "microsecond": 1623 output, toUnit = dd/float64(time.Microsecond), "us" 1624 case "ms", "millisecond": 1625 output, toUnit = dd/float64(time.Millisecond), "ms" 1626 case "min", "minute": 1627 output, toUnit = dd/float64(time.Minute), "mins" 1628 case "hour", "hr": 1629 output, toUnit = dd/float64(time.Hour), "hrs" 1630 case "day": 1631 output, toUnit = dd/float64(24*time.Hour), "days" 1632 case "week", "wk": 1633 output, toUnit = dd/float64(7*24*time.Hour), "wks" 1634 case "year", "yr": 1635 output, toUnit = dd/float64(365*7*24*time.Hour), "yrs" 1636 default: 1637 fallthrough 1638 case "sec", "second", "s": 1639 output, toUnit = dd/float64(time.Second), "s" 1640 } 1641 return output, toUnit, true 1642 } 1643 1644 // prettyName determines the printable name to be used for a node. 1645 func (info *nodeInfo) prettyName() string { 1646 var name string 1647 if info.address != 0 { 1648 name = fmt.Sprintf("%016x", info.address) 1649 } 1650 1651 if info.name != "" { 1652 name = name + " " + info.name 1653 } 1654 1655 if info.file != "" { 1656 name += " " + trimPath(info.file) 1657 if info.lineno != 0 { 1658 name += fmt.Sprintf(":%d", info.lineno) 1659 } 1660 } 1661 1662 if info.inline { 1663 name = name + " (inline)" 1664 } 1665 1666 if name = strings.TrimSpace(name); name == "" && info.objfile != "" { 1667 name = "[" + filepath.Base(info.objfile) + "]" 1668 } 1669 return name 1670 } 1671 1672 // New builds a new report indexing the sample values interpreting the 1673 // samples with the provided function. 1674 func New(prof *profile.Profile, options Options, value func(s *profile.Sample) int64, unit string) *Report { 1675 o := &options 1676 if o.SampleUnit == "" { 1677 o.SampleUnit = unit 1678 } 1679 format := func(v int64) string { 1680 if r := o.Ratio; r > 0 && r != 1 { 1681 fv := float64(v) * r 1682 v = int64(fv) 1683 } 1684 return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) 1685 } 1686 return &Report{prof, computeTotal(prof, value), o, value, format} 1687 } 1688 1689 // NewDefault builds a new report indexing the sample values with the 1690 // last value available. 1691 func NewDefault(prof *profile.Profile, options Options) *Report { 1692 index := len(prof.SampleType) - 1 1693 o := &options 1694 if o.SampleUnit == "" { 1695 o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) 1696 } 1697 value := func(s *profile.Sample) int64 { 1698 return s.Value[index] 1699 } 1700 format := func(v int64) string { 1701 if r := o.Ratio; r > 0 && r != 1 { 1702 fv := float64(v) * r 1703 v = int64(fv) 1704 } 1705 return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) 1706 } 1707 return &Report{prof, computeTotal(prof, value), o, value, format} 1708 } 1709 1710 func computeTotal(prof *profile.Profile, value func(s *profile.Sample) int64) int64 { 1711 var ret int64 1712 for _, sample := range prof.Sample { 1713 ret += value(sample) 1714 } 1715 return ret 1716 } 1717 1718 // Report contains the data and associated routines to extract a 1719 // report from a profile. 1720 type Report struct { 1721 prof *profile.Profile 1722 total int64 1723 options *Options 1724 sampleValue func(*profile.Sample) int64 1725 formatValue func(int64) string 1726 } 1727