Bladeren bron

Adds correct trimming with heuristics for trees

Wade Simba Khadder 8 jaren geleden
bovenliggende
commit
d51fda846e
5 gewijzigde bestanden met toevoegingen van 396 en 37 verwijderingen
  1. 68
    5
      internal/graph/graph.go
  2. 298
    0
      internal/graph/graph_test.go
  3. 16
    18
      internal/report/report.go
  4. 2
    2
      internal/report/source.go
  5. 12
    12
      internal/report/testdata/source.dot

+ 68
- 5
internal/graph/graph.go Bestand weergeven

@@ -18,6 +18,7 @@ package graph
18 18
 import (
19 19
 	"fmt"
20 20
 	"math"
21
+	"os"
21 22
 	"path/filepath"
22 23
 	"sort"
23 24
 	"strconv"
@@ -142,14 +143,17 @@ func (i *NodeInfo) NameComponents() []string {
142 143
 type NodeMap map[NodeInfo]*Node
143 144
 
144 145
 // NodeSet maps is a collection of node info structs.
145
-type NodeSet map[NodeInfo]bool
146
+type NodeSet struct {
147
+	Info map[NodeInfo]bool
148
+	Ptr  map[*Node]bool
149
+}
146 150
 
147 151
 // FindOrInsertNode takes the info for a node and either returns a matching node
148 152
 // from the node map if one exists, or adds one to the map if one does not.
149 153
 // If kept is non-nil, nodes are only added if they can be located on it.
150 154
 func (nm NodeMap) FindOrInsertNode(info NodeInfo, kept NodeSet) *Node {
151
-	if kept != nil {
152
-		if _, ok := kept[info]; !ok {
155
+	if kept.Info != nil {
156
+		if _, ok := kept.Info[info]; !ok {
153 157
 			return nil
154 158
 		}
155 159
 	}
@@ -331,6 +335,61 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
331 335
 	return selectNodesForGraph(nodes, o.DropNegative)
332 336
 }
333 337
 
338
+// Trims a Graph that is in forest form to contain only the nodes in kept. This
339
+// will not work correctly in the case that a node has multiple parents.
340
+func (g *Graph) TrimTree(kept NodeSet) *Graph {
341
+	// Creates a new list of nodes
342
+	oldNodes := g.Nodes
343
+	g.Nodes = make(Nodes, 0, len(kept.Ptr))
344
+
345
+	for _, cur := range oldNodes {
346
+		// A node may not have multiple parents
347
+		if len(cur.In) > 1 {
348
+			fmt.Fprintf(os.Stderr, "ERROR: TrimTree only works on trees.\n")
349
+		}
350
+
351
+		// If a node should be kept, add it to the next list of nodes
352
+		if _, ok := kept.Ptr[cur]; ok {
353
+			g.Nodes = append(g.Nodes, cur)
354
+			continue
355
+		}
356
+
357
+		// Get the parent. Since cur.In may only be of size 0 or 1, parent will be
358
+		// equal to either nil or the only node in cur.In
359
+		var parent *Node
360
+		for _, edge := range cur.In {
361
+			parent = edge.Src
362
+		}
363
+
364
+		if parent != nil {
365
+			// Remove the edge from the parent to this node
366
+			delete(parent.Out, cur)
367
+
368
+			// Reconfigure every edge from the current node to now begin at the parent.
369
+			for _, outEdge := range cur.Out {
370
+				child := outEdge.Dest
371
+
372
+				delete(child.In, cur)
373
+				child.In[parent] = outEdge
374
+				parent.Out[child] = outEdge
375
+
376
+				outEdge.Src = parent
377
+				outEdge.Residual = true
378
+				// Any reconfigured edge can no longer be Inline.
379
+				outEdge.Inline = false
380
+			}
381
+		} else {
382
+			// If a node has no parents, delete all the in edges of the children to make them
383
+			// all roots of their own trees.
384
+			for _, outEdge := range cur.Out {
385
+				delete(outEdge.Dest.In, cur)
386
+			}
387
+		}
388
+	}
389
+	g.RemoveRedundantEdges()
390
+	return g
391
+}
392
+
334 393
 func joinLabels(s *profile.Sample) string {
335 394
 	if len(s.Label) == 0 {
336 395
 		return ""
@@ -538,12 +597,16 @@ func (g *Graph) DiscardLowFrequencyNodes(nodeCutoff int64) NodeSet {
538 597
 }
539 598
 
540 599
 func makeNodeSet(nodes Nodes, nodeCutoff int64) NodeSet {
541
-	kept := make(NodeSet, len(nodes))
600
+	kept := NodeSet{
601
+		Info: make(map[NodeInfo]bool, len(nodes)),
602
+		Ptr:  make(map[*Node]bool, len(nodes)),
603
+	}
542 604
 	for _, n := range nodes {
543 605
 		if abs64(n.Cum) < nodeCutoff {
544 606
 			continue
545 607
 		}
546
-		kept[n.Info] = true
608
+		kept.Info[n.Info] = true
609
+		kept.Ptr[n] = true
547 610
 	}
548 611
 	return kept
549 612
 }

+ 298
- 0
internal/graph/graph_test.go Bestand weergeven

@@ -0,0 +1,298 @@
1
+package graph
2
+
3
+import (
4
+	"fmt"
5
+	"testing"
6
+)
7
+
8
+func edgeDebugString(edge *Edge) string {
9
+	debug := ""
10
+	debug += fmt.Sprintf("\t\tSrc: %p\n", edge.Src)
11
+	debug += fmt.Sprintf("\t\tDest: %p\n", edge.Dest)
12
+	debug += fmt.Sprintf("\t\tResidual: %t\n", edge.Residual)
13
+	debug += fmt.Sprintf("\t\tInline: %t\n", edge.Inline)
14
+	return debug
15
+}
16
+
17
+func edgeMapsDebugString(in, out EdgeMap) string {
18
+	debug := ""
19
+	debug += "In Edges:\n"
20
+	for parent, edge := range in {
21
+		debug += fmt.Sprintf("\tParent: %p\n", parent)
22
+		debug += edgeDebugString(edge)
23
+	}
24
+	debug += "Out Edges:\n"
25
+	for child, edge := range out {
26
+		debug += fmt.Sprintf("\tChild: %p\n", child)
27
+		debug += edgeDebugString(edge)
28
+	}
29
+	return debug
30
+}
31
+
32
+func graphDebugString(graph *Graph) string {
33
+	debug := ""
34
+	for i, node := range graph.Nodes {
35
+		debug += fmt.Sprintf("Node %d: %p\n", i, node)
36
+	}
37
+
38
+	for i, node := range graph.Nodes {
39
+		debug += "\n"
40
+		debug += fmt.Sprintf("===  Node %d: %p  ===\n", i, node)
41
+		debug += edgeMapsDebugString(node.In, node.Out)
42
+	}
43
+	return debug
44
+}
45
+
46
+func expectedNodesDebugString(Expected []ExpectedNode) string {
47
+	debug := ""
48
+	for i, node := range Expected {
49
+		debug += fmt.Sprintf("Node %d: %p\n", i, node.Node)
50
+	}
51
+
52
+	for i, node := range Expected {
53
+		debug += "\n"
54
+		debug += fmt.Sprintf("===  Node %d: %p  ===\n", i, node.Node)
55
+		debug += edgeMapsDebugString(node.In, node.Out)
56
+	}
57
+	return debug
58
+}
59
+
60
+// Checks if two edges are equal
61
+func edgesEqual(this, that *Edge) bool {
62
+	return this.Src == that.Src && this.Dest == that.Dest &&
63
+		this.Residual == that.Residual && this.Inline == that.Inline
64
+}
65
+
66
+// Checks if all the edges in this equal all the edges in that.
67
+func edgeMapsEqual(this, that EdgeMap) bool {
68
+	if len(this) != len(that) {
69
+		return false
70
+	}
71
+	for node, thisEdge := range this {
72
+		if !edgesEqual(thisEdge, that[node]) {
73
+			return false
74
+		}
75
+	}
76
+	return true
77
+}
78
+
79
+// Check if node is equal to Expected
80
+func nodesEqual(node *Node, Expected ExpectedNode) bool {
81
+	return node == Expected.Node && edgeMapsEqual(node.In, Expected.In) &&
82
+		edgeMapsEqual(node.Out, Expected.Out)
83
+}
84
+
85
+// Check if the graph equals the one templated by Expected.
86
+func graphsEqual(graph *Graph, Expected []ExpectedNode) bool {
87
+	if len(graph.Nodes) != len(Expected) {
88
+		return false
89
+	}
90
+	ExpectedSet := make(map[*Node]ExpectedNode)
91
+	for i := range Expected {
92
+		ExpectedSet[Expected[i].Node] = Expected[i]
93
+	}
94
+
95
+	for _, node := range graph.Nodes {
96
+		ExpectedNode, found := ExpectedSet[node]
97
+		if !found || !nodesEqual(node, ExpectedNode) {
98
+			return false
99
+		}
100
+	}
101
+	return true
102
+}
103
+
104
+type ExpectedNode struct {
105
+	Node    *Node
106
+	In, Out EdgeMap
107
+}
108
+
109
+type TrimTreeTestCase struct {
110
+	Initial  *Graph
111
+	Expected []ExpectedNode
112
+	Keep     NodeSet
113
+}
114
+
115
+// Makes the edge from parent to child residual
116
+func makeExpectedEdgeResidual(parent, child ExpectedNode) {
117
+	parent.Out[child.Node].Residual = true
118
+	child.In[parent.Node].Residual = true
119
+}
120
+
121
+// Creates a directed edges from the parent to each of the children
122
+func createEdges(parent *Node, children ...*Node) {
123
+	for _, child := range children {
124
+		edge := &Edge{
125
+			Src:  parent,
126
+			Dest: child,
127
+		}
128
+		parent.Out[child] = edge
129
+		child.In[parent] = edge
130
+	}
131
+}
132
+
133
+// Creates a node without any edges
134
+func createEmptyNode() *Node {
135
+	return &Node{
136
+		In:  make(EdgeMap),
137
+		Out: make(EdgeMap),
138
+	}
139
+}
140
+
141
+// Creates an array of ExpectedNodes from nodes.
142
+func createExpectedNodes(nodes ...*Node) ([]ExpectedNode, NodeSet) {
143
+	Expected := make([]ExpectedNode, len(nodes))
144
+	Keep := NodeSet{
145
+		Ptr: make(map[*Node]bool, len(nodes)),
146
+	}
147
+
148
+	for i, node := range nodes {
149
+		Expected[i] = ExpectedNode{
150
+			Node: node,
151
+			In:   make(EdgeMap),
152
+			Out:  make(EdgeMap),
153
+		}
154
+		Keep.Ptr[node] = true
155
+	}
156
+
157
+	return Expected, Keep
158
+}
159
+
160
+// Creates a directed edges from the parent to each of the children
161
+func createExpectedEdges(parent ExpectedNode, children ...ExpectedNode) {
162
+	for _, child := range children {
163
+		edge := &Edge{
164
+			Src:  parent.Node,
165
+			Dest: child.Node,
166
+		}
167
+		parent.Out[child.Node] = edge
168
+		child.In[parent.Node] = edge
169
+	}
170
+}
171
+
172
+// The first test case looks like:
173
+//     0
174
+//     |
175
+//     1
176
+//   /   \
177
+//  2     3
178
+//
179
+// After Keeping 0, 2, 3. We should see:
180
+//     0
181
+//   /   \
182
+//  2     3
183
+func createTestCase1() TrimTreeTestCase {
184
+	// Create Initial graph
185
+	graph := &Graph{make(Nodes, 4)}
186
+	nodes := graph.Nodes
187
+	for i := range nodes {
188
+		nodes[i] = createEmptyNode()
189
+	}
190
+	createEdges(nodes[0], nodes[1])
191
+	createEdges(nodes[1], nodes[2], nodes[3])
192
+
193
+	// Create Expected graph
194
+	Expected, Keep := createExpectedNodes(nodes[0], nodes[2], nodes[3])
195
+	createExpectedEdges(Expected[0], Expected[1], Expected[2])
196
+	makeExpectedEdgeResidual(Expected[0], Expected[1])
197
+	makeExpectedEdgeResidual(Expected[0], Expected[2])
198
+	return TrimTreeTestCase{
199
+		Initial:  graph,
200
+		Expected: Expected,
201
+		Keep:     Keep,
202
+	}
203
+}
204
+
205
+// This test case looks like:
206
+//   3
207
+//   |
208
+//   1
209
+//   |
210
+//   2
211
+//   |
212
+//   0
213
+//   |
214
+//   4
215
+//
216
+// After Keeping 3 and 4. We should see:
217
+//   3
218
+//   |
219
+//   4
220
+func createTestCase2() TrimTreeTestCase {
221
+	// Create Initial graph
222
+	graph := &Graph{make(Nodes, 5)}
223
+	nodes := graph.Nodes
224
+	for i := range nodes {
225
+		nodes[i] = createEmptyNode()
226
+	}
227
+	createEdges(nodes[3], nodes[1])
228
+	createEdges(nodes[1], nodes[2])
229
+	createEdges(nodes[2], nodes[0])
230
+	createEdges(nodes[0], nodes[4])
231
+
232
+	// Create Expected graph
233
+	Expected, Keep := createExpectedNodes(nodes[3], nodes[4])
234
+	createExpectedEdges(Expected[0], Expected[1])
235
+	makeExpectedEdgeResidual(Expected[0], Expected[1])
236
+	return TrimTreeTestCase{
237
+		Initial:  graph,
238
+		Expected: Expected,
239
+		Keep:     Keep,
240
+	}
241
+}
242
+
243
+// If we trim an empty graph it should still be empty afterwards
244
+func createTestCase3() TrimTreeTestCase {
245
+	graph := &Graph{make(Nodes, 0)}
246
+	Expected, Keep := createExpectedNodes()
247
+	return TrimTreeTestCase{
248
+		Initial:  graph,
249
+		Expected: Expected,
250
+		Keep:     Keep,
251
+	}
252
+}
253
+
254
+// This test case looks like:
255
+//   0
256
+//
257
+// After Keeping 0. We should see:
258
+//   0
259
+func createTestCase4() TrimTreeTestCase {
260
+	graph := &Graph{make(Nodes, 1)}
261
+	nodes := graph.Nodes
262
+	for i := range nodes {
263
+		nodes[i] = createEmptyNode()
264
+	}
265
+	Expected, Keep := createExpectedNodes(nodes[0])
266
+	return TrimTreeTestCase{
267
+		Initial:  graph,
268
+		Expected: Expected,
269
+		Keep:     Keep,
270
+	}
271
+}
272
+
273
+func createTrimTreeTestCases() []TrimTreeTestCase {
274
+	caseGenerators := []func() TrimTreeTestCase{
275
+		createTestCase1,
276
+		createTestCase2,
277
+		createTestCase3,
278
+		createTestCase4,
279
+	}
280
+	cases := make([]TrimTreeTestCase, len(caseGenerators))
281
+	for i, gen := range caseGenerators {
282
+		cases[i] = gen()
283
+	}
284
+	return cases
285
+}
286
+
287
+func TestTrimTree(t *testing.T) {
288
+	tests := createTrimTreeTestCases()
289
+	for _, test := range tests {
290
+		graph := test.Initial
291
+		graph.TrimTree(test.Keep)
292
+		if !graphsEqual(graph, test.Expected) {
293
+			t.Fatalf("Graphs do not match.\nExpected: %s\nFound: %s\n",
294
+				expectedNodesDebugString(test.Expected),
295
+				graphDebugString(graph))
296
+		}
297
+	}
298
+}

+ 16
- 18
internal/report/report.go Bestand weergeven

@@ -79,26 +79,20 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
79 79
 	cumSort := o.CumSort
80 80
 
81 81
 	// First step: Build complete graph to identify low frequency nodes, based on their cum weight.
82
-	g = rpt.newGraph(nil)
82
+	g = rpt.newGraph(graph.NodeSet{nil, nil})
83 83
 	totalValue, _ := g.Nodes.Sum()
84 84
 	nodeCutoff := abs64(int64(float64(totalValue) * o.NodeFraction))
85 85
 	edgeCutoff := abs64(int64(float64(totalValue) * o.EdgeFraction))
86 86
 
87
-	// Do not apply edge cutoff to preserve tree structure.
88
-	if o.CallTree {
89
-		if o.OutputFormat == Dot {
90
-			fmt.Println("WARNING: Trimming trees is unsupported.")
91
-			fmt.Printf("Tree will contain at least %d nodes\n", o.NodeCount)
92
-			cumSort = true
93
-		}
94
-		edgeCutoff = 0
95
-	}
96
-
97 87
 	// Filter out nodes with cum value below nodeCutoff.
98 88
 	if nodeCutoff > 0 {
99
-		if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept) {
100
-			droppedNodes = len(g.Nodes) - len(nodesKept)
101
-			g = rpt.newGraph(nodesKept)
89
+		if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept.Ptr) {
90
+			droppedNodes = len(g.Nodes) - len(nodesKept.Ptr)
91
+			if o.CallTree {
92
+				g = g.TrimTree(nodesKept)
93
+			} else {
94
+				g = rpt.newGraph(nodesKept)
95
+			}
102 96
 		}
103 97
 	}
104 98
 	origCount = len(g.Nodes)
@@ -110,8 +104,12 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
110 104
 		// Remove low frequency tags and edges as they affect selection.
111 105
 		g.TrimLowFrequencyTags(nodeCutoff)
112 106
 		g.TrimLowFrequencyEdges(edgeCutoff)
113
-		if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(nodesKept) != len(g.Nodes) {
114
-			g = rpt.newGraph(nodesKept)
107
+		if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(nodesKept.Ptr) != len(g.Nodes) {
108
+			if o.CallTree {
109
+				g = g.TrimTree(nodesKept)
110
+			} else {
111
+				g = rpt.newGraph(nodesKept)
112
+			}
115 113
 			g.SortNodes(cumSort, visualMode)
116 114
 		}
117 115
 	}
@@ -272,7 +270,7 @@ func printAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
272 270
 	o := rpt.options
273 271
 	prof := rpt.prof
274 272
 
275
-	g := rpt.newGraph(nil)
273
+	g := rpt.newGraph(graph.NodeSet{nil, nil})
276 274
 
277 275
 	// If the regexp source can be parsed as an address, also match
278 276
 	// functions that land on that address.
@@ -580,7 +578,7 @@ func printTraces(w io.Writer, rpt *Report) error {
580 578
 
581 579
 	const separator = "-----------+-------------------------------------------------------"
582 580
 
583
-	_, locations := graph.CreateNodes(prof, false, nil)
581
+	_, locations := graph.CreateNodes(prof, false, graph.NodeSet{nil, nil})
584 582
 	for _, sample := range prof.Sample {
585 583
 		var stack graph.Nodes
586 584
 		for _, loc := range sample.Location {

+ 2
- 2
internal/report/source.go Bestand weergeven

@@ -37,7 +37,7 @@ import (
37 37
 // eliminate potential nondeterminism.
38 38
 func printSource(w io.Writer, rpt *Report) error {
39 39
 	o := rpt.options
40
-	g := rpt.newGraph(nil)
40
+	g := rpt.newGraph(graph.NodeSet{nil, nil})
41 41
 
42 42
 	// Identify all the functions that match the regexp provided.
43 43
 	// Group nodes for each matching function.
@@ -117,7 +117,7 @@ func printSource(w io.Writer, rpt *Report) error {
117 117
 // functions with samples that match the regexp rpt.options.symbol.
118 118
 func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
119 119
 	o := rpt.options
120
-	g := rpt.newGraph(nil)
120
+	g := rpt.newGraph(graph.NodeSet{nil, nil})
121 121
 
122 122
 	// If the regexp source can be parsed as an address, also match
123 123
 	// functions that land on that address.

+ 12
- 12
internal/report/testdata/source.dot Bestand weergeven

@@ -1,17 +1,17 @@
1 1
 digraph "unnamed" {
2 2
 node [style=filled fillcolor="#f8f8f8"]
3 3
 subgraph cluster_L { "Duration: 10s, Total samples = 11111 " [shape=box fontsize=16 label="Duration: 10s, Total samples = 11111 \lShowing nodes accounting for 11111, 100% of 11111 total\l"] }
4
-N1 [label="main\nsource1:2\n1 (0.009%)\nof 11111 (100%)" fontsize=9 shape=box tooltip="main testdata/source1:2 (11111)" color="#b20000" fillcolor="#edd5d5"]
5
-N2 [label="tee\nsource2:2\n1000 (9.00%)\nof 11000 (99.00%)" fontsize=14 shape=box tooltip="tee testdata/source2:2 (11000)" color="#b20000" fillcolor="#edd5d5"]
6
-N3 [label="tee\nsource2:8\n10000 (90.00%)" fontsize=24 shape=box tooltip="tee testdata/source2:8 (10000)" color="#b20500" fillcolor="#edd6d5"]
7
-N4 [label="bar\nsource1:10\n0 of 100 (0.9%)" fontsize=8 shape=box tooltip="bar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"]
8
-N5 [label="tee\nsource2:8\n100 (0.9%)" fontsize=10 shape=box tooltip="tee testdata/source2:8 (100)" color="#b2b0aa" fillcolor="#edecec"]
9
-N6 [label="bar\nsource1:10\n10 (0.09%)" fontsize=9 shape=box tooltip="bar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"]
4
+N1 [label="tee\nsource2:8\n10000 (90.00%)" fontsize=24 shape=box tooltip="tee testdata/source2:8 (10000)" color="#b20500" fillcolor="#edd6d5"]
5
+N2 [label="main\nsource1:2\n1 (0.009%)\nof 11111 (100%)" fontsize=9 shape=box tooltip="main testdata/source1:2 (11111)" color="#b20000" fillcolor="#edd5d5"]
6
+N3 [label="tee\nsource2:2\n1000 (9.00%)\nof 11000 (99.00%)" fontsize=14 shape=box tooltip="tee testdata/source2:2 (11000)" color="#b20000" fillcolor="#edd5d5"]
7
+N4 [label="tee\nsource2:8\n100 (0.9%)" fontsize=10 shape=box tooltip="tee testdata/source2:8 (100)" color="#b2b0aa" fillcolor="#edecec"]
8
+N5 [label="bar\nsource1:10\n10 (0.09%)" fontsize=9 shape=box tooltip="bar testdata/source1:10 (10)" color="#b2b2b1" fillcolor="#ededed"]
9
+N6 [label="bar\nsource1:10\n0 of 100 (0.9%)" fontsize=8 shape=box tooltip="bar testdata/source1:10 (100)" color="#b2b0aa" fillcolor="#edecec"]
10 10
 N7 [label="foo\nsource1:4\n0 of 10 (0.09%)" fontsize=8 shape=box tooltip="foo testdata/source1:4 (10)" color="#b2b2b1" fillcolor="#ededed"]
11
-N1 -> N2 [label=" 11000" weight=100 penwidth=5 color="#b20000" tooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)" labeltooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)"]
12
-N2 -> N3 [label=" 10000" weight=91 penwidth=5 color="#b20500" tooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)" labeltooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)"]
13
-N4 -> N5 [label=" 100" color="#b2b0aa" tooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)" labeltooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)"]
14
-N1 -> N4 [label=" 100" color="#b2b0aa" tooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)" labeltooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)"]
15
-N7 -> N6 [label=" 10" color="#b2b2b1" tooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)" labeltooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)"]
16
-N1 -> N7 [label=" 10" color="#b2b2b1" tooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)" labeltooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)"]
11
+N2 -> N3 [label=" 11000" weight=100 penwidth=5 color="#b20000" tooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)" labeltooltip="main testdata/source1:2 -> tee testdata/source2:2 (11000)"]
12
+N3 -> N1 [label=" 10000" weight=91 penwidth=5 color="#b20500" tooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)" labeltooltip="tee testdata/source2:2 -> tee testdata/source2:8 (10000)"]
13
+N6 -> N4 [label=" 100" color="#b2b0aa" tooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)" labeltooltip="bar testdata/source1:10 -> tee testdata/source2:8 (100)"]
14
+N2 -> N6 [label=" 100" color="#b2b0aa" tooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)" labeltooltip="main testdata/source1:2 -> bar testdata/source1:10 (100)"]
15
+N7 -> N5 [label=" 10" color="#b2b2b1" tooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)" labeltooltip="foo testdata/source1:4 -> bar testdata/source1:10 (10)"]
16
+N2 -> N7 [label=" 10" color="#b2b2b1" tooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)" labeltooltip="main testdata/source1:2 -> foo testdata/source1:4 (10)"]
17 17
 }