github.com/consensys/gnark@v0.11.0/profile/internal/report/report.go (about) 1 // Copyright 2014 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package report summarizes a performance profile into a 16 // human-readable report. 17 package report 18 19 import ( 20 "fmt" 21 "io" 22 "path/filepath" 23 "regexp" 24 "sort" 25 "strings" 26 "text/tabwriter" 27 "time" 28 29 "github.com/consensys/gnark/profile/internal/graph" 30 "github.com/consensys/gnark/profile/internal/measurement" 31 "github.com/google/pprof/profile" 32 ) 33 34 // Output formats. 35 const ( 36 Callgrind = iota 37 Comments 38 Dis 39 Dot 40 List 41 Proto 42 Raw 43 Tags 44 Text 45 TopProto 46 Traces 47 Tree 48 WebList 49 ) 50 51 // Options are the formatting and filtering options used to generate a 52 // profile. 53 type Options struct { 54 OutputFormat int 55 56 CumSort bool 57 CallTree bool 58 DropNegative bool 59 CompactLabels bool 60 Ratio float64 61 Title string 62 ProfileLabels []string 63 ActiveFilters []string 64 NumLabelUnits map[string]string 65 66 NodeCount int 67 NodeFraction float64 68 EdgeFraction float64 69 70 SampleValue func(s []int64) int64 71 SampleMeanDivisor func(s []int64) int64 72 SampleType string 73 SampleUnit string // Unit for the sample data from the profile. 74 75 OutputUnit string // Units for data formatting in report. 76 77 Symbol *regexp.Regexp // Symbols to include on disassembly report. 78 SourcePath string // Search path for source files. 79 TrimPath string // Paths to trim from source file paths. 80 81 IntelSyntax bool // Whether or not to print assembly in Intel syntax. 82 } 83 84 // Generate generates a report as directed by the Report. 85 func Generate(w io.Writer, rpt *Report) error { 86 o := rpt.options 87 88 switch o.OutputFormat { 89 case Comments: 90 return printComments(w, rpt) 91 case Dot: 92 return printDOT(w, rpt) 93 case Tree: 94 return printTree(w, rpt) 95 case Text: 96 return printText(w, rpt) 97 case Traces: 98 return printTraces(w, rpt) 99 case Raw: 100 fmt.Fprint(w, rpt.prof.String()) 101 return nil 102 case Tags: 103 return printTags(w, rpt) 104 case Proto: 105 return printProto(w, rpt) 106 case TopProto: 107 return printTopProto(w, rpt) 108 case Callgrind: 109 return printCallgrind(w, rpt) 110 } 111 return fmt.Errorf("unexpected output format") 112 } 113 114 // newTrimmedGraph creates a graph for this report, trimmed according 115 // to the report options. 116 func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, droppedEdges int) { 117 o := rpt.options 118 119 // Build a graph and refine it. On each refinement step we must rebuild the graph from the samples, 120 // as the graph itself doesn't contain enough information to preserve full precision. 121 visualMode := o.OutputFormat == Dot 122 cumSort := o.CumSort 123 124 // The call_tree option is only honored when generating visual representations of the callgraph. 125 callTree := o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind) 126 127 // First step: Build complete graph to identify low frequency nodes, based on their cum weight. 128 g = rpt.newGraph(nil) 129 totalValue, _ := g.Nodes.Sum() 130 nodeCutoff := abs64(int64(float64(totalValue) * o.NodeFraction)) 131 edgeCutoff := abs64(int64(float64(totalValue) * o.EdgeFraction)) 132 133 // Filter out nodes with cum value below nodeCutoff. 134 if nodeCutoff > 0 { 135 if callTree { 136 if nodesKept := g.DiscardLowFrequencyNodePtrs(nodeCutoff); len(g.Nodes) != len(nodesKept) { 137 droppedNodes = len(g.Nodes) - len(nodesKept) 138 g.TrimTree(nodesKept) 139 } 140 } else { 141 if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept) { 142 droppedNodes = len(g.Nodes) - len(nodesKept) 143 g = rpt.newGraph(nodesKept) 144 } 145 } 146 } 147 origCount = len(g.Nodes) 148 149 // Second step: Limit the total number of nodes. Apply specialized heuristics to improve 150 // visualization when generating dot output. 151 g.SortNodes(cumSort, visualMode) 152 if nodeCount := o.NodeCount; nodeCount > 0 { 153 // Remove low frequency tags and edges as they affect selection. 154 g.TrimLowFrequencyTags(nodeCutoff) 155 g.TrimLowFrequencyEdges(edgeCutoff) 156 if callTree { 157 if nodesKept := g.SelectTopNodePtrs(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) { 158 g.TrimTree(nodesKept) 159 g.SortNodes(cumSort, visualMode) 160 } 161 } else { 162 if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) { 163 g = rpt.newGraph(nodesKept) 164 g.SortNodes(cumSort, visualMode) 165 } 166 } 167 } 168 169 // Final step: Filter out low frequency tags and edges, and remove redundant edges that clutter 170 // the graph. 171 g.TrimLowFrequencyTags(nodeCutoff) 172 droppedEdges = g.TrimLowFrequencyEdges(edgeCutoff) 173 if visualMode { 174 g.RemoveRedundantEdges() 175 } 176 return 177 } 178 179 func (rpt *Report) selectOutputUnit(g *graph.Graph) { 180 o := rpt.options 181 182 // Select best unit for profile output. 183 // Find the appropriate units for the smallest non-zero sample 184 if o.OutputUnit != "minimum" || len(g.Nodes) == 0 { 185 return 186 } 187 var minValue int64 188 189 for _, n := range g.Nodes { 190 nodeMin := abs64(n.FlatValue()) 191 if nodeMin == 0 { 192 nodeMin = abs64(n.CumValue()) 193 } 194 if nodeMin > 0 && (minValue == 0 || nodeMin < minValue) { 195 minValue = nodeMin 196 } 197 } 198 maxValue := rpt.total 199 if minValue == 0 { 200 minValue = maxValue 201 } 202 203 if r := o.Ratio; r > 0 && r != 1 { 204 minValue = int64(float64(minValue) * r) 205 maxValue = int64(float64(maxValue) * r) 206 } 207 208 _, minUnit := measurement.Scale(minValue, o.SampleUnit, "minimum") 209 _, maxUnit := measurement.Scale(maxValue, o.SampleUnit, "minimum") 210 211 unit := minUnit 212 if minUnit != maxUnit && minValue*100 < maxValue && o.OutputFormat != Callgrind { 213 // Minimum and maximum values have different units. Scale 214 // minimum by 100 to use larger units, allowing minimum value to 215 // be scaled down to 0.01, except for callgrind reports since 216 // they can only represent integer values. 217 _, unit = measurement.Scale(100*minValue, o.SampleUnit, "minimum") 218 } 219 220 if unit != "" { 221 o.OutputUnit = unit 222 } else { 223 o.OutputUnit = o.SampleUnit 224 } 225 } 226 227 // newGraph creates a new graph for this report. If nodes is non-nil, 228 // only nodes whose info matches are included. Otherwise, all nodes 229 // are included, without trimming. 230 func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { 231 o := rpt.options 232 233 // Clean up file paths using heuristics. 234 prof := rpt.prof 235 for _, f := range prof.Function { 236 f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath) 237 } 238 // Removes all numeric tags except for the bytes tag prior 239 // to making graph. 240 // TODO: modify to select first numeric tag if no bytes tag 241 for _, s := range prof.Sample { 242 numLabels := make(map[string][]int64, len(s.NumLabel)) 243 numUnits := make(map[string][]string, len(s.NumLabel)) 244 for k, vs := range s.NumLabel { 245 if k == "bytes" { 246 unit := o.NumLabelUnits[k] 247 numValues := make([]int64, len(vs)) 248 numUnit := make([]string, len(vs)) 249 for i, v := range vs { 250 numValues[i] = v 251 numUnit[i] = unit 252 } 253 numLabels[k] = append(numLabels[k], numValues...) 254 numUnits[k] = append(numUnits[k], numUnit...) 255 } 256 } 257 s.NumLabel = numLabels 258 s.NumUnit = numUnits 259 } 260 261 // Remove label marking samples from the base profiles, so it does not appear 262 // as a nodelet in the graph view. 263 prof.RemoveLabel("pprof::base") 264 265 formatTag := func(v int64, key string) string { 266 return measurement.ScaledLabel(v, key, o.OutputUnit) 267 } 268 269 gopt := &graph.Options{ 270 SampleValue: o.SampleValue, 271 SampleMeanDivisor: o.SampleMeanDivisor, 272 FormatTag: formatTag, 273 CallTree: o.CallTree && (o.OutputFormat == Dot || o.OutputFormat == Callgrind), 274 DropNegative: o.DropNegative, 275 KeptNodes: nodes, 276 } 277 278 // Only keep binary names for disassembly-based reports, otherwise 279 // remove it to allow merging of functions across binaries. 280 switch o.OutputFormat { 281 case Raw, List, WebList, Dis, Callgrind: 282 gopt.ObjNames = true 283 } 284 285 return graph.New(rpt.prof, gopt) 286 } 287 288 // printProto writes the incoming proto via the writer w. 289 // If the divide_by option has been specified, samples are scaled appropriately. 290 func printProto(w io.Writer, rpt *Report) error { 291 p, o := rpt.prof, rpt.options 292 293 // Apply the sample ratio to all samples before saving the profile. 294 if r := o.Ratio; r > 0 && r != 1 { 295 for _, sample := range p.Sample { 296 for i, v := range sample.Value { 297 sample.Value[i] = int64(float64(v) * r) 298 } 299 } 300 } 301 return p.Write(w) 302 } 303 304 // printTopProto writes a list of the hottest routines in a profile as a profile.proto. 305 func printTopProto(w io.Writer, rpt *Report) error { 306 p := rpt.prof 307 o := rpt.options 308 g, _, _, _ := rpt.newTrimmedGraph() 309 rpt.selectOutputUnit(g) 310 311 out := profile.Profile{ 312 SampleType: []*profile.ValueType{ 313 {Type: "cum", Unit: o.OutputUnit}, 314 {Type: "flat", Unit: o.OutputUnit}, 315 }, 316 TimeNanos: p.TimeNanos, 317 DurationNanos: p.DurationNanos, 318 PeriodType: p.PeriodType, 319 Period: p.Period, 320 } 321 functionMap := make(functionMap) 322 for i, n := range g.Nodes { 323 f, added := functionMap.findOrAdd(n.Info) 324 if added { 325 out.Function = append(out.Function, f) 326 } 327 flat, cum := n.FlatValue(), n.CumValue() 328 l := &profile.Location{ 329 ID: uint64(i + 1), 330 Address: n.Info.Address, 331 Line: []profile.Line{ 332 { 333 Line: int64(n.Info.Lineno), 334 Function: f, 335 }, 336 }, 337 } 338 339 fv, _ := measurement.Scale(flat, o.SampleUnit, o.OutputUnit) 340 cv, _ := measurement.Scale(cum, o.SampleUnit, o.OutputUnit) 341 s := &profile.Sample{ 342 Location: []*profile.Location{l}, 343 Value: []int64{int64(cv), int64(fv)}, 344 } 345 out.Location = append(out.Location, l) 346 out.Sample = append(out.Sample, s) 347 } 348 349 return out.Write(w) 350 } 351 352 type functionMap map[string]*profile.Function 353 354 // findOrAdd takes a node representing a function, adds the function 355 // represented by the node to the map if the function is not already present, 356 // and returns the function the node represents. This also returns a boolean, 357 // which is true if the function was added and false otherwise. 358 func (fm functionMap) findOrAdd(ni graph.NodeInfo) (*profile.Function, bool) { 359 fName := fmt.Sprintf("%q%q%q%d", ni.Name, ni.OrigName, ni.File, ni.StartLine) 360 361 if f := fm[fName]; f != nil { 362 return f, false 363 } 364 365 f := &profile.Function{ 366 ID: uint64(len(fm) + 1), 367 Name: ni.Name, 368 SystemName: ni.OrigName, 369 Filename: ni.File, 370 StartLine: int64(ni.StartLine), 371 } 372 fm[fName] = f 373 return f, true 374 } 375 376 // printTags collects all tags referenced in the profile and prints 377 // them in a sorted table. 378 func printTags(w io.Writer, rpt *Report) error { 379 p := rpt.prof 380 381 o := rpt.options 382 formatTag := func(v int64, key string) string { 383 return measurement.ScaledLabel(v, key, o.OutputUnit) 384 } 385 386 // Hashtable to keep accumulate tags as key,value,count. 387 tagMap := make(map[string]map[string]int64) 388 for _, s := range p.Sample { 389 for key, vals := range s.Label { 390 for _, val := range vals { 391 valueMap, ok := tagMap[key] 392 if !ok { 393 valueMap = make(map[string]int64) 394 tagMap[key] = valueMap 395 } 396 valueMap[val] += o.SampleValue(s.Value) 397 } 398 } 399 for key, vals := range s.NumLabel { 400 unit := o.NumLabelUnits[key] 401 for _, nval := range vals { 402 val := formatTag(nval, unit) 403 valueMap, ok := tagMap[key] 404 if !ok { 405 valueMap = make(map[string]int64) 406 tagMap[key] = valueMap 407 } 408 valueMap[val] += o.SampleValue(s.Value) 409 } 410 } 411 } 412 413 tagKeys := make([]*graph.Tag, 0, len(tagMap)) 414 for key := range tagMap { 415 tagKeys = append(tagKeys, &graph.Tag{Name: key}) 416 } 417 tabw := tabwriter.NewWriter(w, 0, 0, 1, ' ', tabwriter.AlignRight) 418 for _, tagKey := range graph.SortTags(tagKeys, true) { 419 var total int64 420 key := tagKey.Name 421 tags := make([]*graph.Tag, 0, len(tagMap[key])) 422 for t, c := range tagMap[key] { 423 total += c 424 tags = append(tags, &graph.Tag{Name: t, Flat: c}) 425 } 426 427 f, u := measurement.Scale(total, o.SampleUnit, o.OutputUnit) 428 fmt.Fprintf(tabw, "%s:\t Total %.1f%s\n", key, f, u) 429 for _, t := range graph.SortTags(tags, true) { 430 f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit) 431 if total > 0 { 432 fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name) 433 } else { 434 fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name) 435 } 436 } 437 fmt.Fprintln(tabw) 438 } 439 return tabw.Flush() 440 } 441 442 // printComments prints all freeform comments in the profile. 443 func printComments(w io.Writer, rpt *Report) error { 444 p := rpt.prof 445 446 for _, c := range p.Comments { 447 fmt.Fprintln(w, c) 448 } 449 return nil 450 } 451 452 // TextItem holds a single text report entry. 453 type TextItem struct { 454 Name string 455 InlineLabel string // Not empty if inlined 456 Flat, Cum int64 // Raw values 457 FlatFormat, CumFormat string // Formatted values 458 } 459 460 // TextItems returns a list of text items from the report and a list 461 // of labels that describe the report. 462 func TextItems(rpt *Report) ([]TextItem, []string) { 463 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 464 rpt.selectOutputUnit(g) 465 labels := reportLabels(rpt, g, origCount, droppedNodes, 0, false) 466 467 var items []TextItem 468 var flatSum int64 469 for _, n := range g.Nodes { 470 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 471 472 var inline, noinline bool 473 for _, e := range n.In { 474 if e.Inline { 475 inline = true 476 } else { 477 noinline = true 478 } 479 } 480 481 var inl string 482 if inline { 483 if noinline { 484 inl = "(partial-inline)" 485 } else { 486 inl = "(inline)" 487 } 488 } 489 490 flatSum += flat 491 items = append(items, TextItem{ 492 Name: name, 493 InlineLabel: inl, 494 Flat: flat, 495 Cum: cum, 496 FlatFormat: rpt.formatValue(flat), 497 CumFormat: rpt.formatValue(cum), 498 }) 499 } 500 return items, labels 501 } 502 503 // printText prints a flat text report for a profile. 504 func printText(w io.Writer, rpt *Report) error { 505 items, labels := TextItems(rpt) 506 fmt.Fprintln(w, strings.Join(labels, "\n")) 507 fmt.Fprintf(w, "%10s %5s%% %5s%% %10s %5s%%\n", 508 "flat", "flat", "sum", "cum", "cum") 509 var flatSum int64 510 for _, item := range items { 511 inl := item.InlineLabel 512 if inl != "" { 513 inl = " " + inl 514 } 515 flatSum += item.Flat 516 fmt.Fprintf(w, "%10s %s %s %10s %s %s%s\n", 517 item.FlatFormat, measurement.Percentage(item.Flat, rpt.total), 518 measurement.Percentage(flatSum, rpt.total), 519 item.CumFormat, measurement.Percentage(item.Cum, rpt.total), 520 item.Name, inl) 521 } 522 return nil 523 } 524 525 // printTraces prints all traces from a profile. 526 func printTraces(w io.Writer, rpt *Report) error { 527 fmt.Fprintln(w, strings.Join(ProfileLabels(rpt), "\n")) 528 529 prof := rpt.prof 530 o := rpt.options 531 532 const separator = "-----------+-------------------------------------------------------" 533 534 _, locations := graph.CreateNodes(prof, &graph.Options{}) 535 for _, sample := range prof.Sample { 536 type stk struct { 537 *graph.NodeInfo 538 inline bool 539 } 540 var stack []stk 541 for _, loc := range sample.Location { 542 nodes := locations[loc.ID] 543 for i, n := range nodes { 544 // The inline flag may be inaccurate if 'show' or 'hide' filter is 545 // used. See https://github.com/google/pprof/issues/511. 546 inline := i != len(nodes)-1 547 stack = append(stack, stk{&n.Info, inline}) // #nosec G601 false positive 548 } 549 } 550 551 if len(stack) == 0 { 552 continue 553 } 554 555 fmt.Fprintln(w, separator) 556 // Print any text labels for the sample. 557 var labels []string 558 for s, vs := range sample.Label { 559 labels = append(labels, fmt.Sprintf("%10s: %s\n", s, strings.Join(vs, " "))) 560 } 561 sort.Strings(labels) 562 fmt.Fprint(w, strings.Join(labels, "")) 563 564 // Print any numeric labels for the sample 565 var numLabels []string 566 for key, vals := range sample.NumLabel { 567 unit := o.NumLabelUnits[key] 568 numValues := make([]string, len(vals)) 569 for i, vv := range vals { 570 numValues[i] = measurement.Label(vv, unit) 571 } 572 numLabels = append(numLabels, fmt.Sprintf("%10s: %s\n", key, strings.Join(numValues, " "))) 573 } 574 sort.Strings(numLabels) 575 fmt.Fprint(w, strings.Join(numLabels, "")) 576 577 var d, v int64 578 v = o.SampleValue(sample.Value) 579 if o.SampleMeanDivisor != nil { 580 d = o.SampleMeanDivisor(sample.Value) 581 } 582 // Print call stack. 583 if d != 0 { 584 v = v / d 585 } 586 for i, s := range stack { 587 var vs, inline string 588 if i == 0 { 589 vs = rpt.formatValue(v) 590 } 591 if s.inline { 592 inline = " (inline)" 593 } 594 fmt.Fprintf(w, "%10s %s%s\n", vs, s.PrintableName(), inline) 595 } 596 } 597 fmt.Fprintln(w, separator) 598 return nil 599 } 600 601 // printCallgrind prints a graph for a profile on callgrind format. 602 func printCallgrind(w io.Writer, rpt *Report) error { 603 o := rpt.options 604 rpt.options.NodeFraction = 0 605 rpt.options.EdgeFraction = 0 606 rpt.options.NodeCount = 0 607 608 g, _, _, _ := rpt.newTrimmedGraph() 609 rpt.selectOutputUnit(g) 610 611 nodeNames := getDisambiguatedNames(g) 612 613 fmt.Fprintln(w, "positions: instr line") 614 fmt.Fprintln(w, "events:", o.SampleType+"("+o.OutputUnit+")") 615 616 objfiles := make(map[string]int) 617 files := make(map[string]int) 618 names := make(map[string]int) 619 620 // prevInfo points to the previous NodeInfo. 621 // It is used to group cost lines together as much as possible. 622 var prevInfo *graph.NodeInfo 623 for _, n := range g.Nodes { 624 if prevInfo == nil || n.Info.Objfile != prevInfo.Objfile || n.Info.File != prevInfo.File || n.Info.Name != prevInfo.Name { 625 fmt.Fprintln(w) 626 fmt.Fprintln(w, "ob="+callgrindName(objfiles, n.Info.Objfile)) 627 fmt.Fprintln(w, "fl="+callgrindName(files, n.Info.File)) 628 fmt.Fprintln(w, "fn="+callgrindName(names, n.Info.Name)) 629 } 630 631 addr := callgrindAddress(prevInfo, n.Info.Address) 632 sv, _ := measurement.Scale(n.FlatValue(), o.SampleUnit, o.OutputUnit) 633 fmt.Fprintf(w, "%s %d %d\n", addr, n.Info.Lineno, int64(sv)) 634 635 // Print outgoing edges. 636 for _, out := range n.Out.Sort() { 637 c, _ := measurement.Scale(out.Weight, o.SampleUnit, o.OutputUnit) 638 callee := out.Dest 639 fmt.Fprintln(w, "cfl="+callgrindName(files, callee.Info.File)) 640 fmt.Fprintln(w, "cfn="+callgrindName(names, nodeNames[callee])) 641 // pprof doesn't have a flat weight for a call, leave as 0. 642 fmt.Fprintf(w, "calls=0 %s %d\n", callgrindAddress(prevInfo, callee.Info.Address), callee.Info.Lineno) 643 // TODO: This address may be in the middle of a call 644 // instruction. It would be best to find the beginning 645 // of the instruction, but the tools seem to handle 646 // this OK. 647 fmt.Fprintf(w, "* * %d\n", int64(c)) 648 } 649 650 prevInfo = &n.Info //#nosec G601 false positive 651 } 652 653 return nil 654 } 655 656 // getDisambiguatedNames returns a map from each node in the graph to 657 // the name to use in the callgrind output. Callgrind merges all 658 // functions with the same [file name, function name]. Add a [%d/n] 659 // suffix to disambiguate nodes with different values of 660 // node.Function, which we want to keep separate. In particular, this 661 // affects graphs created with --call_tree, where nodes from different 662 // contexts are associated to different Functions. 663 func getDisambiguatedNames(g *graph.Graph) map[*graph.Node]string { 664 nodeName := make(map[*graph.Node]string, len(g.Nodes)) 665 666 type names struct { 667 file, function string 668 } 669 670 // nameFunctionIndex maps the callgrind names (filename, function) 671 // to the node.Function values found for that name, and each 672 // node.Function value to a sequential index to be used on the 673 // disambiguated name. 674 nameFunctionIndex := make(map[names]map[*graph.Node]int) 675 for _, n := range g.Nodes { 676 nm := names{n.Info.File, n.Info.Name} 677 p, ok := nameFunctionIndex[nm] 678 if !ok { 679 p = make(map[*graph.Node]int) 680 nameFunctionIndex[nm] = p 681 } 682 if _, ok := p[n.Function]; !ok { 683 p[n.Function] = len(p) 684 } 685 } 686 687 for _, n := range g.Nodes { 688 nm := names{n.Info.File, n.Info.Name} 689 nodeName[n] = n.Info.Name 690 if p := nameFunctionIndex[nm]; len(p) > 1 { 691 // If there is more than one function, add suffix to disambiguate. 692 nodeName[n] += fmt.Sprintf(" [%d/%d]", p[n.Function]+1, len(p)) 693 } 694 } 695 return nodeName 696 } 697 698 // callgrindName implements the callgrind naming compression scheme. 699 // For names not previously seen returns "(N) name", where N is a 700 // unique index. For names previously seen returns "(N)" where N is 701 // the index returned the first time. 702 func callgrindName(names map[string]int, name string) string { 703 if name == "" { 704 return "" 705 } 706 if id, ok := names[name]; ok { 707 return fmt.Sprintf("(%d)", id) 708 } 709 id := len(names) + 1 710 names[name] = id 711 return fmt.Sprintf("(%d) %s", id, name) 712 } 713 714 // callgrindAddress implements the callgrind subposition compression scheme if 715 // possible. If prevInfo != nil, it contains the previous address. The current 716 // address can be given relative to the previous address, with an explicit +/- 717 // to indicate it is relative, or * for the same address. 718 func callgrindAddress(prevInfo *graph.NodeInfo, curr uint64) string { 719 abs := fmt.Sprintf("%#x", curr) 720 if prevInfo == nil { 721 return abs 722 } 723 724 prev := prevInfo.Address 725 if prev == curr { 726 return "*" 727 } 728 729 diff := int64(curr - prev) 730 relative := fmt.Sprintf("%+d", diff) 731 732 // Only bother to use the relative address if it is actually shorter. 733 if len(relative) < len(abs) { 734 return relative 735 } 736 737 return abs 738 } 739 740 // printTree prints a tree-based report in text form. 741 func printTree(w io.Writer, rpt *Report) error { 742 const separator = "----------------------------------------------------------+-------------" 743 const legend = " flat flat% sum% cum cum% calls calls% + context " 744 745 g, origCount, droppedNodes, _ := rpt.newTrimmedGraph() 746 rpt.selectOutputUnit(g) 747 748 fmt.Fprintln(w, strings.Join(reportLabels(rpt, g, origCount, droppedNodes, 0, false), "\n")) 749 750 fmt.Fprintln(w, separator) 751 fmt.Fprintln(w, legend) 752 var flatSum int64 753 754 rx := rpt.options.Symbol 755 matched := 0 756 for _, n := range g.Nodes { 757 name, flat, cum := n.Info.PrintableName(), n.FlatValue(), n.CumValue() 758 759 // Skip any entries that do not match the regexp (for the "peek" command). 760 if rx != nil && !rx.MatchString(name) { 761 continue 762 } 763 matched++ 764 765 fmt.Fprintln(w, separator) 766 // Print incoming edges. 767 inEdges := n.In.Sort() 768 for _, in := range inEdges { 769 var inline string 770 if in.Inline { 771 inline = " (inline)" 772 } 773 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(in.Weight), 774 measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline) 775 } 776 777 // Print current node. 778 flatSum += flat 779 fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", 780 rpt.formatValue(flat), 781 measurement.Percentage(flat, rpt.total), 782 measurement.Percentage(flatSum, rpt.total), 783 rpt.formatValue(cum), 784 measurement.Percentage(cum, rpt.total), 785 name) 786 787 // Print outgoing edges. 788 outEdges := n.Out.Sort() 789 for _, out := range outEdges { 790 var inline string 791 if out.Inline { 792 inline = " (inline)" 793 } 794 fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(out.Weight), 795 measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline) 796 } 797 } 798 if len(g.Nodes) > 0 { 799 fmt.Fprintln(w, separator) 800 } 801 if rx != nil && matched == 0 { 802 return fmt.Errorf("no matches found for regexp: %s", rx) 803 } 804 return nil 805 } 806 807 // GetDOT returns a graph suitable for dot processing along with some 808 // configuration information. 809 func GetDOT(rpt *Report) (*graph.Graph, *graph.DotConfig) { 810 g, origCount, droppedNodes, droppedEdges := rpt.newTrimmedGraph() 811 rpt.selectOutputUnit(g) 812 labels := reportLabels(rpt, g, origCount, droppedNodes, droppedEdges, true) 813 814 c := &graph.DotConfig{ 815 Title: rpt.options.Title, 816 Labels: labels, 817 FormatValue: rpt.formatValue, 818 Total: rpt.total, 819 } 820 return g, c 821 } 822 823 // printDOT prints an annotated callgraph in DOT format. 824 func printDOT(w io.Writer, rpt *Report) error { 825 g, c := GetDOT(rpt) 826 graph.ComposeDot(w, g, &graph.DotAttributes{}, c) 827 return nil 828 } 829 830 // ProfileLabels returns printable labels for a profile. 831 func ProfileLabels(rpt *Report) []string { 832 label := []string{} 833 prof := rpt.prof 834 o := rpt.options 835 if len(prof.Mapping) > 0 { 836 if prof.Mapping[0].File != "" { 837 label = append(label, "File: "+filepath.Base(prof.Mapping[0].File)) 838 } 839 if prof.Mapping[0].BuildID != "" { 840 label = append(label, "Build ID: "+prof.Mapping[0].BuildID) 841 } 842 } 843 // Only include comments that do not start with '#'. 844 for _, c := range prof.Comments { 845 if !strings.HasPrefix(c, "#") { 846 label = append(label, c) 847 } 848 } 849 if o.SampleType != "" { 850 label = append(label, "Type: "+o.SampleType) 851 } 852 if prof.TimeNanos != 0 { 853 const layout = "Jan 2, 2006 at 3:04pm (MST)" 854 label = append(label, "Time: "+time.Unix(0, prof.TimeNanos).Format(layout)) 855 } 856 if prof.DurationNanos != 0 { 857 duration := measurement.Label(prof.DurationNanos, "nanoseconds") 858 totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds") 859 var ratio string 860 if totalUnit == "ns" && totalNanos != 0 { 861 ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")" 862 } 863 label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio)) 864 } 865 return label 866 } 867 868 // reportLabels returns printable labels for a report. Includes 869 // profileLabels. 870 func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedEdges int, fullHeaders bool) []string { 871 nodeFraction := rpt.options.NodeFraction 872 edgeFraction := rpt.options.EdgeFraction 873 nodeCount := len(g.Nodes) 874 875 var label []string 876 if len(rpt.options.ProfileLabels) > 0 { 877 label = append(label, rpt.options.ProfileLabels...) 878 } else if fullHeaders || !rpt.options.CompactLabels { 879 label = ProfileLabels(rpt) 880 } 881 882 var flatSum int64 883 for _, n := range g.Nodes { 884 flatSum = flatSum + n.FlatValue() 885 } 886 887 if len(rpt.options.ActiveFilters) > 0 { 888 activeFilters := legendActiveFilters(rpt.options.ActiveFilters) 889 label = append(label, activeFilters...) 890 } 891 892 label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(measurement.Percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total))) 893 894 if rpt.total != 0 { 895 if droppedNodes > 0 { 896 label = append(label, genLabel(droppedNodes, "node", "cum", 897 rpt.formatValue(abs64(int64(float64(rpt.total)*nodeFraction))))) 898 } 899 if droppedEdges > 0 { 900 label = append(label, genLabel(droppedEdges, "edge", "freq", 901 rpt.formatValue(abs64(int64(float64(rpt.total)*edgeFraction))))) 902 } 903 if nodeCount > 0 && nodeCount < origCount { 904 label = append(label, fmt.Sprintf("Showing top %d nodes out of %d", 905 nodeCount, origCount)) 906 } 907 } 908 909 // Help new users understand the graph. 910 // A new line is intentionally added here to better show this message. 911 if fullHeaders { 912 label = append(label, "\nSee https://git.io/JfYMW for how to read the graph") 913 } 914 915 return label 916 } 917 918 func legendActiveFilters(activeFilters []string) []string { 919 legendActiveFilters := make([]string, len(activeFilters)+1) 920 legendActiveFilters[0] = "Active filters:" 921 for i, s := range activeFilters { 922 if len(s) > 80 { 923 s = s[:80] + "…" 924 } 925 legendActiveFilters[i+1] = " " + s 926 } 927 return legendActiveFilters 928 } 929 930 func genLabel(d int, n, l, f string) string { 931 if d > 1 { 932 n = n + "s" 933 } 934 return fmt.Sprintf("Dropped %d %s (%s <= %s)", d, n, l, f) 935 } 936 937 // New builds a new report indexing the sample values interpreting the 938 // samples with the provided function. 939 func New(prof *profile.Profile, o *Options) *Report { 940 format := func(v int64) string { 941 if r := o.Ratio; r > 0 && r != 1 { 942 fv := float64(v) * r 943 v = int64(fv) 944 } 945 return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit) 946 } 947 return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor), 948 o, format} 949 } 950 951 // NewDefault builds a new report indexing the last sample value 952 // available. 953 func NewDefault(prof *profile.Profile, options Options) *Report { 954 index := len(prof.SampleType) - 1 955 o := &options 956 if o.Title == "" && len(prof.Mapping) > 0 && prof.Mapping[0].File != "" { 957 o.Title = filepath.Base(prof.Mapping[0].File) 958 } 959 o.SampleType = prof.SampleType[index].Type 960 o.SampleUnit = strings.ToLower(prof.SampleType[index].Unit) 961 o.SampleValue = func(v []int64) int64 { 962 return v[index] 963 } 964 return New(prof, o) 965 } 966 967 // computeTotal computes the sum of the absolute value of all sample values. 968 // If any samples have label indicating they belong to the diff base, then the 969 // total will only include samples with that label. 970 func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 { 971 var div, total, diffDiv, diffTotal int64 972 for _, sample := range prof.Sample { 973 var d, v int64 974 v = value(sample.Value) 975 if meanDiv != nil { 976 d = meanDiv(sample.Value) 977 } 978 if v < 0 { 979 v = -v 980 } 981 total += v 982 div += d 983 if sample.DiffBaseSample() { 984 diffTotal += v 985 diffDiv += d 986 } 987 } 988 if diffTotal > 0 { 989 total = diffTotal 990 div = diffDiv 991 } 992 if div != 0 { 993 return total / div 994 } 995 return total 996 } 997 998 // Report contains the data and associated routines to extract a 999 // report from a profile. 1000 type Report struct { 1001 prof *profile.Profile 1002 total int64 1003 options *Options 1004 formatValue func(int64) string 1005 } 1006 1007 // Total returns the total number of samples in a report. 1008 func (rpt *Report) Total() int64 { return rpt.total } 1009 1010 func abs64(i int64) int64 { 1011 if i < 0 { 1012 return -i 1013 } 1014 return i 1015 } 1016 1017 func trimPath(path, trimPath, searchPath string) string { 1018 const gnarkCIRoot = "/gnark/gnark/" 1019 const gnarkRoot = "/gnark/" 1020 1021 // Keep path variable intact as it's used below to form the return value. 1022 path, searchPath = filepath.ToSlash(path), filepath.ToSlash(searchPath) 1023 1024 if idx := strings.Index(path, gnarkCIRoot); idx != -1 { 1025 path = path[idx+len(gnarkCIRoot):] 1026 return path 1027 } 1028 1029 if idx := strings.Index(path, gnarkRoot); idx != -1 { 1030 path = path[idx+len(gnarkRoot):] 1031 return path 1032 } 1033 1034 if trimPath == "" { 1035 // If the trim path is not configured, try to guess it heuristically: 1036 // search for basename of each search path in the original path and, if 1037 // found, strip everything up to and including the basename. So, for 1038 // example, given original path "/some/remote/path/my-project/foo/bar.c" 1039 // and search path "/my/local/path/my-project" the heuristic will return 1040 // "/my/local/path/my-project/foo/bar.c". 1041 for _, dir := range filepath.SplitList(searchPath) { 1042 want := "/" + filepath.Base(dir) + "/" 1043 if found := strings.Index(path, want); found != -1 { 1044 return path[found+len(want):] 1045 } 1046 } 1047 } 1048 // Trim configured trim prefixes. 1049 trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/") 1050 for _, trimPath := range trimPaths { 1051 if !strings.HasSuffix(trimPath, "/") { 1052 trimPath += "/" 1053 } 1054 if strings.HasPrefix(path, trimPath) { 1055 return path[len(trimPath):] 1056 } 1057 } 1058 return path 1059 }