github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/pgo/internal/graph/graph.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 graph represents a pprof profile as a directed graph.
    16  //
    17  // This package is a simplified fork of github.com/google/pprof/github.com/go-asm/go/graph.
    18  package graph
    19  
    20  import (
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/go-asm/go/profile"
    26  )
    27  
    28  // Options encodes the options for constructing a graph
    29  type Options struct {
    30  	SampleValue       func(s []int64) int64 // Function to compute the value of a sample
    31  	SampleMeanDivisor func(s []int64) int64 // Function to compute the divisor for mean graphs, or nil
    32  
    33  	DropNegative bool // Drop nodes with overall negative values
    34  
    35  	KeptNodes NodeSet // If non-nil, only use nodes in this set
    36  }
    37  
    38  // Nodes is an ordered collection of graph nodes.
    39  type Nodes []*Node
    40  
    41  // Node is an entry on a profiling report. It represents a unique
    42  // program location.
    43  type Node struct {
    44  	// Info describes the source location associated to this node.
    45  	Info NodeInfo
    46  
    47  	// Function represents the function that this node belongs to. On
    48  	// graphs with sub-function resolution (eg line number or
    49  	// addresses), two nodes in a NodeMap that are part of the same
    50  	// function have the same value of Node.Function. If the Node
    51  	// represents the whole function, it points back to itself.
    52  	Function *Node
    53  
    54  	// Values associated to this node. Flat is exclusive to this node,
    55  	// Cum includes all descendents.
    56  	Flat, FlatDiv, Cum, CumDiv int64
    57  
    58  	// In and out Contains the nodes immediately reaching or reached by
    59  	// this node.
    60  	In, Out EdgeMap
    61  }
    62  
    63  // Graph summarizes a performance profile into a format that is
    64  // suitable for visualization.
    65  type Graph struct {
    66  	Nodes Nodes
    67  }
    68  
    69  // FlatValue returns the exclusive value for this node, computing the
    70  // mean if a divisor is available.
    71  func (n *Node) FlatValue() int64 {
    72  	if n.FlatDiv == 0 {
    73  		return n.Flat
    74  	}
    75  	return n.Flat / n.FlatDiv
    76  }
    77  
    78  // CumValue returns the inclusive value for this node, computing the
    79  // mean if a divisor is available.
    80  func (n *Node) CumValue() int64 {
    81  	if n.CumDiv == 0 {
    82  		return n.Cum
    83  	}
    84  	return n.Cum / n.CumDiv
    85  }
    86  
    87  // AddToEdge increases the weight of an edge between two nodes. If
    88  // there isn't such an edge one is created.
    89  func (n *Node) AddToEdge(to *Node, v int64, residual, inline bool) {
    90  	n.AddToEdgeDiv(to, 0, v, residual, inline)
    91  }
    92  
    93  // AddToEdgeDiv increases the weight of an edge between two nodes. If
    94  // there isn't such an edge one is created.
    95  func (n *Node) AddToEdgeDiv(to *Node, dv, v int64, residual, inline bool) {
    96  	if e := n.Out.FindTo(to); e != nil {
    97  		e.WeightDiv += dv
    98  		e.Weight += v
    99  		if residual {
   100  			e.Residual = true
   101  		}
   102  		if !inline {
   103  			e.Inline = false
   104  		}
   105  		return
   106  	}
   107  
   108  	info := &Edge{Src: n, Dest: to, WeightDiv: dv, Weight: v, Residual: residual, Inline: inline}
   109  	n.Out.Add(info)
   110  	to.In.Add(info)
   111  }
   112  
   113  // NodeInfo contains the attributes for a node.
   114  type NodeInfo struct {
   115  	Name              string
   116  	Address           uint64
   117  	StartLine, Lineno int
   118  }
   119  
   120  // PrintableName calls the Node's Formatter function with a single space separator.
   121  func (i *NodeInfo) PrintableName() string {
   122  	return strings.Join(i.NameComponents(), " ")
   123  }
   124  
   125  // NameComponents returns the components of the printable name to be used for a node.
   126  func (i *NodeInfo) NameComponents() []string {
   127  	var name []string
   128  	if i.Address != 0 {
   129  		name = append(name, fmt.Sprintf("%016x", i.Address))
   130  	}
   131  	if fun := i.Name; fun != "" {
   132  		name = append(name, fun)
   133  	}
   134  
   135  	switch {
   136  	case i.Lineno != 0:
   137  		// User requested line numbers, provide what we have.
   138  		name = append(name, fmt.Sprintf(":%d", i.Lineno))
   139  	case i.Name != "":
   140  		// User requested function name. It was already included.
   141  	default:
   142  		// Do not leave it empty if there is no information at all.
   143  		name = append(name, "<unknown>")
   144  	}
   145  	return name
   146  }
   147  
   148  // NodeMap maps from a node info struct to a node. It is used to merge
   149  // report entries with the same info.
   150  type NodeMap map[NodeInfo]*Node
   151  
   152  // NodeSet is a collection of node info structs.
   153  type NodeSet map[NodeInfo]bool
   154  
   155  // NodePtrSet is a collection of nodes. Trimming a graph or tree requires a set
   156  // of objects which uniquely identify the nodes to keep. In a graph, NodeInfo
   157  // works as a unique identifier; however, in a tree multiple nodes may share
   158  // identical NodeInfos. A *Node does uniquely identify a node so we can use that
   159  // instead. Though a *Node also uniquely identifies a node in a graph,
   160  // currently, during trimming, graphs are rebuilt from scratch using only the
   161  // NodeSet, so there would not be the required context of the initial graph to
   162  // allow for the use of *Node.
   163  type NodePtrSet map[*Node]bool
   164  
   165  // FindOrInsertNode takes the info for a node and either returns a matching node
   166  // from the node map if one exists, or adds one to the map if one does not.
   167  // If kept is non-nil, nodes are only added if they can be located on it.
   168  func (nm NodeMap) FindOrInsertNode(info NodeInfo, kept NodeSet) *Node {
   169  	if kept != nil {
   170  		if _, ok := kept[info]; !ok {
   171  			return nil
   172  		}
   173  	}
   174  
   175  	if n, ok := nm[info]; ok {
   176  		return n
   177  	}
   178  
   179  	n := &Node{
   180  		Info: info,
   181  	}
   182  	nm[info] = n
   183  	if info.Address == 0 && info.Lineno == 0 {
   184  		// This node represents the whole function, so point Function
   185  		// back to itself.
   186  		n.Function = n
   187  		return n
   188  	}
   189  	// Find a node that represents the whole function.
   190  	info.Address = 0
   191  	info.Lineno = 0
   192  	n.Function = nm.FindOrInsertNode(info, nil)
   193  	return n
   194  }
   195  
   196  // EdgeMap is used to represent the incoming/outgoing edges from a node.
   197  type EdgeMap []*Edge
   198  
   199  func (em EdgeMap) FindTo(n *Node) *Edge {
   200  	for _, e := range em {
   201  		if e.Dest == n {
   202  			return e
   203  		}
   204  	}
   205  	return nil
   206  }
   207  
   208  func (em *EdgeMap) Add(e *Edge) {
   209  	*em = append(*em, e)
   210  }
   211  
   212  func (em *EdgeMap) Delete(e *Edge) {
   213  	for i, edge := range *em {
   214  		if edge == e {
   215  			(*em)[i] = (*em)[len(*em)-1]
   216  			*em = (*em)[:len(*em)-1]
   217  			return
   218  		}
   219  	}
   220  }
   221  
   222  // Edge contains any attributes to be represented about edges in a graph.
   223  type Edge struct {
   224  	Src, Dest *Node
   225  	// The summary weight of the edge
   226  	Weight, WeightDiv int64
   227  
   228  	// residual edges connect nodes that were connected through a
   229  	// separate node, which has been removed from the report.
   230  	Residual bool
   231  	// An inline edge represents a call that was inlined into the caller.
   232  	Inline bool
   233  }
   234  
   235  // WeightValue returns the weight value for this edge, normalizing if a
   236  // divisor is available.
   237  func (e *Edge) WeightValue() int64 {
   238  	if e.WeightDiv == 0 {
   239  		return e.Weight
   240  	}
   241  	return e.Weight / e.WeightDiv
   242  }
   243  
   244  // NewGraph computes a graph from a profile.
   245  func NewGraph(prof *profile.Profile, o *Options) *Graph {
   246  	nodes, locationMap := CreateNodes(prof, o)
   247  	seenNode := make(map[*Node]bool)
   248  	seenEdge := make(map[nodePair]bool)
   249  	for _, sample := range prof.Sample {
   250  		var w, dw int64
   251  		w = o.SampleValue(sample.Value)
   252  		if o.SampleMeanDivisor != nil {
   253  			dw = o.SampleMeanDivisor(sample.Value)
   254  		}
   255  		if dw == 0 && w == 0 {
   256  			continue
   257  		}
   258  		for k := range seenNode {
   259  			delete(seenNode, k)
   260  		}
   261  		for k := range seenEdge {
   262  			delete(seenEdge, k)
   263  		}
   264  		var parent *Node
   265  		// A residual edge goes over one or more nodes that were not kept.
   266  		residual := false
   267  
   268  		// Group the sample frames, based on a global map.
   269  		// Count only the last two frames as a call edge. Frames higher up
   270  		// the stack are unlikely to be repeated calls (e.g. runtime.main
   271  		// calling main.main). So adding weights to call edges higher up
   272  		// the stack may be not reflecting the actual call edge weights
   273  		// in the program. Without a branch profile this is just an
   274  		// approximation.
   275  		i := 1
   276  		if last := len(sample.Location) - 1; last < i {
   277  			i = last
   278  		}
   279  		for ; i >= 0; i-- {
   280  			l := sample.Location[i]
   281  			locNodes := locationMap.get(l.ID)
   282  			for ni := len(locNodes) - 1; ni >= 0; ni-- {
   283  				n := locNodes[ni]
   284  				if n == nil {
   285  					residual = true
   286  					continue
   287  				}
   288  				// Add cum weight to all nodes in stack, avoiding double counting.
   289  				_, sawNode := seenNode[n]
   290  				if !sawNode {
   291  					seenNode[n] = true
   292  					n.addSample(dw, w, false)
   293  				}
   294  				// Update edge weights for all edges in stack, avoiding double counting.
   295  				if (!sawNode || !seenEdge[nodePair{n, parent}]) && parent != nil && n != parent {
   296  					seenEdge[nodePair{n, parent}] = true
   297  					parent.AddToEdgeDiv(n, dw, w, residual, ni != len(locNodes)-1)
   298  				}
   299  
   300  				parent = n
   301  				residual = false
   302  			}
   303  		}
   304  		if parent != nil && !residual {
   305  			// Add flat weight to leaf node.
   306  			parent.addSample(dw, w, true)
   307  		}
   308  	}
   309  
   310  	return selectNodesForGraph(nodes, o.DropNegative)
   311  }
   312  
   313  func selectNodesForGraph(nodes Nodes, dropNegative bool) *Graph {
   314  	// Collect nodes into a graph.
   315  	gNodes := make(Nodes, 0, len(nodes))
   316  	for _, n := range nodes {
   317  		if n == nil {
   318  			continue
   319  		}
   320  		if n.Cum == 0 && n.Flat == 0 {
   321  			continue
   322  		}
   323  		if dropNegative && isNegative(n) {
   324  			continue
   325  		}
   326  		gNodes = append(gNodes, n)
   327  	}
   328  	return &Graph{gNodes}
   329  }
   330  
   331  type nodePair struct {
   332  	src, dest *Node
   333  }
   334  
   335  // isNegative returns true if the node is considered as "negative" for the
   336  // purposes of drop_negative.
   337  func isNegative(n *Node) bool {
   338  	switch {
   339  	case n.Flat < 0:
   340  		return true
   341  	case n.Flat == 0 && n.Cum < 0:
   342  		return true
   343  	default:
   344  		return false
   345  	}
   346  }
   347  
   348  type locationMap struct {
   349  	s []Nodes          // a slice for small sequential IDs
   350  	m map[uint64]Nodes // fallback for large IDs (unlikely)
   351  }
   352  
   353  func (l *locationMap) add(id uint64, n Nodes) {
   354  	if id < uint64(len(l.s)) {
   355  		l.s[id] = n
   356  	} else {
   357  		l.m[id] = n
   358  	}
   359  }
   360  
   361  func (l locationMap) get(id uint64) Nodes {
   362  	if id < uint64(len(l.s)) {
   363  		return l.s[id]
   364  	} else {
   365  		return l.m[id]
   366  	}
   367  }
   368  
   369  // CreateNodes creates graph nodes for all locations in a profile. It
   370  // returns set of all nodes, plus a mapping of each location to the
   371  // set of corresponding nodes (one per location.Line).
   372  func CreateNodes(prof *profile.Profile, o *Options) (Nodes, locationMap) {
   373  	locations := locationMap{make([]Nodes, len(prof.Location)+1), make(map[uint64]Nodes)}
   374  	nm := make(NodeMap, len(prof.Location))
   375  	for _, l := range prof.Location {
   376  		lines := l.Line
   377  		if len(lines) == 0 {
   378  			lines = []profile.Line{{}} // Create empty line to include location info.
   379  		}
   380  		nodes := make(Nodes, len(lines))
   381  		for ln := range lines {
   382  			nodes[ln] = nm.findOrInsertLine(l, lines[ln], o)
   383  		}
   384  		locations.add(l.ID, nodes)
   385  	}
   386  	return nm.nodes(), locations
   387  }
   388  
   389  func (nm NodeMap) nodes() Nodes {
   390  	nodes := make(Nodes, 0, len(nm))
   391  	for _, n := range nm {
   392  		nodes = append(nodes, n)
   393  	}
   394  	return nodes
   395  }
   396  
   397  func (nm NodeMap) findOrInsertLine(l *profile.Location, li profile.Line, o *Options) *Node {
   398  	var objfile string
   399  	if m := l.Mapping; m != nil && m.File != "" {
   400  		objfile = m.File
   401  	}
   402  
   403  	if ni := nodeInfo(l, li, objfile, o); ni != nil {
   404  		return nm.FindOrInsertNode(*ni, o.KeptNodes)
   405  	}
   406  	return nil
   407  }
   408  
   409  func nodeInfo(l *profile.Location, line profile.Line, objfile string, o *Options) *NodeInfo {
   410  	if line.Function == nil {
   411  		return &NodeInfo{Address: l.Address}
   412  	}
   413  	ni := &NodeInfo{
   414  		Address: l.Address,
   415  		Lineno:  int(line.Line),
   416  		Name:    line.Function.Name,
   417  	}
   418  	ni.StartLine = int(line.Function.StartLine)
   419  	return ni
   420  }
   421  
   422  // Sum adds the flat and cum values of a set of nodes.
   423  func (ns Nodes) Sum() (flat int64, cum int64) {
   424  	for _, n := range ns {
   425  		flat += n.Flat
   426  		cum += n.Cum
   427  	}
   428  	return
   429  }
   430  
   431  func (n *Node) addSample(dw, w int64, flat bool) {
   432  	// Update sample value
   433  	if flat {
   434  		n.FlatDiv += dw
   435  		n.Flat += w
   436  	} else {
   437  		n.CumDiv += dw
   438  		n.Cum += w
   439  	}
   440  }
   441  
   442  // String returns a text representation of a graph, for debugging purposes.
   443  func (g *Graph) String() string {
   444  	var s []string
   445  
   446  	nodeIndex := make(map[*Node]int, len(g.Nodes))
   447  
   448  	for i, n := range g.Nodes {
   449  		nodeIndex[n] = i + 1
   450  	}
   451  
   452  	for i, n := range g.Nodes {
   453  		name := n.Info.PrintableName()
   454  		var in, out []int
   455  
   456  		for _, from := range n.In {
   457  			in = append(in, nodeIndex[from.Src])
   458  		}
   459  		for _, to := range n.Out {
   460  			out = append(out, nodeIndex[to.Dest])
   461  		}
   462  		s = append(s, fmt.Sprintf("%d: %s[flat=%d cum=%d] %x -> %v ", i+1, name, n.Flat, n.Cum, in, out))
   463  	}
   464  	return strings.Join(s, "\n")
   465  }
   466  
   467  // Sort returns a slice of the edges in the map, in a consistent
   468  // order. The sort order is first based on the edge weight
   469  // (higher-to-lower) and then by the node names to avoid flakiness.
   470  func (em EdgeMap) Sort() []*Edge {
   471  	el := make(edgeList, 0, len(em))
   472  	for _, w := range em {
   473  		el = append(el, w)
   474  	}
   475  
   476  	sort.Sort(el)
   477  	return el
   478  }
   479  
   480  // Sum returns the total weight for a set of nodes.
   481  func (em EdgeMap) Sum() int64 {
   482  	var ret int64
   483  	for _, edge := range em {
   484  		ret += edge.Weight
   485  	}
   486  	return ret
   487  }
   488  
   489  type edgeList []*Edge
   490  
   491  func (el edgeList) Len() int {
   492  	return len(el)
   493  }
   494  
   495  func (el edgeList) Less(i, j int) bool {
   496  	if el[i].Weight != el[j].Weight {
   497  		return abs64(el[i].Weight) > abs64(el[j].Weight)
   498  	}
   499  
   500  	from1 := el[i].Src.Info.PrintableName()
   501  	from2 := el[j].Src.Info.PrintableName()
   502  	if from1 != from2 {
   503  		return from1 < from2
   504  	}
   505  
   506  	to1 := el[i].Dest.Info.PrintableName()
   507  	to2 := el[j].Dest.Info.PrintableName()
   508  
   509  	return to1 < to2
   510  }
   511  
   512  func (el edgeList) Swap(i, j int) {
   513  	el[i], el[j] = el[j], el[i]
   514  }
   515  
   516  func abs64(i int64) int64 {
   517  	if i < 0 {
   518  		return -i
   519  	}
   520  	return i
   521  }