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