@cryptotaxi247 / kubo / commits / b8757d18e

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