From b8757d18eff8c419a36b00931be4929132ce926a Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 13 Jan 2015 21:18:05 -0800 Subject: [PATCH 1/5] 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. --- core/commands/diag.go | 91 ++++++++++++++++++++++++-------------- diagnostics/d3/d3view | 22 +++++++++ diagnostics/d3/viewer.html | 59 ++++++++++++++++++++++++ diagnostics/vis.go | 85 ++++++++++++++++++++++++++++++++++- 4 files changed, 221 insertions(+), 36 deletions(-) create mode 100755 diagnostics/d3/d3view create mode 100644 diagnostics/d3/viewer.html diff --git a/core/commands/diag.go b/core/commands/diag.go index e817f939f..ca4285871 100644 --- a/core/commands/diag.go +++ b/core/commands/diag.go @@ -3,11 +3,12 @@ package commands import ( "bytes" "io" + "strings" "text/template" "time" cmds "github.com/jbenet/go-ipfs/commands" - util "github.com/jbenet/go-ipfs/util" + diag "github.com/jbenet/go-ipfs/diagnostics" ) type DiagnosticConnection struct { @@ -17,6 +18,12 @@ type DiagnosticConnection struct { Count int } +var ( + visD3 = "d3" + visDot = "dot" + visFmts = []string{visD3, visDot} +) + type DiagnosticPeer struct { ID string UptimeSeconds uint64 @@ -49,6 +56,10 @@ connected peers and latencies between them. `, }, + Options: []cmds.Option{ + cmds.StringOption("vis", "output vis. one of: "+strings.Join(visFmts, ", ")), + }, + Run: func(req cmds.Request) (interface{}, error) { n, err := req.Context().GetNode() if err != nil { @@ -59,48 +70,60 @@ connected peers and latencies between them. return nil, errNotOnline } + vis, _, err := req.Option("vis").String() + if err != nil { + return nil, err + } + info, err := n.Diagnostics.GetDiagnostic(time.Second * 20) if err != nil { return nil, err } - output := make([]DiagnosticPeer, len(info)) - for i, peer := range info { - connections := make([]DiagnosticConnection, len(peer.Connections)) - for j, conn := range peer.Connections { - connections[j] = DiagnosticConnection{ - ID: conn.ID, - NanosecondsLatency: uint64(conn.Latency.Nanoseconds()), - Count: conn.Count, - } - } + switch vis { + case visD3: + return bytes.NewReader(diag.GetGraphJson(info)), nil + case visDot: + var buf bytes.Buffer + w := diag.DotWriter{W: &buf} + err := w.WriteGraph(info) + return io.Reader(&buf), err + } - output[i] = DiagnosticPeer{ - ID: peer.ID, - UptimeSeconds: uint64(peer.LifeSpan.Seconds()), - BandwidthBytesIn: peer.BwIn, - BandwidthBytesOut: peer.BwOut, - Connections: connections, + return stdDiagOutputMarshal(standardDiagOutput(info)) + }, +} + +func stdDiagOutputMarshal(output *DiagnosticOutput) (io.Reader, error) { + var buf bytes.Buffer + err := printDiagnostics(&buf, output) + if err != nil { + return nil, err + } + return &buf, nil +} + +func standardDiagOutput(info []*diag.DiagInfo) *DiagnosticOutput { + output := make([]DiagnosticPeer, len(info)) + for i, peer := range info { + connections := make([]DiagnosticConnection, len(peer.Connections)) + for j, conn := range peer.Connections { + connections[j] = DiagnosticConnection{ + ID: conn.ID, + NanosecondsLatency: uint64(conn.Latency.Nanoseconds()), + Count: conn.Count, } } - return &DiagnosticOutput{output}, nil - }, - Type: DiagnosticOutput{}, - Marshalers: cmds.MarshalerMap{ - cmds.Text: func(r cmds.Response) (io.Reader, error) { - output, ok := r.Output().(*DiagnosticOutput) - if !ok { - return nil, util.ErrCast() - } - var buf bytes.Buffer - err := printDiagnostics(&buf, output) - if err != nil { - return nil, err - } - return &buf, nil - }, - }, + output[i] = DiagnosticPeer{ + ID: peer.ID, + UptimeSeconds: uint64(peer.LifeSpan.Seconds()), + BandwidthBytesIn: peer.BwIn, + BandwidthBytesOut: peer.BwOut, + Connections: connections, + } + } + return &DiagnosticOutput{output} } func printDiagnostics(out io.Writer, info *DiagnosticOutput) error { diff --git a/diagnostics/d3/d3view b/diagnostics/d3/d3view new file mode 100755 index 000000000..a3f4932e1 --- /dev/null +++ b/diagnostics/d3/d3view @@ -0,0 +1,22 @@ +#!/bin/sh + +# put stdin in temp file +file=`mktemp -t d3view` +cat >"$file" + +# add file to ipfs +hash=$(ipfs add -q "$file" /dev/null + +# output the url at the gateway +url="$gatewayHTTP/ipfs/$viewer#$hash" +echo "$url" diff --git a/diagnostics/d3/viewer.html b/diagnostics/d3/viewer.html new file mode 100644 index 000000000..03251ca65 --- /dev/null +++ b/diagnostics/d3/viewer.html @@ -0,0 +1,59 @@ + + + + +

Ipfs Visualization

+ + diff --git a/diagnostics/vis.go b/diagnostics/vis.go index def418e5c..77cf29dc5 100644 --- a/diagnostics/vis.go +++ b/diagnostics/vis.go @@ -1,6 +1,10 @@ package diagnostics -import "encoding/json" +import ( + "encoding/json" + "fmt" + "io" +) type node struct { Name string `json:"name"` @@ -19,7 +23,7 @@ func GetGraphJson(dinfo []*DiagInfo) []byte { var nodes []*node for _, di := range dinfo { names[di.ID] = len(nodes) - val := di.BwIn + di.BwOut + val := di.BwIn + di.BwOut + 10 nodes = append(nodes, &node{Name: di.ID, Value: val}) } @@ -54,3 +58,80 @@ func GetGraphJson(dinfo []*DiagInfo) []byte { return b } + +type DotWriter struct { + W io.Writer + err error +} + +// Write writes a buffer to the internal writer. +// It handles errors as in: http://blog.golang.org/errors-are-values +func (w *DotWriter) Write(buf []byte) (n int, err error) { + if w.err == nil { + n, w.err = w.W.Write(buf) + } + return n, w.err +} + +// WriteS writes a string +func (w *DotWriter) WriteS(s string) (n int, err error) { + return w.Write([]byte(s)) +} + +func (w *DotWriter) WriteNetHeader(dinfo []*DiagInfo) error { + label := fmt.Sprintf("Nodes: %d\\l", len(dinfo)) + + w.WriteS("subgraph cluster_L { ") + w.WriteS("L [shape=box fontsize=32 label=\"" + label + "\"] ") + w.WriteS("}\n") + return w.err +} + +func (w *DotWriter) WriteNode(i int, di *DiagInfo) error { + box := "[label=\"%s\n%d conns\" fontsize=8 shape=box tooltip=\"%s (%d conns)\"]" + box = fmt.Sprintf(box, di.ID, len(di.Connections), di.ID, len(di.Connections)) + + w.WriteS(fmt.Sprintf("N%d %s\n", i, box)) + return w.err +} + +func (w *DotWriter) WriteEdge(i, j int, di *DiagInfo, conn connDiagInfo) error { + + n := fmt.Sprintf("%s ... %s (%d)", di.ID, conn.ID, conn.Latency) + s := "[label=\" %d\" weight=%d tooltip=\"%s\" labeltooltip=\"%s\" style=\"dotted\"]" + s = fmt.Sprintf(s, conn.Latency, conn.Count, n, n) + + w.WriteS(fmt.Sprintf("N%d -> N%d %s\n", i, j, s)) + return w.err +} + +func (w *DotWriter) WriteGraph(dinfo []*DiagInfo) error { + w.WriteS("digraph \"diag-net\" {\n") + w.WriteNetHeader(dinfo) + + idx := make(map[string]int) + for i, di := range dinfo { + if _, found := idx[di.ID]; found { + log.Debugf("DotWriter skipped duplicate %s", di.ID) + continue + } + + idx[di.ID] = i + w.WriteNode(i, di) + } + + for i, di := range dinfo { + for _, conn := range di.Connections { + j, found := idx[conn.ID] + if !found { // if we didnt get it earlier... + j = len(idx) + idx[conn.ID] = j + } + + w.WriteEdge(i, j, di, conn) + } + } + + w.WriteS("}") + return w.err +} From 2669479056feeab47d84ff5667f9afc7a86cf2d5 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Tue, 13 Jan 2015 23:50:23 -0800 Subject: [PATCH 2/5] diagnostics/d3 viewer.html -> force.html --- diagnostics/d3/{viewer.html => force.html} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename diagnostics/d3/{viewer.html => force.html} (100%) diff --git a/diagnostics/d3/viewer.html b/diagnostics/d3/force.html similarity index 100% rename from diagnostics/d3/viewer.html rename to diagnostics/d3/force.html From b9f828e1e3e04ca9f4bb8f38a6be1e66d40b43d9 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Wed, 14 Jan 2015 09:18:21 -0800 Subject: [PATCH 3/5] diagnostics: added chord viewer --- diagnostics/d3/chord.html | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 diagnostics/d3/chord.html diff --git a/diagnostics/d3/chord.html b/diagnostics/d3/chord.html new file mode 100644 index 000000000..77caaabce --- /dev/null +++ b/diagnostics/d3/chord.html @@ -0,0 +1,166 @@ + + + + + + From dd409fb151afdb96e07f0b2b8f4496666b53e752 Mon Sep 17 00:00:00 2001 From: Juan Batiz-Benet Date: Wed, 14 Jan 2015 09:21:37 -0800 Subject: [PATCH 4/5] diagnostics/d3/view: added chord viewer hash --- diagnostics/d3/chord.html | 2 +- diagnostics/d3/d3view | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/diagnostics/d3/chord.html b/diagnostics/d3/chord.html index 77caaabce..5cecb19a6 100644 --- a/diagnostics/d3/chord.html +++ b/diagnostics/d3/chord.html @@ -55,7 +55,7 @@ d3.json(hash, function(error, data) { .attr("dy", ".31em") // .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) // .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }) - .text(function(d) { return truncate(d.name, 16); }); + .text(function(d) { return d.name; }); var p = projection var link = svg.selectAll(".link") diff --git a/diagnostics/d3/d3view b/diagnostics/d3/d3view index a3f4932e1..2322049d7 100755 --- a/diagnostics/d3/d3view +++ b/diagnostics/d3/d3view @@ -8,7 +8,9 @@ cat >"$file" hash=$(ipfs add -q "$file" Date: Wed, 14 Jan 2015 10:29:55 -0800 Subject: [PATCH 5/5] diagnostics/d3: added # of conns --- diagnostics/d3/chord.html | 9 ++++++--- diagnostics/d3/d3view | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/diagnostics/d3/chord.html b/diagnostics/d3/chord.html index 5cecb19a6..2dade1aaa 100644 --- a/diagnostics/d3/chord.html +++ b/diagnostics/d3/chord.html @@ -47,15 +47,15 @@ d3.json(hash, function(error, data) { .attr("transform", function(d) { return "rotate(" + (d.x - 90 + rotate) + ")translate(" + d.y + ")"; }) node.append("svg:circle") - .attr("r", function(d) { return 4; }) - .style("fill", function(d, i) { return color(i % 3); }) + .attr("r", function(d) { return 6; }) + .style("fill", function(d, i) { return color(i % 20); }) node.append("text") .attr("dx", function(d) { return 8; }) .attr("dy", ".31em") // .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) // .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }) - .text(function(d) { return d.name; }); + .text(function(d) { return d.conns + " - " + d.name; }); var p = projection var link = svg.selectAll(".link") @@ -99,6 +99,7 @@ function parseGraph(graph2) { graph2.nodes.forEach(function(data, i) { data.y = innerRadius data.x = ((360 / graph2.nodes.length) * i) + data.conns = 0 graph.nodes.push(data) graph.byName[data.name] = data }) @@ -106,6 +107,8 @@ function parseGraph(graph2) { graph2.links.forEach(function(link) { var source = graph2.nodes[link.source] var target = graph2.nodes[link.target] + source.conns++ + target.conns++ var mid = curveNode(source, target) graph.mids.push(mid) diff --git a/diagnostics/d3/d3view b/diagnostics/d3/d3view index 2322049d7..66b0057a5 100755 --- a/diagnostics/d3/d3view +++ b/diagnostics/d3/d3view @@ -9,7 +9,7 @@ hash=$(ipfs add -q "$file"