| // 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 graph collects a set of samples into a directed graph. |
| |
| // Original file location: https://github.com/google/pprof/tree/main/internal/graph/graph.go |
| package pgo |
| |
| import ( |
| "fmt" |
| "internal/profile" |
| "math" |
| "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 |
| |
| CallTree bool // Build a tree instead of a graph |
| 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 |
| //File string |
| //OrigName string |
| //Objfile string |
| } |
| |
| // 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.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 |
| } |
| for k := range seenNode { |
| delete(seenNode, k) |
| } |
| for k := range seenEdge { |
| delete(seenEdge, k) |
| } |
| 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 |
| } |
| |
| func newTree(prof *profile.Profile, o *Options) (g *Graph) { |
| parentNodeMap := make(map[*Node]NodeMap, len(prof.Sample)) |
| 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 |
| } |
| var parent *Node |
| // Group the sample frames, based on a per-node map. |
| for i := len(sample.Location) - 1; i >= 0; i-- { |
| l := sample.Location[i] |
| lines := l.Line |
| if len(lines) == 0 { |
| lines = []profile.Line{{}} // Create empty line to include location info. |
| } |
| for lidx := len(lines) - 1; lidx >= 0; lidx-- { |
| nodeMap := parentNodeMap[parent] |
| if nodeMap == nil { |
| nodeMap = make(NodeMap) |
| parentNodeMap[parent] = nodeMap |
| } |
| n := nodeMap.findOrInsertLine(l, lines[lidx], o) |
| if n == nil { |
| continue |
| } |
| n.addSample(dw, w, false) |
| if parent != nil { |
| parent.AddToEdgeDiv(n, dw, w, false, lidx != len(lines)-1) |
| } |
| parent = n |
| } |
| } |
| if parent != nil { |
| parent.addSample(dw, w, true) |
| } |
| } |
| |
| nodes := make(Nodes, len(prof.Location)) |
| for _, nm := range parentNodeMap { |
| nodes = append(nodes, nm.nodes()...) |
| } |
| return selectNodesForGraph(nodes, o.DropNegative) |
| } |
| |
| // 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.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 = []profile.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 *profile.Location, li profile.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 *profile.Location, line profile.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") |
| } |
| |
| // DiscardLowFrequencyNodes returns a set of the nodes at or over a |
| // specific cum value cutoff. |
| func (g *Graph) DiscardLowFrequencyNodes(nodeCutoff int64) NodeSet { |
| return makeNodeSet(g.Nodes, nodeCutoff) |
| } |
| |
| // DiscardLowFrequencyNodePtrs returns a NodePtrSet of nodes at or over a |
| // specific cum value cutoff. |
| func (g *Graph) DiscardLowFrequencyNodePtrs(nodeCutoff int64) NodePtrSet { |
| cutNodes := getNodesAboveCumCutoff(g.Nodes, nodeCutoff) |
| kept := make(NodePtrSet, len(cutNodes)) |
| for _, n := range cutNodes { |
| kept[n] = true |
| } |
| return kept |
| } |
| |
| func makeNodeSet(nodes Nodes, nodeCutoff int64) NodeSet { |
| cutNodes := getNodesAboveCumCutoff(nodes, nodeCutoff) |
| kept := make(NodeSet, len(cutNodes)) |
| for _, n := range cutNodes { |
| kept[n.Info] = true |
| } |
| return kept |
| } |
| |
| // getNodesAboveCumCutoff returns all the nodes which have a Cum value greater |
| // than or equal to cutoff. |
| func getNodesAboveCumCutoff(nodes Nodes, nodeCutoff int64) Nodes { |
| cutoffNodes := make(Nodes, 0, len(nodes)) |
| for _, n := range nodes { |
| if abs64(n.Cum) < nodeCutoff { |
| continue |
| } |
| cutoffNodes = append(cutoffNodes, n) |
| } |
| return cutoffNodes |
| } |
| |
| // TrimLowFrequencyEdges removes edges that have less than |
| // the specified weight. Returns the number of edges removed |
| func (g *Graph) TrimLowFrequencyEdges(edgeCutoff int64) int { |
| var droppedEdges int |
| for _, n := range g.Nodes { |
| for _, e := range n.In { |
| if abs64(e.Weight) < edgeCutoff { |
| n.In.Delete(e) |
| e.Src.Out.Delete(e) |
| droppedEdges++ |
| } |
| } |
| } |
| return droppedEdges |
| } |
| |
| // SortNodes sorts the nodes in a graph based on a specific heuristic. |
| func (g *Graph) SortNodes(cum bool, visualMode bool) { |
| // Sort nodes based on requested mode |
| switch { |
| case visualMode: |
| // Specialized sort to produce a more visually-interesting graph |
| g.Nodes.Sort(EntropyOrder) |
| case cum: |
| g.Nodes.Sort(CumNameOrder) |
| default: |
| g.Nodes.Sort(FlatNameOrder) |
| } |
| } |
| |
| // SelectTopNodePtrs returns a set of the top maxNodes *Node in a graph. |
| func (g *Graph) SelectTopNodePtrs(maxNodes int, visualMode bool) NodePtrSet { |
| set := make(NodePtrSet) |
| for _, node := range g.selectTopNodes(maxNodes, visualMode) { |
| set[node] = true |
| } |
| return set |
| } |
| |
| // SelectTopNodes returns a set of the top maxNodes nodes in a graph. |
| func (g *Graph) SelectTopNodes(maxNodes int, visualMode bool) NodeSet { |
| return makeNodeSet(g.selectTopNodes(maxNodes, visualMode), 0) |
| } |
| |
| // selectTopNodes returns a slice of the top maxNodes nodes in a graph. |
| func (g *Graph) selectTopNodes(maxNodes int, visualMode bool) Nodes { |
| if maxNodes > len(g.Nodes) { |
| maxNodes = len(g.Nodes) |
| } |
| return g.Nodes[:maxNodes] |
| } |
| |
| // nodeSorter is a mechanism used to allow a report to be sorted |
| // in different ways. |
| type nodeSorter struct { |
| rs Nodes |
| less func(l, r *Node) bool |
| } |
| |
| func (s nodeSorter) Len() int { return len(s.rs) } |
| func (s nodeSorter) Swap(i, j int) { s.rs[i], s.rs[j] = s.rs[j], s.rs[i] } |
| func (s nodeSorter) Less(i, j int) bool { return s.less(s.rs[i], s.rs[j]) } |
| |
| // Sort reorders a slice of nodes based on the specified ordering |
| // criteria. The result is sorted in decreasing order for (absolute) |
| // numeric quantities, alphabetically for text, and increasing for |
| // addresses. |
| func (ns Nodes) Sort(o NodeOrder) error { |
| var s nodeSorter |
| |
| switch o { |
| case FlatNameOrder: |
| s = nodeSorter{ns, |
| func(l, r *Node) bool { |
| if iv, jv := abs64(l.Flat), abs64(r.Flat); iv != jv { |
| return iv > jv |
| } |
| if iv, jv := l.Info.PrintableName(), r.Info.PrintableName(); iv != jv { |
| return iv < jv |
| } |
| if iv, jv := abs64(l.Cum), abs64(r.Cum); iv != jv { |
| return iv > jv |
| } |
| return compareNodes(l, r) |
| }, |
| } |
| case FlatCumNameOrder: |
| s = nodeSorter{ns, |
| func(l, r *Node) bool { |
| if iv, jv := abs64(l.Flat), abs64(r.Flat); iv != jv { |
| return iv > jv |
| } |
| if iv, jv := abs64(l.Cum), abs64(r.Cum); iv != jv { |
| return iv > jv |
| } |
| if iv, jv := l.Info.PrintableName(), r.Info.PrintableName(); iv != jv { |
| return iv < jv |
| } |
| return compareNodes(l, r) |
| }, |
| } |
| case NameOrder: |
| s = nodeSorter{ns, |
| func(l, r *Node) bool { |
| if iv, jv := l.Info.Name, r.Info.Name; iv != jv { |
| return iv < jv |
| } |
| return compareNodes(l, r) |
| }, |
| } |
| case FileOrder: |
| s = nodeSorter{ns, |
| func(l, r *Node) bool { |
| if iv, jv := l.Info.StartLine, r.Info.StartLine; iv != jv { |
| return iv < jv |
| } |
| return compareNodes(l, r) |
| }, |
| } |
| case AddressOrder: |
| s = nodeSorter{ns, |
| func(l, r *Node) bool { |
| if iv, jv := l.Info.Address, r.Info.Address; iv != jv { |
| return iv < jv |
| } |
| return compareNodes(l, r) |
| }, |
| } |
| case CumNameOrder, EntropyOrder: |
| // Hold scoring for score-based ordering |
| var score map[*Node]int64 |
| scoreOrder := func(l, r *Node) bool { |
| if iv, jv := abs64(score[l]), abs64(score[r]); iv != jv { |
| return iv > jv |
| } |
| if iv, jv := l.Info.PrintableName(), r.Info.PrintableName(); iv != jv { |
| return iv < jv |
| } |
| if iv, jv := abs64(l.Flat), abs64(r.Flat); iv != jv { |
| return iv > jv |
| } |
| return compareNodes(l, r) |
| } |
| |
| switch o { |
| case CumNameOrder: |
| score = make(map[*Node]int64, len(ns)) |
| for _, n := range ns { |
| score[n] = n.Cum |
| } |
| s = nodeSorter{ns, scoreOrder} |
| case EntropyOrder: |
| score = make(map[*Node]int64, len(ns)) |
| for _, n := range ns { |
| score[n] = entropyScore(n) |
| } |
| s = nodeSorter{ns, scoreOrder} |
| } |
| default: |
| return fmt.Errorf("report: unrecognized sort ordering: %d", o) |
| } |
| sort.Sort(s) |
| return nil |
| } |
| |
| // compareNodes compares two nodes to provide a deterministic ordering |
| // between them. Two nodes cannot have the same Node.Info value. |
| func compareNodes(l, r *Node) bool { |
| return fmt.Sprint(l.Info) < fmt.Sprint(r.Info) |
| } |
| |
| // entropyScore computes a score for a node representing how important |
| // it is to include this node on a graph visualization. It is used to |
| // sort the nodes and select which ones to display if we have more |
| // nodes than desired in the graph. This number is computed by looking |
| // at the flat and cum weights of the node and the incoming/outgoing |
| // edges. The fundamental idea is to penalize nodes that have a simple |
| // fallthrough from their incoming to the outgoing edge. |
| func entropyScore(n *Node) int64 { |
| score := float64(0) |
| |
| if len(n.In) == 0 { |
| score++ // Favor entry nodes |
| } else { |
| score += edgeEntropyScore(n, n.In, 0) |
| } |
| |
| if len(n.Out) == 0 { |
| score++ // Favor leaf nodes |
| } else { |
| score += edgeEntropyScore(n, n.Out, n.Flat) |
| } |
| |
| return int64(score*float64(n.Cum)) + n.Flat |
| } |
| |
| // edgeEntropyScore computes the entropy value for a set of edges |
| // coming in or out of a node. Entropy (as defined in information |
| // theory) refers to the amount of information encoded by the set of |
| // edges. A set of edges that have a more interesting distribution of |
| // samples gets a higher score. |
| func edgeEntropyScore(n *Node, edges EdgeMap, self int64) float64 { |
| score := float64(0) |
| total := self |
| for _, e := range edges { |
| if e.Weight > 0 { |
| total += abs64(e.Weight) |
| } |
| } |
| if total != 0 { |
| for _, e := range edges { |
| frac := float64(abs64(e.Weight)) / float64(total) |
| score += -frac * math.Log2(frac) |
| } |
| if self > 0 { |
| frac := float64(abs64(self)) / float64(total) |
| score += -frac * math.Log2(frac) |
| } |
| } |
| return score |
| } |
| |
| // NodeOrder sets the ordering for a Sort operation |
| type NodeOrder int |
| |
| // Sorting options for node sort. |
| const ( |
| FlatNameOrder NodeOrder = iota |
| FlatCumNameOrder |
| CumNameOrder |
| NameOrder |
| FileOrder |
| AddressOrder |
| EntropyOrder |
| ) |
| |
| // 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 (e EdgeMap) Sort() []*Edge { |
| el := make(edgeList, 0, len(e)) |
| for _, w := range e { |
| el = append(el, w) |
| } |
| |
| sort.Sort(el) |
| return el |
| } |
| |
| // Sum returns the total weight for a set of nodes. |
| func (e EdgeMap) Sum() int64 { |
| var ret int64 |
| for _, edge := range e { |
| 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 |
| } |