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  }