Просмотр исходного кода

Merge pull request #23 from wade-k/fix_tree_trimming

Fix tree trimming
Raul Silvera 8 лет назад
Родитель
Сommit
ca80e2c92f
4 измененных файлов: 460 добавлений и 35 удалений
  1. 114
    7
      internal/graph/graph.go
  2. 314
    0
      internal/graph/graph_test.go
  3. 20
    16
      internal/report/report.go
  4. 12
    12
      internal/report/testdata/source.dot

+ 114
- 7
internal/graph/graph.go Просмотреть файл

@@ -141,9 +141,19 @@ func (i *NodeInfo) NameComponents() []string {
141 141
 // report entries with the same info.
142 142
 type NodeMap map[NodeInfo]*Node
143 143
 
144
-// NodeSet maps is a collection of node info structs.
144
+// NodeSet is a collection of node info structs.
145 145
 type NodeSet map[NodeInfo]bool
146 146
 
147
+// NodePtrSet is a collection of nodes. Trimming a graph or tree requires a set
148
+// of objects which uniquely identify the nodes to keep. In a graph, NodeInfo
149
+// works as a unique identifier; however, in a tree multiple nodes may share
150
+// identical NodeInfos. A *Node does uniquely identify a node so we can use that
151
+// instead. Though a *Node also uniquely identifies a node in a graph,
152
+// currently, during trimming, graphs are rebult from scratch using only the
153
+// NodeSet, so there would not be the required context of the initial graph to
154
+// allow for the use of *Node.
155
+type NodePtrSet map[*Node]bool
156
+
147 157
 // FindOrInsertNode takes the info for a node and either returns a matching node
148 158
 // from the node map if one exists, or adds one to the map if one does not.
149 159
 // If kept is non-nil, nodes are only added if they can be located on it.
@@ -288,7 +298,6 @@ func newTree(prof *profile.Profile, o *Options) (g *Graph) {
288 298
 	kept := o.KeptNodes
289 299
 	keepBinary := o.ObjNames
290 300
 	parentNodeMap := make(map[*Node]NodeMap, len(prof.Sample))
291
-nextSample:
292 301
 	for _, sample := range prof.Sample {
293 302
 		weight := o.SampleValue(sample.Value)
294 303
 		if weight == 0 {
@@ -311,7 +320,7 @@ nextSample:
311 320
 				}
312 321
 				n := nodeMap.findOrInsertLine(l, lines[lidx], keepBinary, kept)
313 322
 				if n == nil {
314
-					continue nextSample
323
+					continue
315 324
 				}
316 325
 				n.addSample(weight, labels, sample.NumLabel, o.FormatTag, false)
317 326
 				if parent != nil {
@@ -332,6 +341,68 @@ nextSample:
332 341
 	return selectNodesForGraph(nodes, o.DropNegative)
333 342
 }
334 343
 
344
+// TrimTree trims a Graph in forest form, keeping only the nodes in kept. This
345
+// will not work correctly if even a single node has multiple parents.
346
+func (g *Graph) TrimTree(kept NodePtrSet) {
347
+	// Creates a new list of nodes
348
+	oldNodes := g.Nodes
349
+	g.Nodes = make(Nodes, 0, len(kept))
350
+
351
+	for _, cur := range oldNodes {
352
+		// A node may not have multiple parents
353
+		if len(cur.In) > 1 {
354
+			panic("TrimTree only works on trees")
355
+		}
356
+
357
+		// If a node should be kept, add it to the new list of nodes
358
+		if _, ok := kept[cur]; ok {
359
+			g.Nodes = append(g.Nodes, cur)
360
+			continue
361
+		}
362
+
363
+		// If a node has no parents, then delete all of the in edges of its
364
+		// children to make them each roots of their own trees.
365
+		if len(cur.In) == 0 {
366
+			for _, outEdge := range cur.Out {
367
+				delete(outEdge.Dest.In, cur)
368
+			}
369
+			continue
370
+		}
371
+
372
+		// Get the parent. This works since at this point cur.In must contain only
373
+		// one element.
374
+		if len(cur.In) != 1 {
375
+			panic("Get parent assertion failed. cur.In expected to be of length 1.")
376
+		}
377
+		var parent *Node
378
+		for _, edge := range cur.In {
379
+			parent = edge.Src
380
+		}
381
+
382
+		parentEdgeInline := parent.Out[cur].Inline
383
+
384
+		// Remove the edge from the parent to this node
385
+		delete(parent.Out, cur)
386
+
387
+		// Reconfigure every edge from the current node to now begin at the parent.
388
+		for _, outEdge := range cur.Out {
389
+			child := outEdge.Dest
390
+
391
+			delete(child.In, cur)
392
+			child.In[parent] = outEdge
393
+			parent.Out[child] = outEdge
394
+
395
+			outEdge.Src = parent
396
+			outEdge.Residual = true
397
+			// If the edge from the parent to the current node and the edge from the
398
+			// current node to the child are both inline, then this resulting residual
399
+			// edge should also be inline
400
+			outEdge.Inline = parentEdgeInline && outEdge.Inline
401
+		}
402
+	}
403
+	g.RemoveRedundantEdges()
404
+}
405
+
335 406
 func joinLabels(s *profile.Sample) string {
336 407
 	if len(s.Label) == 0 {
337 408
 		return ""
@@ -538,15 +609,37 @@ func (g *Graph) DiscardLowFrequencyNodes(nodeCutoff int64) NodeSet {
538 609
 	return makeNodeSet(g.Nodes, nodeCutoff)
539 610
 }
540 611
 
612
+// DiscardLowFrequencyNodePtrs returns a NodePtrSet of nodes at or over a
613
+// specific cum value cutoff.
614
+func (g *Graph) DiscardLowFrequencyNodePtrs(nodeCutoff int64) NodePtrSet {
615
+	cutNodes := getNodesAboveCumCutoff(g.Nodes, nodeCutoff)
616
+	kept := make(NodePtrSet, len(cutNodes))
617
+	for _, n := range cutNodes {
618
+		kept[n] = true
619
+	}
620
+	return kept
621
+}
622
+
541 623
 func makeNodeSet(nodes Nodes, nodeCutoff int64) NodeSet {
542
-	kept := make(NodeSet, len(nodes))
624
+	cutNodes := getNodesAboveCumCutoff(nodes, nodeCutoff)
625
+	kept := make(NodeSet, len(cutNodes))
626
+	for _, n := range cutNodes {
627
+		kept[n.Info] = true
628
+	}
629
+	return kept
630
+}
631
+
632
+// getNodesAboveCumCutoff returns all the nodes which have a Cum value greater
633
+// than or equal to cutoff.
634
+func getNodesAboveCumCutoff(nodes Nodes, nodeCutoff int64) Nodes {
635
+	cutoffNodes := make(Nodes, 0, len(nodes))
543 636
 	for _, n := range nodes {
544 637
 		if abs64(n.Cum) < nodeCutoff {
545 638
 			continue
546 639
 		}
547
-		kept[n.Info] = true
640
+		cutoffNodes = append(cutoffNodes, n)
548 641
 	}
549
-	return kept
642
+	return cutoffNodes
550 643
 }
551 644
 
552 645
 // TrimLowFrequencyTags removes tags that have less than
@@ -601,8 +694,22 @@ func (g *Graph) SortNodes(cum bool, visualMode bool) {
601 694
 	}
602 695
 }
603 696
 
697
+// SelectTopNodePtrs returns a set of the top maxNodes *Node in a graph.
698
+func (g *Graph) SelectTopNodePtrs(maxNodes int, visualMode bool) NodePtrSet {
699
+	set := make(NodePtrSet)
700
+	for _, node := range g.selectTopNodes(maxNodes, visualMode) {
701
+		set[node] = true
702
+	}
703
+	return set
704
+}
705
+
604 706
 // SelectTopNodes returns a set of the top maxNodes nodes in a graph.
605 707
 func (g *Graph) SelectTopNodes(maxNodes int, visualMode bool) NodeSet {
708
+	return makeNodeSet(g.selectTopNodes(maxNodes, visualMode), 0)
709
+}
710
+
711
+// selectTopNodes returns a slice of the top maxNodes nodes in a graph.
712
+func (g *Graph) selectTopNodes(maxNodes int, visualMode bool) Nodes {
606 713
 	if maxNodes > 0 {
607 714
 		if visualMode {
608 715
 			var count int
@@ -619,7 +726,7 @@ func (g *Graph) SelectTopNodes(maxNodes int, visualMode bool) NodeSet {
619 726
 	if maxNodes > len(g.Nodes) {
620 727
 		maxNodes = len(g.Nodes)
621 728
 	}
622
-	return makeNodeSet(g.Nodes[:maxNodes], 0)
729
+	return g.Nodes[:maxNodes]
623 730
 }
624 731
 
625 732
 // countTags counts the tags with flat count. This underestimates the

+ 314
- 0
internal/graph/graph_test.go Просмотреть файл

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

+ 20
- 16
internal/report/report.go Просмотреть файл

@@ -84,21 +84,18 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
84 84
 	nodeCutoff := abs64(int64(float64(totalValue) * o.NodeFraction))
85 85
 	edgeCutoff := abs64(int64(float64(totalValue) * o.EdgeFraction))
86 86
 
87
-	// Visual mode optimization only supports graph output, not tree.
88
-	// Do not apply edge cutoff to preserve tree structure.
89
-	if o.CallTree {
90
-		visualMode = false
91
-		if o.OutputFormat == Dot {
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 o.CallTree {
90
+			if nodesKept := g.DiscardLowFrequencyNodePtrs(nodeCutoff); len(g.Nodes) != len(nodesKept) {
91
+				droppedNodes = len(g.Nodes) - len(nodesKept)
92
+				g.TrimTree(nodesKept)
93
+			}
94
+		} else {
95
+			if nodesKept := g.DiscardLowFrequencyNodes(nodeCutoff); len(g.Nodes) != len(nodesKept) {
96
+				droppedNodes = len(g.Nodes) - len(nodesKept)
97
+				g = rpt.newGraph(nodesKept)
98
+			}
102 99
 		}
103 100
 	}
104 101
 	origCount = len(g.Nodes)
@@ -110,9 +107,16 @@ func (rpt *Report) newTrimmedGraph() (g *graph.Graph, origCount, droppedNodes, d
110 107
 		// Remove low frequency tags and edges as they affect selection.
111 108
 		g.TrimLowFrequencyTags(nodeCutoff)
112 109
 		g.TrimLowFrequencyEdges(edgeCutoff)
113
-		if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(nodesKept) != len(g.Nodes) {
114
-			g = rpt.newGraph(nodesKept)
115
-			g.SortNodes(cumSort, visualMode)
110
+		if o.CallTree {
111
+			if nodesKept := g.SelectTopNodePtrs(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) {
112
+				g.TrimTree(nodesKept)
113
+				g.SortNodes(cumSort, visualMode)
114
+			}
115
+		} else {
116
+			if nodesKept := g.SelectTopNodes(nodeCount, visualMode); len(g.Nodes) != len(nodesKept) {
117
+				g = rpt.newGraph(nodesKept)
118
+				g.SortNodes(cumSort, visualMode)
119
+			}
116 120
 		}
117 121
 	}
118 122
 

+ 12
- 12
internal/report/testdata/source.dot Просмотреть файл

@@ -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
 }