diag/net: visualizing in d3 and dot
Try it out: ``` ipfs net diag --vis=d3 | diagnostics/d3/d3view ``` Notes: this is not the best way to do it, because it breaks `--encoding=json`. Not sure what the best way is, and right now this provides more utility than the other.
Juan Batiz-Benet committed
Jan 13, 2015 at 21:18 UTC
b8757d18eff8c419a36b00931be4929132ce926a
4 files changed
+221
-36
core/commands/diag.go
+57
-34
@@ -3,11 +3,12 @@ package commands
3
import (
4
"bytes"
5
"io"
6
+ "strings"
7
"text/template"
8
"time"
9
10
cmds "github.com/jbenet/go-ipfs/commands"
10
- util "github.com/jbenet/go-ipfs/util"
11
+ diag "github.com/jbenet/go-ipfs/diagnostics"
12
)
13
14
type DiagnosticConnection struct {
@@ -17,6 +18,12 @@ type DiagnosticConnection struct {
18
Count int
19
}
20
21
+var (
22
+ visD3 = "d3"
23
+ visDot = "dot"
24
+ visFmts = []string{visD3, visDot}
25
+)
26
+
27
type DiagnosticPeer struct {
28
ID string
29
UptimeSeconds uint64
@@ -49,6 +56,10 @@ connected peers and latencies between them.
56
`,
57
},
58
59
+ Options: []cmds.Option{
60
+ cmds.StringOption("vis", "output vis. one of: "+strings.Join(visFmts, ", ")),
61
+ },
62
+
63
Run: func(req cmds.Request) (interface{}, error) {
64
n, err := req.Context().GetNode()
65
if err != nil {
@@ -59,48 +70,60 @@ connected peers and latencies between them.
70
return nil, errNotOnline
71
}
72
62
- info, err := n.Diagnostics.GetDiagnostic(time.Second * 20)
73
+ vis, _, err := req.Option("vis").String()
74
if err != nil {
75
return nil, err
76
}
77
67
- output := make([]DiagnosticPeer, len(info))
68
- for i, peer := range info {
69
- connections := make([]DiagnosticConnection, len(peer.Connections))
70
- for j, conn := range peer.Connections {
71
- connections[j] = DiagnosticConnection{
72
- ID: conn.ID,
73
- NanosecondsLatency: uint64(conn.Latency.Nanoseconds()),
74
- Count: conn.Count,
75
- }
76
- }
78
+ info, err := n.Diagnostics.GetDiagnostic(time.Second * 20)
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
78
- output[i] = DiagnosticPeer{
79
- ID: peer.ID,
80
- UptimeSeconds: uint64(peer.LifeSpan.Seconds()),
81
- BandwidthBytesIn: peer.BwIn,
82
- BandwidthBytesOut: peer.BwOut,
83
- Connections: connections,
84
- }
83
+ switch vis {
84
+ case visD3:
85
+ return bytes.NewReader(diag.GetGraphJson(info)), nil
86
+ case visDot:
87
+ var buf bytes.Buffer
88
+ w := diag.DotWriter{W: &buf}
89
+ err := w.WriteGraph(info)
90
+ return io.Reader(&buf), err
91
}
92
87
- return &DiagnosticOutput{output}, nil
93
+ return stdDiagOutputMarshal(standardDiagOutput(info))
94
},
89
- Type: DiagnosticOutput{},
90
- Marshalers: cmds.MarshalerMap{
91
- cmds.Text: func(r cmds.Response) (io.Reader, error) {
92
- output, ok := r.Output().(*DiagnosticOutput)
93
- if !ok {
94
- return nil, util.ErrCast()
95
- }
96
- var buf bytes.Buffer
97
- err := printDiagnostics(&buf, output)
98
- if err != nil {
99
- return nil, err
95
+}
96
+
97
+func stdDiagOutputMarshal(output *DiagnosticOutput) (io.Reader, error) {
98
+ var buf bytes.Buffer
99
+ err := printDiagnostics(&buf, output)
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+ return &buf, nil
104
+}
105
+
106
+func standardDiagOutput(info []*diag.DiagInfo) *DiagnosticOutput {
107
+ output := make([]DiagnosticPeer, len(info))
108
+ for i, peer := range info {
109
+ connections := make([]DiagnosticConnection, len(peer.Connections))
110
+ for j, conn := range peer.Connections {
111
+ connections[j] = DiagnosticConnection{
112
+ ID: conn.ID,
113
+ NanosecondsLatency: uint64(conn.Latency.Nanoseconds()),
114
+ Count: conn.Count,
115
}
101
- return &buf, nil
102
- },
103
- },
116
+ }
117
+
118
+ output[i] = DiagnosticPeer{
119
+ ID: peer.ID,
120
+ UptimeSeconds: uint64(peer.LifeSpan.Seconds()),
121
+ BandwidthBytesIn: peer.BwIn,
122
+ BandwidthBytesOut: peer.BwOut,
123
+ Connections: connections,
124
+ }
125
+ }
126
+ return &DiagnosticOutput{output}
127
}
128
129
func printDiagnostics(out io.Writer, info *DiagnosticOutput) error {
diagnostics/d3/d3view
new
+22
@@ -0,0 +1,22 @@
1
+#!/bin/sh
2
+
3
+# put stdin in temp file
4
+file=`mktemp -t d3view`
5
+cat >"$file"
6
+
7
+# add file to ipfs
8
+hash=$(ipfs add -q "$file" </dev/null | tail -n1)
9
+
10
+# this viewer is the hash of go-ipfs/diagnostics/d3/viewer.html
11
+viewer="QmaY6Lq9MEhDfWUc1VfHcu9aLWSyvi4VDLvWQXLoVZ4Mau"
12
+
13
+# the ipfs gateway to use
14
+gatewayHTTP="http://ipfs.benet.ai:8080"
15
+gatewayIPFS="/ip4/104.236.32.22/tcp/4001/Qme7peMbkRH8qzb9TMXSoRwVmVDZz3Z4dseRXAyBwBmxA7"
16
+
17
+# make sure you're reachable (no NAT yet)
18
+ipfs swarm connect "$gatewayIPFS" </dev/null >/dev/null
19
+
20
+# output the url at the gateway
21
+url="$gatewayHTTP/ipfs/$viewer#$hash"
22
+echo "$url"
diagnostics/d3/viewer.html
new
+59
@@ -0,0 +1,59 @@
1
+<!DOCTYPE html>
2
+<meta charset="utf-8">
3
+<style>
4
+.node {
5
+ stroke: #fff;
6
+ stroke-width: 1.5px;
7
+}
8
+.link {
9
+ stroke: #999;
10
+ stroke-opacity: .6;
11
+}
12
+</style>
13
+<body>
14
+ <h1>Ipfs Visualization</h1>
15
+<script src="http://d3js.org/d3.v3.min.js"></script>
16
+<script>
17
+var hash = window.location.hash.substring(1)
18
+
19
+var width = 960,
20
+ height = 800;
21
+var color = d3.scale.category20();
22
+var force = d3.layout.force()
23
+ .charge(-50)
24
+ .linkDistance(90)
25
+ .gravity(0.01)
26
+ .size([width, height]);
27
+var svg = d3.select("body").append("svg")
28
+ .attr("width", width)
29
+ .attr("height", height);
30
+
31
+d3.json(hash, function(error, graph) {
32
+ force
33
+ .nodes(graph.nodes)
34
+ .links(graph.links)
35
+ .start();
36
+ var link = svg.selectAll(".link")
37
+ .data(graph.links)
38
+ .enter().append("line")
39
+ .attr("class", "link")
40
+ .style("stroke-width", function(d) { return Math.sqrt(d.value); });
41
+ var node = svg.selectAll(".node")
42
+ .data(graph.nodes)
43
+ .enter().append("circle")
44
+ .attr("class", "node")
45
+ .attr("r", function(d) {return 1.0 + Math.log(d.value)})
46
+ .style("fill", function(d) { return color(d.group); })
47
+ .call(force.drag);
48
+ node.append("title")
49
+ .text(function(d) { return d.name; });
50
+ force.on("tick", function() {
51
+ link.attr("x1", function(d) { return d.source.x; })
52
+ .attr("y1", function(d) { return d.source.y; })
53
+ .attr("x2", function(d) { return d.target.x; })
54
+ .attr("y2", function(d) { return d.target.y; });
55
+ node.attr("cx", function(d) { return d.x; })
56
+ .attr("cy", function(d) { return d.y; });
57
+ });
58
+});
59
+</script>
diagnostics/vis.go
+83
-2
@@ -1,6 +1,10 @@
1
package diagnostics
2
3
-import "encoding/json"
3
+import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+)
8
9
type node struct {
10
Name string `json:"name"`
@@ -19,7 +23,7 @@ func GetGraphJson(dinfo []*DiagInfo) []byte {
23
var nodes []*node
24
for _, di := range dinfo {
25
names[di.ID] = len(nodes)
22
- val := di.BwIn + di.BwOut
26
+ val := di.BwIn + di.BwOut + 10
27
nodes = append(nodes, &node{Name: di.ID, Value: val})
28
}
29
@@ -54,3 +58,80 @@ func GetGraphJson(dinfo []*DiagInfo) []byte {
58
59
return b
60
}
61
+
62
+type DotWriter struct {
63
+ W io.Writer
64
+ err error
65
+}
66
+
67
+// Write writes a buffer to the internal writer.
68
+// It handles errors as in: http://blog.golang.org/errors-are-values
69
+func (w *DotWriter) Write(buf []byte) (n int, err error) {
70
+ if w.err == nil {
71
+ n, w.err = w.W.Write(buf)
72
+ }
73
+ return n, w.err
74
+}
75
+
76
+// WriteS writes a string
77
+func (w *DotWriter) WriteS(s string) (n int, err error) {
78
+ return w.Write([]byte(s))
79
+}
80
+
81
+func (w *DotWriter) WriteNetHeader(dinfo []*DiagInfo) error {
82
+ label := fmt.Sprintf("Nodes: %d\\l", len(dinfo))
83
+
84
+ w.WriteS("subgraph cluster_L { ")
85
+ w.WriteS("L [shape=box fontsize=32 label=\"" + label + "\"] ")
86
+ w.WriteS("}\n")
87
+ return w.err
88
+}
89
+
90
+func (w *DotWriter) WriteNode(i int, di *DiagInfo) error {
91
+ box := "[label=\"%s\n%d conns\" fontsize=8 shape=box tooltip=\"%s (%d conns)\"]"
92
+ box = fmt.Sprintf(box, di.ID, len(di.Connections), di.ID, len(di.Connections))
93
+
94
+ w.WriteS(fmt.Sprintf("N%d %s\n", i, box))
95
+ return w.err
96
+}
97
+
98
+func (w *DotWriter) WriteEdge(i, j int, di *DiagInfo, conn connDiagInfo) error {
99
+
100
+ n := fmt.Sprintf("%s ... %s (%d)", di.ID, conn.ID, conn.Latency)
101
+ s := "[label=\" %d\" weight=%d tooltip=\"%s\" labeltooltip=\"%s\" style=\"dotted\"]"
102
+ s = fmt.Sprintf(s, conn.Latency, conn.Count, n, n)
103
+
104
+ w.WriteS(fmt.Sprintf("N%d -> N%d %s\n", i, j, s))
105
+ return w.err
106
+}
107
+
108
+func (w *DotWriter) WriteGraph(dinfo []*DiagInfo) error {
109
+ w.WriteS("digraph \"diag-net\" {\n")
110
+ w.WriteNetHeader(dinfo)
111
+
112
+ idx := make(map[string]int)
113
+ for i, di := range dinfo {
114
+ if _, found := idx[di.ID]; found {
115
+ log.Debugf("DotWriter skipped duplicate %s", di.ID)
116
+ continue
117
+ }
118
+
119
+ idx[di.ID] = i
120
+ w.WriteNode(i, di)
121
+ }
122
+
123
+ for i, di := range dinfo {
124
+ for _, conn := range di.Connections {
125
+ j, found := idx[conn.ID]
126
+ if !found { // if we didnt get it earlier...
127
+ j = len(idx)
128
+ idx[conn.ID] = j
129
+ }
130
+
131
+ w.WriteEdge(i, j, di, conn)
132
+ }
133
+ }
134
+
135
+ w.WriteS("}")
136
+ return w.err
137
+}