| // Copyright 2014 Google Inc. All Rights Reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // Package profile represents a pprof profile as a directed graph. | |
| // | |
| // This package is a simplified fork of github.com/google/pprof/internal/graph. | |
| package profile | |
| import ( | |
| "fmt" | |
| "sort" | |
| "strings" | |
| ) | |
| // Options encodes the options for constructing a graph | |
| type Options struct { | |
| SampleValue func(s []int64) int64 // Function to compute the value of a sample | |
| SampleMeanDivisor func(s []int64) int64 // Function to compute the divisor for mean graphs, or nil | |
| DropNegative bool // Drop nodes with overall negative values | |
| KeptNodes NodeSet // If non-nil, only use nodes in this set | |
| } | |
| // Nodes is an ordered collection of graph nodes. | |
| type Nodes []*Node | |
| // Node is an entry on a profiling report. It represents a unique | |
| // program location. | |
| type Node struct { | |
| // Info describes the source location associated to this node. | |
| Info NodeInfo | |
| // Function represents the function that this node belongs to. On | |
| // graphs with sub-function resolution (eg line number or | |
| // addresses), two nodes in a NodeMap that are part of the same | |
| // function have the same value of Node.Function. If the Node | |
| // represents the whole function, it points back to itself. | |
| Function *Node | |
| // Values associated to this node. Flat is exclusive to this node, | |
| // Cum includes all descendents. | |
| Flat, FlatDiv, Cum, CumDiv int64 | |
| // In and out Contains the nodes immediately reaching or reached by | |
| // this node. | |
| In, Out EdgeMap | |
| } | |
| // Graph summarizes a performance profile into a format that is | |
| // suitable for visualization. | |
| type Graph struct { | |
| Nodes Nodes | |
| } | |
| // FlatValue returns the exclusive value for this node, computing the | |
| // mean if a divisor is available. | |
| func (n *Node) FlatValue() int64 { | |
| if n.FlatDiv == 0 { | |
| return n.Flat | |
| } | |
| return n.Flat / n.FlatDiv | |
| } | |
| // CumValue returns the inclusive value for this node, computing the | |
| // mean if a divisor is available. | |
| func (n *Node) CumValue() int64 { | |
| if n.CumDiv == 0 { | |
| return n.Cum | |
| } | |
| return n.Cum / n.CumDiv | |
| } | |
| // AddToEdge increases the weight of an edge between two nodes. If | |
| // there isn't such an edge one is created. | |
| func (n *Node) AddToEdge(to *Node, v int64, residual, inline bool) { | |
| n.AddToEdgeDiv(to, 0, v, residual, inline) | |
| } | |
| // AddToEdgeDiv increases the weight of an edge between two nodes. If | |
| // there isn't such an edge one is created. | |
| func (n *Node) AddToEdgeDiv(to *Node, dv, v int64, residual, inline bool) { | |
| if e := n.Out.FindTo(to); e != nil { | |
| e.WeightDiv += dv | |
| e.Weight += v | |
| if residual { | |
| e.Residual = true | |
| } | |
| if !inline { | |
| e.Inline = false | |
| } | |
| return | |
| } | |
| info := &Edge{Src: n, Dest: to, WeightDiv: dv, Weight: v, Residual: residual, Inline: inline} | |
| n.Out.Add(info) | |
| to.In.Add(info) | |
| } | |
| // NodeInfo contains the attributes for a node. | |
| type NodeInfo struct { | |
| Name string | |
| Address uint64 | |
| StartLine, Lineno int | |
| } | |
| // PrintableName calls the Node's Formatter function with a single space separator. | |
| func (i *NodeInfo) PrintableName() string { | |
| return strings.Join(i.NameComponents(), " ") | |
| } | |
| // NameComponents returns the components of the printable name to be used for a node. | |
| func (i *NodeInfo) NameComponents() []string { | |
| var name []string | |
| if i.Address != 0 { | |
| name = append(name, fmt.Sprintf("%016x", i.Address)) | |
| } | |
| if fun := i.Name; fun != "" { | |
| name = append(name, fun) | |
| } | |
| switch { | |
| case i.Lineno != 0: | |
| // User requested line numbers, provide what we have. | |
| name = append(name, fmt.Sprintf(":%d", i.Lineno)) | |
| case i.Name != "": | |
| // User requested function name. It was already included. | |
| default: | |
| // Do not leave it empty if there is no information at all. | |
| name = append(name, "<unknown>") | |
| } | |
| return name | |
| } | |
| // NodeMap maps from a node info struct to a node. It is used to merge | |
| // report entries with the same info. | |
| type NodeMap map[NodeInfo]*Node | |
| // NodeSet is a collection of node info structs. | |
| type NodeSet map[NodeInfo]bool | |
| // NodePtrSet is a collection of nodes. Trimming a graph or tree requires a set | |
| // of objects which uniquely identify the nodes to keep. In a graph, NodeInfo | |
| // works as a unique identifier; however, in a tree multiple nodes may share | |
| // identical NodeInfos. A *Node does uniquely identify a node so we can use that | |
| // instead. Though a *Node also uniquely identifies a node in a graph, | |
| // currently, during trimming, graphs are rebuilt from scratch using only the | |
| // NodeSet, so there would not be the required context of the initial graph to | |
| // allow for the use of *Node. | |
| type NodePtrSet map[*Node]bool | |
| // FindOrInsertNode takes the info for a node and either returns a matching node | |
| // from the node map if one exists, or adds one to the map if one does not. | |
| // If kept is non-nil, nodes are only added if they can be located on it. | |
| func (nm NodeMap) FindOrInsertNode(info NodeInfo, kept NodeSet) *Node { | |
| if kept != nil { | |
| if _, ok := kept[info]; !ok { | |
| return nil | |
| } | |
| } | |
| if n, ok := nm[info]; ok { | |
| return n | |
| } | |
| n := &Node{ | |
| Info: info, | |
| } | |
| nm[info] = n | |
| if info.Address == 0 && info.Lineno == 0 { | |
| // This node represents the whole function, so point Function | |
| // back to itself. | |
| n.Function = n | |
| return n | |
| } | |
| // Find a node that represents the whole function. | |
| info.Address = 0 | |
| info.Lineno = 0 | |
| n.Function = nm.FindOrInsertNode(info, nil) | |
| return n | |
| } | |
| // EdgeMap is used to represent the incoming/outgoing edges from a node. | |
| type EdgeMap []*Edge | |
| func (em EdgeMap) FindTo(n *Node) *Edge { | |
| for _, e := range em { | |
| if e.Dest == n { | |
| return e | |
| } | |
| } | |
| return nil | |
| } | |
| func (em *EdgeMap) Add(e *Edge) { | |
| *em = append(*em, e) | |
| } | |
| func (em *EdgeMap) Delete(e *Edge) { | |
| for i, edge := range *em { | |
| if edge == e { | |
| (*em)[i] = (*em)[len(*em)-1] | |
| *em = (*em)[:len(*em)-1] | |
| return | |
| } | |
| } | |
| } | |
| // Edge contains any attributes to be represented about edges in a graph. | |
| type Edge struct { | |
| Src, Dest *Node | |
| // The summary weight of the edge | |
| Weight, WeightDiv int64 | |
| // residual edges connect nodes that were connected through a | |
| // separate node, which has been removed from the report. | |
| Residual bool | |
| // An inline edge represents a call that was inlined into the caller. | |
| Inline bool | |
| } | |
| // WeightValue returns the weight value for this edge, normalizing if a | |
| // divisor is available. | |
| func (e *Edge) WeightValue() int64 { | |
| if e.WeightDiv == 0 { | |
| return e.Weight | |
| } | |
| return e.Weight / e.WeightDiv | |
| } | |
| // NewGraph computes a graph from a profile. | |
| func NewGraph(prof *Profile, o *Options) *Graph { | |
| nodes, locationMap := CreateNodes(prof, o) | |
| seenNode := make(map[*Node]bool) | |
| seenEdge := make(map[nodePair]bool) | |
| for _, sample := range prof.Sample { | |
| var w, dw int64 | |
| w = o.SampleValue(sample.Value) | |
| if o.SampleMeanDivisor != nil { | |
| dw = o.SampleMeanDivisor(sample.Value) | |
| } | |
| if dw == 0 && w == 0 { | |
| continue | |
| } | |
| clear(seenNode) | |
| clear(seenEdge) | |
| var parent *Node | |
| // A residual edge goes over one or more nodes that were not kept. | |
| residual := false | |
| // Group the sample frames, based on a global map. | |
| // Count only the last two frames as a call edge. Frames higher up | |
| // the stack are unlikely to be repeated calls (e.g. runtime.main | |
| // calling main.main). So adding weights to call edges higher up | |
| // the stack may be not reflecting the actual call edge weights | |
| // in the program. Without a branch profile this is just an | |
| // approximation. | |
| i := 1 | |
| if last := len(sample.Location) - 1; last < i { | |
| i = last | |
| } | |
| for ; i >= 0; i-- { | |
| l := sample.Location[i] | |
| locNodes := locationMap.get(l.ID) | |
| for ni := len(locNodes) - 1; ni >= 0; ni-- { | |
| n := locNodes[ni] | |
| if n == nil { | |
| residual = true | |
| continue | |
| } | |
| // Add cum weight to all nodes in stack, avoiding double counting. | |
| _, sawNode := seenNode[n] | |
| if !sawNode { | |
| seenNode[n] = true | |
| n.addSample(dw, w, false) | |
| } | |
| // Update edge weights for all edges in stack, avoiding double counting. | |
| if (!sawNode || !seenEdge[nodePair{n, parent}]) && parent != nil && n != parent { | |
| seenEdge[nodePair{n, parent}] = true | |
| parent.AddToEdgeDiv(n, dw, w, residual, ni != len(locNodes)-1) | |
| } | |
| parent = n | |
| residual = false | |
| } | |
| } | |
| if parent != nil && !residual { | |
| // Add flat weight to leaf node. | |
| parent.addSample(dw, w, true) | |
| } | |
| } | |
| return selectNodesForGraph(nodes, o.DropNegative) | |
| } | |
| func selectNodesForGraph(nodes Nodes, dropNegative bool) *Graph { | |
| // Collect nodes into a graph. | |
| gNodes := make(Nodes, 0, len(nodes)) | |
| for _, n := range nodes { | |
| if n == nil { | |
| continue | |
| } | |
| if n.Cum == 0 && n.Flat == 0 { | |
| continue | |
| } | |
| if dropNegative && isNegative(n) { | |
| continue | |
| } | |
| gNodes = append(gNodes, n) | |
| } | |
| return &Graph{gNodes} | |
| } | |
| type nodePair struct { | |
| src, dest *Node | |
| } | |
| // isNegative returns true if the node is considered as "negative" for the | |
| // purposes of drop_negative. | |
| func isNegative(n *Node) bool { | |
| switch { | |
| case n.Flat < 0: | |
| return true | |
| case n.Flat == 0 && n.Cum < 0: | |
| return true | |
| default: | |
| return false | |
| } | |
| } | |
| type locationMap struct { | |
| s []Nodes // a slice for small sequential IDs | |
| m map[uint64]Nodes // fallback for large IDs (unlikely) | |
| } | |
| func (l *locationMap) add(id uint64, n Nodes) { | |
| if id < uint64(len(l.s)) { | |
| l.s[id] = n | |
| } else { | |
| l.m[id] = n | |
| } | |
| } | |
| func (l locationMap) get(id uint64) Nodes { | |
| if id < uint64(len(l.s)) { | |
| return l.s[id] | |
| } else { | |
| return l.m[id] | |
| } | |
| } | |
| // CreateNodes creates graph nodes for all locations in a profile. It | |
| // returns set of all nodes, plus a mapping of each location to the | |
| // set of corresponding nodes (one per location.Line). | |
| func CreateNodes(prof *Profile, o *Options) (Nodes, locationMap) { | |
| locations := locationMap{make([]Nodes, len(prof.Location)+1), make(map[uint64]Nodes)} | |
| nm := make(NodeMap, len(prof.Location)) | |
| for _, l := range prof.Location { | |
| lines := l.Line | |
| if len(lines) == 0 { | |
| lines = []Line{{}} // Create empty line to include location info. | |
| } | |
| nodes := make(Nodes, len(lines)) | |
| for ln := range lines { | |
| nodes[ln] = nm.findOrInsertLine(l, lines[ln], o) | |
| } | |
| locations.add(l.ID, nodes) | |
| } | |
| return nm.nodes(), locations | |
| } | |
| func (nm NodeMap) nodes() Nodes { | |
| nodes := make(Nodes, 0, len(nm)) | |
| for _, n := range nm { | |
| nodes = append(nodes, n) | |
| } | |
| return nodes | |
| } | |
| func (nm NodeMap) findOrInsertLine(l *Location, li Line, o *Options) *Node { | |
| var objfile string | |
| if m := l.Mapping; m != nil && m.File != "" { | |
| objfile = m.File | |
| } | |
| if ni := nodeInfo(l, li, objfile, o); ni != nil { | |
| return nm.FindOrInsertNode(*ni, o.KeptNodes) | |
| } | |
| return nil | |
| } | |
| func nodeInfo(l *Location, line Line, objfile string, o *Options) *NodeInfo { | |
| if line.Function == nil { | |
| return &NodeInfo{Address: l.Address} | |
| } | |
| ni := &NodeInfo{ | |
| Address: l.Address, | |
| Lineno: int(line.Line), | |
| Name: line.Function.Name, | |
| } | |
| ni.StartLine = int(line.Function.StartLine) | |
| return ni | |
| } | |
| // Sum adds the flat and cum values of a set of nodes. | |
| func (ns Nodes) Sum() (flat int64, cum int64) { | |
| for _, n := range ns { | |
| flat += n.Flat | |
| cum += n.Cum | |
| } | |
| return | |
| } | |
| func (n *Node) addSample(dw, w int64, flat bool) { | |
| // Update sample value | |
| if flat { | |
| n.FlatDiv += dw | |
| n.Flat += w | |
| } else { | |
| n.CumDiv += dw | |
| n.Cum += w | |
| } | |
| } | |
| // String returns a text representation of a graph, for debugging purposes. | |
| func (g *Graph) String() string { | |
| var s []string | |
| nodeIndex := make(map[*Node]int, len(g.Nodes)) | |
| for i, n := range g.Nodes { | |
| nodeIndex[n] = i + 1 | |
| } | |
| for i, n := range g.Nodes { | |
| name := n.Info.PrintableName() | |
| var in, out []int | |
| for _, from := range n.In { | |
| in = append(in, nodeIndex[from.Src]) | |
| } | |
| for _, to := range n.Out { | |
| out = append(out, nodeIndex[to.Dest]) | |
| } | |
| s = append(s, fmt.Sprintf("%d: %s[flat=%d cum=%d] %x -> %v ", i+1, name, n.Flat, n.Cum, in, out)) | |
| } | |
| return strings.Join(s, "\n") | |
| } | |
| // Sort returns a slice of the edges in the map, in a consistent | |
| // order. The sort order is first based on the edge weight | |
| // (higher-to-lower) and then by the node names to avoid flakiness. | |
| func (em EdgeMap) Sort() []*Edge { | |
| el := make(edgeList, 0, len(em)) | |
| for _, w := range em { | |
| el = append(el, w) | |
| } | |
| sort.Sort(el) | |
| return el | |
| } | |
| // Sum returns the total weight for a set of nodes. | |
| func (em EdgeMap) Sum() int64 { | |
| var ret int64 | |
| for _, edge := range em { | |
| ret += edge.Weight | |
| } | |
| return ret | |
| } | |
| type edgeList []*Edge | |
| func (el edgeList) Len() int { | |
| return len(el) | |
| } | |
| func (el edgeList) Less(i, j int) bool { | |
| if el[i].Weight != el[j].Weight { | |
| return abs64(el[i].Weight) > abs64(el[j].Weight) | |
| } | |
| from1 := el[i].Src.Info.PrintableName() | |
| from2 := el[j].Src.Info.PrintableName() | |
| if from1 != from2 { | |
| return from1 < from2 | |
| } | |
| to1 := el[i].Dest.Info.PrintableName() | |
| to2 := el[j].Dest.Info.PrintableName() | |
| return to1 < to2 | |
| } | |
| func (el edgeList) Swap(i, j int) { | |
| el[i], el[j] = el[j], el[i] | |
| } | |
| func abs64(i int64) int64 { | |
| if i < 0 { | |
| return -i | |
| } | |
| return i | |
| } |