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