github.com/euank/go@v0.0.0-20160829210321-495514729181/src/cmd/internal/pprof/report/report.go (about) 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/internal/pprof/plugin" 21 "cmd/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[filepath.Base(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, "events:", o.SampleType+"("+o.OutputUnit+")") 359 360 files := make(map[string]int) 361 names := make(map[string]int) 362 for _, n := range g.ns { 363 fmt.Fprintln(w, "fl="+callgrindName(files, n.info.file)) 364 fmt.Fprintln(w, "fn="+callgrindName(names, n.info.name)) 365 sv, _ := ScaleValue(n.flat, o.SampleUnit, o.OutputUnit) 366 fmt.Fprintf(w, "%d %d\n", n.info.lineno, int(sv)) 367 368 // Print outgoing edges. 369 for _, out := range sortedEdges(n.out) { 370 c, _ := ScaleValue(out.weight, o.SampleUnit, o.OutputUnit) 371 count := fmt.Sprintf("%d", int(c)) 372 callee := out.dest 373 fmt.Fprintln(w, "cfl="+callgrindName(files, callee.info.file)) 374 fmt.Fprintln(w, "cfn="+callgrindName(names, callee.info.name)) 375 fmt.Fprintln(w, "calls="+count, callee.info.lineno) 376 fmt.Fprintln(w, n.info.lineno, count) 377 } 378 fmt.Fprintln(w) 379 } 380 381 return nil 382 } 383 384 // callgrindName implements the callgrind naming compression scheme. 385 // For names not previously seen returns "(N) name", where N is a 386 // unique index. For names previously seen returns "(N)" where N is 387 // the index returned the first time. 388 func callgrindName(names map[string]int, name string) string { 389 if name == "" { 390 return "" 391 } 392 if id, ok := names[name]; ok { 393 return fmt.Sprintf("(%d)", id) 394 } 395 id := len(names) + 1 396 names[name] = id 397 return fmt.Sprintf("(%d) %s", id, name) 398 } 399 400 // printTree prints a tree-based report in text form. 401 func printTree(w io.Writer, rpt *Report) error { 402 const separator = "----------------------------------------------------------+-------------" 403 const legend = " flat flat% sum% cum cum% calls calls% + context " 404 405 g, err := newGraph(rpt) 406 if err != nil { 407 return err 408 } 409 410 origCount, droppedNodes, _ := g.preprocess(rpt) 411 fmt.Fprintln(w, strings.Join(legendDetailLabels(rpt, g, origCount, droppedNodes, 0), "\n")) 412 413 fmt.Fprintln(w, separator) 414 fmt.Fprintln(w, legend) 415 var flatSum int64 416 417 rx := rpt.options.Symbol 418 for _, n := range g.ns { 419 name, flat, cum := n.info.prettyName(), n.flat, n.cum 420 421 // Skip any entries that do not match the regexp (for the "peek" command). 422 if rx != nil && !rx.MatchString(name) { 423 continue 424 } 425 426 fmt.Fprintln(w, separator) 427 // Print incoming edges. 428 inEdges := sortedEdges(n.in) 429 inSum := inEdges.sum() 430 for _, in := range inEdges { 431 fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(in.weight), 432 percentage(in.weight, inSum), in.src.info.prettyName()) 433 } 434 435 // Print current node. 436 flatSum += flat 437 fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", 438 rpt.formatValue(flat), 439 percentage(flat, rpt.total), 440 percentage(flatSum, rpt.total), 441 rpt.formatValue(cum), 442 percentage(cum, rpt.total), 443 name) 444 445 // Print outgoing edges. 446 outEdges := sortedEdges(n.out) 447 outSum := outEdges.sum() 448 for _, out := range outEdges { 449 fmt.Fprintf(w, "%50s %s | %s\n", rpt.formatValue(out.weight), 450 percentage(out.weight, outSum), out.dest.info.prettyName()) 451 } 452 } 453 if len(g.ns) > 0 { 454 fmt.Fprintln(w, separator) 455 } 456 return nil 457 } 458 459 // printDOT prints an annotated callgraph in DOT format. 460 func printDOT(w io.Writer, rpt *Report) error { 461 g, err := newGraph(rpt) 462 if err != nil { 463 return err 464 } 465 466 origCount, droppedNodes, droppedEdges := g.preprocess(rpt) 467 468 prof := rpt.prof 469 graphname := "unnamed" 470 if len(prof.Mapping) > 0 { 471 graphname = filepath.Base(prof.Mapping[0].File) 472 } 473 fmt.Fprintln(w, `digraph "`+graphname+`" {`) 474 fmt.Fprintln(w, `node [style=filled fillcolor="#f8f8f8"]`) 475 fmt.Fprintln(w, dotLegend(rpt, g, origCount, droppedNodes, droppedEdges)) 476 477 if len(g.ns) == 0 { 478 fmt.Fprintln(w, "}") 479 return nil 480 } 481 482 // Make sure nodes have a unique consistent id. 483 nodeIndex := make(map[*node]int) 484 maxFlat := float64(g.ns[0].flat) 485 for i, n := range g.ns { 486 nodeIndex[n] = i + 1 487 if float64(n.flat) > maxFlat { 488 maxFlat = float64(n.flat) 489 } 490 } 491 var edges edgeList 492 for _, n := range g.ns { 493 node := dotNode(rpt, maxFlat, nodeIndex[n], n) 494 fmt.Fprintln(w, node) 495 if nodelets := dotNodelets(rpt, nodeIndex[n], n); nodelets != "" { 496 fmt.Fprint(w, nodelets) 497 } 498 499 // Collect outgoing edges. 500 for _, e := range n.out { 501 edges = append(edges, e) 502 } 503 } 504 // Sort edges by frequency as a hint to the graph layout engine. 505 sort.Sort(edges) 506 for _, e := range edges { 507 fmt.Fprintln(w, dotEdge(rpt, nodeIndex[e.src], nodeIndex[e.dest], e)) 508 } 509 fmt.Fprintln(w, "}") 510 return nil 511 } 512 513 // percentage computes the percentage of total of a value, and encodes 514 // it as a string. At least two digits of precision are printed. 515 func percentage(value, total int64) string { 516 var ratio float64 517 if total != 0 { 518 ratio = float64(value) / float64(total) * 100 519 } 520 switch { 521 case ratio >= 99.95: 522 return " 100%" 523 case ratio >= 1.0: 524 return fmt.Sprintf("%5.2f%%", ratio) 525 default: 526 return fmt.Sprintf("%5.2g%%", ratio) 527 } 528 } 529 530 // dotLegend generates the overall graph label for a report in DOT format. 531 func dotLegend(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) string { 532 label := legendLabels(rpt) 533 label = append(label, legendDetailLabels(rpt, g, origCount, droppedNodes, droppedEdges)...) 534 return fmt.Sprintf(`subgraph cluster_L { L [shape=box fontsize=32 label="%s\l"] }`, strings.Join(label, `\l`)) 535 } 536 537 // legendLabels generates labels exclusive to graph visualization. 538 func legendLabels(rpt *Report) []string { 539 prof := rpt.prof 540 o := rpt.options 541 var label []string 542 if len(prof.Mapping) > 0 { 543 if prof.Mapping[0].File != "" { 544 label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) 545 } 546 if prof.Mapping[0].BuildID != "" { 547 label = append(label, "Build ID: "+prof.Mapping[0].BuildID) 548 } 549 } 550 if o.SampleType != "" { 551 label = append(label, "Type: "+o.SampleType) 552 } 553 if prof.TimeNanos != 0 { 554 const layout = "Jan 2, 2006 at 3:04pm (MST)" 555 label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) 556 } 557 if prof.DurationNanos != 0 { 558 label = append(label, fmt.Sprintf("Duration: %v", time.Duration(prof.DurationNanos))) 559 } 560 return label 561 } 562 563 // legendDetailLabels generates labels common to graph and text visualization. 564 func legendDetailLabels(rpt *Report, g graph, origCount, droppedNodes, droppedEdges int) []string { 565 nodeFraction := rpt.options.NodeFraction 566 edgeFraction := rpt.options.EdgeFraction 567 nodeCount := rpt.options.NodeCount 568 569 label := []string{} 570 571 var flatSum int64 572 for _, n := range g.ns { 573 flatSum = flatSum + n.flat 574 } 575 576 label = append(label, fmt.Sprintf("%s of %s total (%s)", rpt.formatValue(flatSum), rpt.formatValue(rpt.total), percentage(flatSum, rpt.total))) 577 578 if rpt.total > 0 { 579 if droppedNodes > 0 { 580 label = append(label, genLabel(droppedNodes, "node", "cum", 581 rpt.formatValue(int64(float64(rpt.total)*nodeFraction)))) 582 } 583 if droppedEdges > 0 { 584 label = append(label, genLabel(droppedEdges, "edge", "freq", 585 rpt.formatValue(int64(float64(rpt.total)*edgeFraction)))) 586 } 587 if nodeCount > 0 && nodeCount < origCount { 588 label = append(label, fmt.Sprintf("Showing top %d nodes out of %d (cum >= %s)", 589 nodeCount, origCount, 590 rpt.formatValue(g.ns[len(g.ns)-1].cum))) 591 } 592 } 593 return label 594 } 595 596 func genLabel(d int, n, l, f string) string { 597 if d > 1 { 598 n = n + "s" 599 } 600 return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) 601 } 602 603 // dotNode generates a graph node in DOT format. 604 func dotNode(rpt *Report, maxFlat float64, rIndex int, n *node) string { 605 flat, cum := n.flat, n.cum 606 607 labels := strings.Split(n.info.prettyName(), "::") 608 label := strings.Join(labels, `\n`) + `\n` 609 610 flatValue := rpt.formatValue(flat) 611 if flat > 0 { 612 label = label + fmt.Sprintf(`%s(%s)`, 613 flatValue, 614 strings.TrimSpace(percentage(flat, rpt.total))) 615 } else { 616 label = label + "0" 617 } 618 cumValue := flatValue 619 if cum != flat { 620 if flat > 0 { 621 label = label + `\n` 622 } else { 623 label = label + " " 624 } 625 cumValue = rpt.formatValue(cum) 626 label = label + fmt.Sprintf(`of %s(%s)`, 627 cumValue, 628 strings.TrimSpace(percentage(cum, rpt.total))) 629 } 630 631 // Scale font sizes from 8 to 24 based on percentage of flat frequency. 632 // Use non linear growth to emphasize the size difference. 633 baseFontSize, maxFontGrowth := 8, 16.0 634 fontSize := baseFontSize 635 if maxFlat > 0 && flat > 0 && float64(flat) <= maxFlat { 636 fontSize += int(math.Ceil(maxFontGrowth * math.Sqrt(float64(flat)/maxFlat))) 637 } 638 return fmt.Sprintf(`N%d [label="%s" fontsize=%d shape=box tooltip="%s (%s)"]`, 639 rIndex, 640 label, 641 fontSize, n.info.prettyName(), cumValue) 642 } 643 644 // dotEdge generates a graph edge in DOT format. 645 func dotEdge(rpt *Report, from, to int, e *edgeInfo) string { 646 w := rpt.formatValue(e.weight) 647 attr := fmt.Sprintf(`label=" %s"`, w) 648 if rpt.total > 0 { 649 if weight := 1 + int(e.weight*100/rpt.total); weight > 1 { 650 attr = fmt.Sprintf(`%s weight=%d`, attr, weight) 651 } 652 if width := 1 + int(e.weight*5/rpt.total); width > 1 { 653 attr = fmt.Sprintf(`%s penwidth=%d`, attr, width) 654 } 655 } 656 arrow := "->" 657 if e.residual { 658 arrow = "..." 659 } 660 tooltip := fmt.Sprintf(`"%s %s %s (%s)"`, 661 e.src.info.prettyName(), arrow, e.dest.info.prettyName(), w) 662 attr = fmt.Sprintf(`%s tooltip=%s labeltooltip=%s`, 663 attr, tooltip, tooltip) 664 665 if e.residual { 666 attr = attr + ` style="dotted"` 667 } 668 669 if len(e.src.tags) > 0 { 670 // Separate children further if source has tags. 671 attr = attr + " minlen=2" 672 } 673 return fmt.Sprintf("N%d -> N%d [%s]", from, to, attr) 674 } 675 676 // dotNodelets generates the DOT boxes for the node tags. 677 func dotNodelets(rpt *Report, rIndex int, n *node) (dot string) { 678 const maxNodelets = 4 // Number of nodelets for alphanumeric labels 679 const maxNumNodelets = 4 // Number of nodelets for numeric labels 680 681 var ts, nts tags 682 for _, t := range n.tags { 683 if t.unit == "" { 684 ts = append(ts, t) 685 } else { 686 nts = append(nts, t) 687 } 688 } 689 690 // Select the top maxNodelets alphanumeric labels by weight 691 sort.Sort(ts) 692 if len(ts) > maxNodelets { 693 ts = ts[:maxNodelets] 694 } 695 for i, t := range ts { 696 weight := rpt.formatValue(t.weight) 697 dot += fmt.Sprintf(`N%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) 698 dot += fmt.Sprintf(`N%d -> N%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) 699 } 700 701 // Collapse numeric labels into maxNumNodelets buckets, of the form: 702 // 1MB..2MB, 3MB..5MB, ... 703 nts = collapseTags(nts, maxNumNodelets) 704 sort.Sort(nts) 705 for i, t := range nts { 706 weight := rpt.formatValue(t.weight) 707 dot += fmt.Sprintf(`NN%d_%d [label = "%s" fontsize=8 shape=box3d tooltip="%s"]`+"\n", rIndex, i, t.name, weight) 708 dot += fmt.Sprintf(`N%d -> NN%d_%d [label=" %s" weight=100 tooltip="\L" labeltooltip="\L"]`+"\n", rIndex, rIndex, i, weight) 709 } 710 711 return dot 712 } 713 714 // graph summarizes a performance profile into a format that is 715 // suitable for visualization. 716 type graph struct { 717 ns nodes 718 } 719 720 // nodes is an ordered collection of graph nodes. 721 type nodes []*node 722 723 // tags represent sample annotations 724 type tags []*tag 725 type tagMap map[string]*tag 726 727 type tag struct { 728 name string 729 unit string // Describe the value, "" for non-numeric tags 730 value int64 731 weight int64 732 } 733 734 func (t tags) Len() int { return len(t) } 735 func (t tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 736 func (t tags) Less(i, j int) bool { 737 if t[i].weight == t[j].weight { 738 return t[i].name < t[j].name 739 } 740 return t[i].weight > t[j].weight 741 } 742 743 // node is an entry on a profiling report. It represents a unique 744 // program location. It can include multiple names to represent 745 // inlined functions. 746 type node struct { 747 info nodeInfo // Information associated to this entry. 748 749 // values associated to this node. 750 // flat is exclusive to this node, cum includes all descendents. 751 flat, cum int64 752 753 // in and out contains the nodes immediately reaching or reached by this nodes. 754 in, out edgeMap 755 756 // tags provide additional information about subsets of a sample. 757 tags tagMap 758 } 759 760 type nodeInfo struct { 761 name string 762 origName string 763 address uint64 764 file string 765 startLine, lineno int 766 inline bool 767 lowPriority bool 768 objfile string 769 parent *node // Used only if creating a calltree 770 } 771 772 func (n *node) addTags(s *profile.Sample, weight int64) { 773 // Add a tag with all string labels 774 var labels []string 775 for key, vals := range s.Label { 776 for _, v := range vals { 777 labels = append(labels, key+":"+v) 778 } 779 } 780 if len(labels) > 0 { 781 sort.Strings(labels) 782 l := n.tags.findOrAddTag(strings.Join(labels, `\n`), "", 0) 783 l.weight += weight 784 } 785 786 for key, nvals := range s.NumLabel { 787 for _, v := range nvals { 788 label := scaledValueLabel(v, key, "auto") 789 l := n.tags.findOrAddTag(label, key, v) 790 l.weight += weight 791 } 792 } 793 } 794 795 func (m tagMap) findOrAddTag(label, unit string, value int64) *tag { 796 if l := m[label]; l != nil { 797 return l 798 } 799 l := &tag{ 800 name: label, 801 unit: unit, 802 value: value, 803 } 804 m[label] = l 805 return l 806 } 807 808 // collapseTags reduces the number of entries in a tagMap by merging 809 // adjacent nodes into ranges. It uses a greedy approach to merge 810 // starting with the entries with the lowest weight. 811 func collapseTags(ts tags, count int) tags { 812 if len(ts) <= count { 813 return ts 814 } 815 816 sort.Sort(ts) 817 tagGroups := make([]tags, count) 818 for i, t := range ts[:count] { 819 tagGroups[i] = tags{t} 820 } 821 for _, t := range ts[count:] { 822 g, d := 0, tagDistance(t, tagGroups[0][0]) 823 for i := 1; i < count; i++ { 824 if nd := tagDistance(t, tagGroups[i][0]); nd < d { 825 g, d = i, nd 826 } 827 } 828 tagGroups[g] = append(tagGroups[g], t) 829 } 830 831 var nts tags 832 for _, g := range tagGroups { 833 l, w := tagGroupLabel(g) 834 nts = append(nts, &tag{ 835 name: l, 836 weight: w, 837 }) 838 } 839 return nts 840 } 841 842 func tagDistance(t, u *tag) float64 { 843 v, _ := ScaleValue(u.value, u.unit, t.unit) 844 if v < float64(t.value) { 845 return float64(t.value) - v 846 } 847 return v - float64(t.value) 848 } 849 850 func tagGroupLabel(g tags) (string, int64) { 851 if len(g) == 1 { 852 t := g[0] 853 return scaledValueLabel(t.value, t.unit, "auto"), t.weight 854 } 855 min := g[0] 856 max := g[0] 857 w := min.weight 858 for _, t := range g[1:] { 859 if v, _ := ScaleValue(t.value, t.unit, min.unit); int64(v) < min.value { 860 min = t 861 } 862 if v, _ := ScaleValue(t.value, t.unit, max.unit); int64(v) > max.value { 863 max = t 864 } 865 w += t.weight 866 } 867 return scaledValueLabel(min.value, min.unit, "auto") + ".." + 868 scaledValueLabel(max.value, max.unit, "auto"), w 869 } 870 871 // sumNodes adds the flat and sum values on a report. 872 func sumNodes(ns nodes) (flat int64, cum int64) { 873 for _, n := range ns { 874 flat += n.flat 875 cum += n.cum 876 } 877 return 878 } 879 880 type edgeMap map[*node]*edgeInfo 881 882 // edgeInfo contains any attributes to be represented about edges in a graph/ 883 type edgeInfo struct { 884 src, dest *node 885 // The summary weight of the edge 886 weight int64 887 // residual edges connect nodes that were connected through a 888 // separate node, which has been removed from the report. 889 residual bool 890 } 891 892 // bumpWeight increases the weight of an edge. If there isn't such an 893 // edge in the map one is created. 894 func bumpWeight(from, to *node, w int64, residual bool) { 895 if from.out[to] != to.in[from] { 896 panic(fmt.Errorf("asymmetric edges %v %v", *from, *to)) 897 } 898 899 if n := from.out[to]; n != nil { 900 n.weight += w 901 if n.residual && !residual { 902 n.residual = false 903 } 904 return 905 } 906 907 info := &edgeInfo{src: from, dest: to, weight: w, residual: residual} 908 from.out[to] = info 909 to.in[from] = info 910 } 911 912 // Output formats. 913 const ( 914 Proto = iota 915 Dot 916 Tags 917 Tree 918 Text 919 Raw 920 Dis 921 List 922 WebList 923 Callgrind 924 ) 925 926 // Options are the formatting and filtering options used to generate a 927 // profile. 928 type Options struct { 929 OutputFormat int 930 931 CumSort bool 932 CallTree bool 933 PrintAddresses bool 934 DropNegative bool 935 Ratio float64 936 937 NodeCount int 938 NodeFraction float64 939 EdgeFraction float64 940 941 SampleType string 942 SampleUnit string // Unit for the sample data from the profile. 943 OutputUnit string // Units for data formatting in report. 944 945 Symbol *regexp.Regexp // Symbols to include on disassembly report. 946 } 947 948 // newGraph summarizes performance data from a profile into a graph. 949 func newGraph(rpt *Report) (g graph, err error) { 950 prof := rpt.prof 951 o := rpt.options 952 953 // Generate a tree for graphical output if requested. 954 buildTree := o.CallTree && o.OutputFormat == Dot 955 956 locations := make(map[uint64][]nodeInfo) 957 for _, l := range prof.Location { 958 locations[l.ID] = newLocInfo(l) 959 } 960 961 nm := make(nodeMap) 962 for _, sample := range prof.Sample { 963 if sample.Location == nil { 964 continue 965 } 966 967 // Construct list of node names for sample. 968 var stack []nodeInfo 969 for _, loc := range sample.Location { 970 id := loc.ID 971 stack = append(stack, locations[id]...) 972 } 973 974 // Upfront pass to update the parent chains, to prevent the 975 // merging of nodes with different parents. 976 if buildTree { 977 var nn *node 978 for i := len(stack); i > 0; i-- { 979 n := &stack[i-1] 980 n.parent = nn 981 nn = nm.findOrInsertNode(*n) 982 } 983 } 984 985 leaf := nm.findOrInsertNode(stack[0]) 986 weight := rpt.sampleValue(sample) 987 leaf.addTags(sample, weight) 988 989 // Aggregate counter data. 990 leaf.flat += weight 991 seen := make(map[*node]bool) 992 var nn *node 993 for _, s := range stack { 994 n := nm.findOrInsertNode(s) 995 if !seen[n] { 996 seen[n] = true 997 n.cum += weight 998 999 if nn != nil { 1000 bumpWeight(n, nn, weight, false) 1001 } 1002 } 1003 nn = n 1004 } 1005 } 1006 1007 // Collect new nodes into a report. 1008 ns := make(nodes, 0, len(nm)) 1009 for _, n := range nm { 1010 if rpt.options.DropNegative && n.flat < 0 { 1011 continue 1012 } 1013 ns = append(ns, n) 1014 } 1015 1016 return graph{ns}, nil 1017 } 1018 1019 // Create a slice of formatted names for a location. 1020 func newLocInfo(l *profile.Location) []nodeInfo { 1021 var objfile string 1022 1023 if m := l.Mapping; m != nil { 1024 objfile = filepath.Base(m.File) 1025 } 1026 1027 if len(l.Line) == 0 { 1028 return []nodeInfo{ 1029 { 1030 address: l.Address, 1031 objfile: objfile, 1032 }, 1033 } 1034 } 1035 var info []nodeInfo 1036 numInlineFrames := len(l.Line) - 1 1037 for li, line := range l.Line { 1038 ni := nodeInfo{ 1039 address: l.Address, 1040 lineno: int(line.Line), 1041 inline: li < numInlineFrames, 1042 objfile: objfile, 1043 } 1044 1045 if line.Function != nil { 1046 ni.name = line.Function.Name 1047 ni.origName = line.Function.SystemName 1048 ni.file = line.Function.Filename 1049 ni.startLine = int(line.Function.StartLine) 1050 } 1051 1052 info = append(info, ni) 1053 } 1054 return info 1055 } 1056 1057 // nodeMap maps from a node info struct to a node. It is used to merge 1058 // report entries with the same info. 1059 type nodeMap map[nodeInfo]*node 1060 1061 func (m nodeMap) findOrInsertNode(info nodeInfo) *node { 1062 rr := m[info] 1063 if rr == nil { 1064 rr = &node{ 1065 info: info, 1066 in: make(edgeMap), 1067 out: make(edgeMap), 1068 tags: make(map[string]*tag), 1069 } 1070 m[info] = rr 1071 } 1072 return rr 1073 } 1074 1075 // preprocess does any required filtering/sorting according to the 1076 // report options. Returns the mapping from each node to any nodes 1077 // removed by path compression and statistics on the nodes/edges removed. 1078 func (g *graph) preprocess(rpt *Report) (origCount, droppedNodes, droppedEdges int) { 1079 o := rpt.options 1080 1081 // Compute total weight of current set of nodes. 1082 // This is <= rpt.total because of node filtering. 1083 var totalValue int64 1084 for _, n := range g.ns { 1085 totalValue += n.flat 1086 } 1087 1088 // Remove nodes with value <= total*nodeFraction 1089 if nodeFraction := o.NodeFraction; nodeFraction > 0 { 1090 var removed nodes 1091 minValue := int64(float64(totalValue) * nodeFraction) 1092 kept := make(nodes, 0, len(g.ns)) 1093 for _, n := range g.ns { 1094 if n.cum < minValue { 1095 removed = append(removed, n) 1096 } else { 1097 kept = append(kept, n) 1098 tagsKept := make(map[string]*tag) 1099 for s, t := range n.tags { 1100 if t.weight >= minValue { 1101 tagsKept[s] = t 1102 } 1103 } 1104 n.tags = tagsKept 1105 } 1106 } 1107 droppedNodes = len(removed) 1108 removeNodes(removed, false, false) 1109 g.ns = kept 1110 } 1111 1112 // Remove edges below minimum frequency. 1113 if edgeFraction := o.EdgeFraction; edgeFraction > 0 { 1114 minEdge := int64(float64(totalValue) * edgeFraction) 1115 for _, n := range g.ns { 1116 for src, e := range n.in { 1117 if e.weight < minEdge { 1118 delete(n.in, src) 1119 delete(src.out, n) 1120 droppedEdges++ 1121 } 1122 } 1123 } 1124 } 1125 1126 sortOrder := flatName 1127 if o.CumSort { 1128 // Force cum sorting for graph output, to preserve connectivity. 1129 sortOrder = cumName 1130 } 1131 1132 // Nodes that have flat==0 and a single in/out do not provide much 1133 // information. Give them first chance to be removed. Do not consider edges 1134 // from/to nodes that are expected to be removed. 1135 maxNodes := o.NodeCount 1136 if o.OutputFormat == Dot { 1137 if maxNodes > 0 && maxNodes < len(g.ns) { 1138 sortOrder = cumName 1139 g.ns.sort(cumName) 1140 cumCutoff := g.ns[maxNodes].cum 1141 for _, n := range g.ns { 1142 if n.flat == 0 { 1143 if count := countEdges(n.out, cumCutoff); count > 1 { 1144 continue 1145 } 1146 if count := countEdges(n.in, cumCutoff); count != 1 { 1147 continue 1148 } 1149 n.info.lowPriority = true 1150 } 1151 } 1152 } 1153 } 1154 1155 g.ns.sort(sortOrder) 1156 if maxNodes > 0 { 1157 origCount = len(g.ns) 1158 for index, nodes := 0, 0; index < len(g.ns); index++ { 1159 nodes++ 1160 // For DOT output, count the tags as nodes since we will draw 1161 // boxes for them. 1162 if o.OutputFormat == Dot { 1163 nodes += len(g.ns[index].tags) 1164 } 1165 if nodes > maxNodes { 1166 // Trim to the top n nodes. Create dotted edges to bridge any 1167 // broken connections. 1168 removeNodes(g.ns[index:], true, true) 1169 g.ns = g.ns[:index] 1170 break 1171 } 1172 } 1173 } 1174 removeRedundantEdges(g.ns) 1175 1176 // Select best unit for profile output. 1177 // Find the appropriate units for the smallest non-zero sample 1178 if o.OutputUnit == "minimum" && len(g.ns) > 0 { 1179 var maxValue, minValue int64 1180 1181 for _, n := range g.ns { 1182 if n.flat > 0 && (minValue == 0 || n.flat < minValue) { 1183 minValue = n.flat 1184 } 1185 if n.cum > maxValue { 1186 maxValue = n.cum 1187 } 1188 } 1189 if r := o.Ratio; r > 0 && r != 1 { 1190 minValue = int64(float64(minValue) * r) 1191 maxValue = int64(float64(maxValue) * r) 1192 } 1193 1194 _, minUnit := ScaleValue(minValue, o.SampleUnit, "minimum") 1195 _, maxUnit := ScaleValue(maxValue, o.SampleUnit, "minimum") 1196 1197 unit := minUnit 1198 if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind { 1199 // Minimum and maximum values have different units. Scale 1200 // minimum by 100 to use larger units, allowing minimum value to 1201 // be scaled down to 0.01, except for callgrind reports since 1202 // they can only represent integer values. 1203 _, unit = ScaleValue(100*minValue, o.SampleUnit, "minimum") 1204 } 1205 1206 if unit != "" { 1207 o.OutputUnit = unit 1208 } else { 1209 o.OutputUnit = o.SampleUnit 1210 } 1211 } 1212 return 1213 } 1214 1215 // countEdges counts the number of edges below the specified cutoff. 1216 func countEdges(el edgeMap, cutoff int64) int { 1217 count := 0 1218 for _, e := range el { 1219 if e.weight > cutoff { 1220 count++ 1221 } 1222 } 1223 return count 1224 } 1225 1226 // removeNodes removes nodes from a report, optionally bridging 1227 // connections between in/out edges and spreading out their weights 1228 // proportionally. residual marks new bridge edges as residual 1229 // (dotted). 1230 func removeNodes(toRemove nodes, bridge, residual bool) { 1231 for _, n := range toRemove { 1232 for ei := range n.in { 1233 delete(ei.out, n) 1234 } 1235 if bridge { 1236 for ei, wi := range n.in { 1237 for eo, wo := range n.out { 1238 var weight int64 1239 if n.cum != 0 { 1240 weight = int64(float64(wo.weight) * (float64(wi.weight) / float64(n.cum))) 1241 } 1242 bumpWeight(ei, eo, weight, residual) 1243 } 1244 } 1245 } 1246 for eo := range n.out { 1247 delete(eo.in, n) 1248 } 1249 } 1250 } 1251 1252 // removeRedundantEdges removes residual edges if the destination can 1253 // be reached through another path. This is done to simplify the graph 1254 // while preserving connectivity. 1255 func removeRedundantEdges(ns nodes) { 1256 // Walk the nodes and outgoing edges in reverse order to prefer 1257 // removing edges with the lowest weight. 1258 for i := len(ns); i > 0; i-- { 1259 n := ns[i-1] 1260 in := sortedEdges(n.in) 1261 for j := len(in); j > 0; j-- { 1262 if e := in[j-1]; e.residual && isRedundant(e) { 1263 delete(e.src.out, e.dest) 1264 delete(e.dest.in, e.src) 1265 } 1266 } 1267 } 1268 } 1269 1270 // isRedundant determines if an edge can be removed without impacting 1271 // connectivity of the whole graph. This is implemented by checking if the 1272 // nodes have a common ancestor after removing the edge. 1273 func isRedundant(e *edgeInfo) bool { 1274 destPred := predecessors(e, e.dest) 1275 if len(destPred) == 1 { 1276 return false 1277 } 1278 srcPred := predecessors(e, e.src) 1279 1280 for n := range srcPred { 1281 if destPred[n] && n != e.dest { 1282 return true 1283 } 1284 } 1285 return false 1286 } 1287 1288 // predecessors collects all the predecessors to node n, excluding edge e. 1289 func predecessors(e *edgeInfo, n *node) map[*node]bool { 1290 seen := map[*node]bool{n: true} 1291 queue := []*node{n} 1292 for len(queue) > 0 { 1293 n := queue[0] 1294 queue = queue[1:] 1295 for _, ie := range n.in { 1296 if e == ie || seen[ie.src] { 1297 continue 1298 } 1299 seen[ie.src] = true 1300 queue = append(queue, ie.src) 1301 } 1302 } 1303 return seen 1304 } 1305 1306 // nodeSorter is a mechanism used to allow a report to be sorted 1307 // in different ways. 1308 type nodeSorter struct { 1309 rs nodes 1310 less func(i, j int) bool 1311 } 1312 1313 func (s nodeSorter) Len() int { return len(s.rs) } 1314 func (s nodeSorter) Swap(i, j int) { s.rs[i], s.rs[j] = s.rs[j], s.rs[i] } 1315 func (s nodeSorter) Less(i, j int) bool { return s.less(i, j) } 1316 1317 type nodeOrder int 1318 1319 const ( 1320 flatName nodeOrder = iota 1321 flatCumName 1322 cumName 1323 nameOrder 1324 fileOrder 1325 addressOrder 1326 ) 1327 1328 // sort reorders the entries in a report based on the specified 1329 // ordering criteria. The result is sorted in decreasing order for 1330 // numeric quantities, alphabetically for text, and increasing for 1331 // addresses. 1332 func (ns nodes) sort(o nodeOrder) error { 1333 var s nodeSorter 1334 1335 switch o { 1336 case flatName: 1337 s = nodeSorter{ns, 1338 func(i, j int) bool { 1339 if iv, jv := ns[i].flat, ns[j].flat; iv != jv { 1340 return iv > jv 1341 } 1342 if ns[i].info.prettyName() != ns[j].info.prettyName() { 1343 return ns[i].info.prettyName() < ns[j].info.prettyName() 1344 } 1345 iv, jv := ns[i].cum, ns[j].cum 1346 return iv > jv 1347 }, 1348 } 1349 case flatCumName: 1350 s = nodeSorter{ns, 1351 func(i, j int) bool { 1352 if iv, jv := ns[i].flat, ns[j].flat; iv != jv { 1353 return iv > jv 1354 } 1355 if iv, jv := ns[i].cum, ns[j].cum; iv != jv { 1356 return iv > jv 1357 } 1358 return ns[i].info.prettyName() < ns[j].info.prettyName() 1359 }, 1360 } 1361 case cumName: 1362 s = nodeSorter{ns, 1363 func(i, j int) bool { 1364 if ns[i].info.lowPriority != ns[j].info.lowPriority { 1365 return ns[j].info.lowPriority 1366 } 1367 if iv, jv := ns[i].cum, ns[j].cum; iv != jv { 1368 return iv > jv 1369 } 1370 if ns[i].info.prettyName() != ns[j].info.prettyName() { 1371 return ns[i].info.prettyName() < ns[j].info.prettyName() 1372 } 1373 iv, jv := ns[i].flat, ns[j].flat 1374 return iv > jv 1375 }, 1376 } 1377 case nameOrder: 1378 s = nodeSorter{ns, 1379 func(i, j int) bool { 1380 return ns[i].info.name < ns[j].info.name 1381 }, 1382 } 1383 case fileOrder: 1384 s = nodeSorter{ns, 1385 func(i, j int) bool { 1386 return ns[i].info.file < ns[j].info.file 1387 }, 1388 } 1389 case addressOrder: 1390 s = nodeSorter{ns, 1391 func(i, j int) bool { 1392 return ns[i].info.address < ns[j].info.address 1393 }, 1394 } 1395 default: 1396 return fmt.Errorf("report: unrecognized sort ordering: %d", o) 1397 } 1398 sort.Sort(s) 1399 return nil 1400 } 1401 1402 type edgeList []*edgeInfo 1403 1404 // sortedEdges return a slice of the edges in the map, sorted for 1405 // visualization. The sort order is first based on the edge weight 1406 // (higher-to-lower) and then by the node names to avoid flakiness. 1407 func sortedEdges(edges map[*node]*edgeInfo) edgeList { 1408 el := make(edgeList, 0, len(edges)) 1409 for _, w := range edges { 1410 el = append(el, w) 1411 } 1412 1413 sort.Sort(el) 1414 return el 1415 } 1416 1417 func (el edgeList) Len() int { 1418 return len(el) 1419 } 1420 1421 func (el edgeList) Less(i, j int) bool { 1422 if el[i].weight != el[j].weight { 1423 return el[i].weight > el[j].weight 1424 } 1425 1426 from1 := el[i].src.info.prettyName() 1427 from2 := el[j].src.info.prettyName() 1428 if from1 != from2 { 1429 return from1 < from2 1430 } 1431 1432 to1 := el[i].dest.info.prettyName() 1433 to2 := el[j].dest.info.prettyName() 1434 1435 return to1 < to2 1436 } 1437 1438 func (el edgeList) Swap(i, j int) { 1439 el[i], el[j] = el[j], el[i] 1440 } 1441 1442 func (el edgeList) sum() int64 { 1443 var ret int64 1444 for _, e := range el { 1445 ret += e.weight 1446 } 1447 return ret 1448 } 1449 1450 // ScaleValue reformats a value from a unit to a different unit. 1451 func ScaleValue(value int64, fromUnit, toUnit string) (sv float64, su string) { 1452 // Avoid infinite recursion on overflow. 1453 if value < 0 && -value > 0 { 1454 v, u := ScaleValue(-value, fromUnit, toUnit) 1455 return -v, u 1456 } 1457 if m, u, ok := memoryLabel(value, fromUnit, toUnit); ok { 1458 return m, u 1459 } 1460 if t, u, ok := timeLabel(value, fromUnit, toUnit); ok { 1461 return t, u 1462 } 1463 // Skip non-interesting units. 1464 switch toUnit { 1465 case "count", "sample", "unit", "minimum": 1466 return float64(value), "" 1467 default: 1468 return float64(value), toUnit 1469 } 1470 } 1471 1472 func scaledValueLabel(value int64, fromUnit, toUnit string) string { 1473 v, u := ScaleValue(value, fromUnit, toUnit) 1474 1475 sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00") 1476 if sv == "0" || sv == "-0" { 1477 return "0" 1478 } 1479 return sv + u 1480 } 1481 1482 func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { 1483 fromUnit = strings.TrimSuffix(strings.ToLower(fromUnit), "s") 1484 toUnit = strings.TrimSuffix(strings.ToLower(toUnit), "s") 1485 1486 switch fromUnit { 1487 case "byte", "b": 1488 case "kilobyte", "kb": 1489 value *= 1024 1490 case "megabyte", "mb": 1491 value *= 1024 * 1024 1492 case "gigabyte", "gb": 1493 value *= 1024 * 1024 * 1024 1494 default: 1495 return 0, "", false 1496 } 1497 1498 if toUnit == "minimum" || toUnit == "auto" { 1499 switch { 1500 case value < 1024: 1501 toUnit = "b" 1502 case value < 1024*1024: 1503 toUnit = "kb" 1504 case value < 1024*1024*1024: 1505 toUnit = "mb" 1506 default: 1507 toUnit = "gb" 1508 } 1509 } 1510 1511 var output float64 1512 switch toUnit { 1513 default: 1514 output, toUnit = float64(value), "B" 1515 case "kb", "kbyte", "kilobyte": 1516 output, toUnit = float64(value)/1024, "kB" 1517 case "mb", "mbyte", "megabyte": 1518 output, toUnit = float64(value)/(1024*1024), "MB" 1519 case "gb", "gbyte", "gigabyte": 1520 output, toUnit = float64(value)/(1024*1024*1024), "GB" 1521 } 1522 return output, toUnit, true 1523 } 1524 1525 func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) { 1526 fromUnit = strings.ToLower(fromUnit) 1527 if len(fromUnit) > 2 { 1528 fromUnit = strings.TrimSuffix(fromUnit, "s") 1529 } 1530 1531 toUnit = strings.ToLower(toUnit) 1532 if len(toUnit) > 2 { 1533 toUnit = strings.TrimSuffix(toUnit, "s") 1534 } 1535 1536 var d time.Duration 1537 switch fromUnit { 1538 case "nanosecond", "ns": 1539 d = time.Duration(value) * time.Nanosecond 1540 case "microsecond": 1541 d = time.Duration(value) * time.Microsecond 1542 case "millisecond", "ms": 1543 d = time.Duration(value) * time.Millisecond 1544 case "second", "sec": 1545 d = time.Duration(value) * time.Second 1546 case "cycle": 1547 return float64(value), "", true 1548 default: 1549 return 0, "", false 1550 } 1551 1552 if toUnit == "minimum" || toUnit == "auto" { 1553 switch { 1554 case d < 1*time.Microsecond: 1555 toUnit = "ns" 1556 case d < 1*time.Millisecond: 1557 toUnit = "us" 1558 case d < 1*time.Second: 1559 toUnit = "ms" 1560 case d < 1*time.Minute: 1561 toUnit = "sec" 1562 case d < 1*time.Hour: 1563 toUnit = "min" 1564 case d < 24*time.Hour: 1565 toUnit = "hour" 1566 case d < 15*24*time.Hour: 1567 toUnit = "day" 1568 case d < 120*24*time.Hour: 1569 toUnit = "week" 1570 default: 1571 toUnit = "year" 1572 } 1573 } 1574 1575 var output float64 1576 dd := float64(d) 1577 switch toUnit { 1578 case "ns", "nanosecond": 1579 output, toUnit = dd/float64(time.Nanosecond), "ns" 1580 case "us", "microsecond": 1581 output, toUnit = dd/float64(time.Microsecond), "us" 1582 case "ms", "millisecond": 1583 output, toUnit = dd/float64(time.Millisecond), "ms" 1584 case "min", "minute": 1585 output, toUnit = dd/float64(time.Minute), "mins" 1586 case "hour", "hr": 1587 output, toUnit = dd/float64(time.Hour), "hrs" 1588 case "day": 1589 output, toUnit = dd/float64(24*time.Hour), "days" 1590 case "week", "wk": 1591 output, toUnit = dd/float64(7*24*time.Hour), "wks" 1592 case "year", "yr": 1593 output, toUnit = dd/float64(365*7*24*time.Hour), "yrs" 1594 default: 1595 fallthrough 1596 case "sec", "second", "s": 1597 output, toUnit = dd/float64(time.Second), "s" 1598 } 1599 return output, toUnit, true 1600 } 1601 1602 // prettyName determines the printable name to be used for a node. 1603 func (info *nodeInfo) prettyName() string { 1604 var name string 1605 if info.address != 0 { 1606 name = fmt.Sprintf("%016x", info.address) 1607 } 1608 1609 if info.name != "" { 1610 name = name + " " + info.name 1611 } 1612 1613 if info.file != "" { 1614 name += " " + trimPath(info.file) 1615 if info.lineno != 0 { 1616 name += fmt.Sprintf(":%d", info.lineno) 1617 } 1618 } 1619 1620 if info.inline { 1621 name = name + " (inline)" 1622 } 1623 1624 if name = strings.TrimSpace(name); name == "" && info.objfile != "" { 1625 name = "[" + info.objfile + "]" 1626 } 1627 return name 1628 } 1629 1630 // New builds a new report indexing the sample values interpreting the 1631 // samples with the provided function. 1632 func New(prof *profile.Profile, options Options, value func(s *profile.Sample) int64, unit string) *Report { 1633 o := &options 1634 if o.SampleUnit == "" { 1635 o.SampleUnit = unit 1636 } 1637 format := func(v int64) string { 1638 if r := o.Ratio; r > 0 && r != 1 { 1639 fv := float64(v) * r 1640 v = int64(fv) 1641 } 1642 return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) 1643 } 1644 return &Report{prof, computeTotal(prof, value), o, value, format} 1645 } 1646 1647 // NewDefault builds a new report indexing the sample values with the 1648 // last value available. 1649 func NewDefault(prof *profile.Profile, options Options) *Report { 1650 index := len(prof.SampleType) - 1 1651 o := &options 1652 if o.SampleUnit == "" { 1653 o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) 1654 } 1655 value := func(s *profile.Sample) int64 { 1656 return s.Value[index] 1657 } 1658 format := func(v int64) string { 1659 if r := o.Ratio; r > 0 && r != 1 { 1660 fv := float64(v) * r 1661 v = int64(fv) 1662 } 1663 return scaledValueLabel(v, o.SampleUnit, o.OutputUnit) 1664 } 1665 return &Report{prof, computeTotal(prof, value), o, value, format} 1666 } 1667 1668 func computeTotal(prof *profile.Profile, value func(s *profile.Sample) int64) int64 { 1669 var ret int64 1670 for _, sample := range prof.Sample { 1671 ret += value(sample) 1672 } 1673 return ret 1674 } 1675 1676 // Report contains the data and associated routines to extract a 1677 // report from a profile. 1678 type Report struct { 1679 prof *profile.Profile 1680 total int64 1681 options *Options 1682 sampleValue func(*profile.Sample) int64 1683 formatValue func(int64) string 1684 }