mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-28 13:57:52 +08:00
Merge pull request #556 from jbenet/net-diag-viewer
diag/net: visualizing in d3 and dot
This commit is contained in:
commit
01283b92f7
@ -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 {
|
||||
|
||||
169
diagnostics/d3/chord.html
Normal file
169
diagnostics/d3/chord.html
Normal file
@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
|
||||
.node {
|
||||
font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.link {
|
||||
stroke: steelblue;
|
||||
stroke-opacity: .4;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
<script src="http://d3js.org/d3.v3.min.js"></script>
|
||||
<script>
|
||||
var hash = window.location.hash.substring(1)
|
||||
|
||||
var diameter = 1400,
|
||||
radius = diameter / 2,
|
||||
innerRadius = radius - 200
|
||||
rotate = 145;
|
||||
|
||||
var color = d3.scale.category10()
|
||||
|
||||
var diagonal = d3.svg.diagonal.radial()
|
||||
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
|
||||
|
||||
var svg = d3.select("body").append("svg")
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", "0 0 " + diameter + " " + diameter )
|
||||
.attr("preserveAspectRatio", "xMidYMid meet")
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + radius + "," + radius + ")");
|
||||
|
||||
d3.json(hash, function(error, data) {
|
||||
|
||||
graph = parseGraph(data)
|
||||
|
||||
var node = svg.selectAll(".node")
|
||||
.data(graph.nodes)
|
||||
.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function(d) { return "rotate(" + (d.x - 90 + rotate) + ")translate(" + d.y + ")"; })
|
||||
|
||||
node.append("svg:circle")
|
||||
.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.conns + " - " + d.name; });
|
||||
|
||||
var p = projection
|
||||
var link = svg.selectAll(".link")
|
||||
.data(graph.paths)
|
||||
.enter().append("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", function(d) {
|
||||
return "M" + p(d[0])[0] + "," + p(d[0])[1]
|
||||
+ "S" + p(d[1])[0] + "," + p(d[1])[1]
|
||||
+ " " + p(d[2])[0] + "," + p(d[2])[1];
|
||||
})
|
||||
|
||||
// var mid = svg.selectAll(".node-mid")
|
||||
// .data(graph.mids)
|
||||
// .enter().append("g")
|
||||
// .attr("class", "node-mid")
|
||||
// .attr("transform", function(d) { return "rotate(" + (d.x + rotate) + ")translate(" + d.y + ")"; })
|
||||
|
||||
// mid.append("svg:circle")
|
||||
// .attr("r", function(d) { return 4; })
|
||||
// .style("fill", function(d, i) { return color(i % 3); })
|
||||
|
||||
console.log(graph.paths)
|
||||
});
|
||||
|
||||
|
||||
function parseGraph(graph2) {
|
||||
graph = {}
|
||||
graph.nodes = []
|
||||
graph.links = []
|
||||
graph.paths = []
|
||||
graph.byName = {}
|
||||
graph.mids = []
|
||||
|
||||
graph2.nodes.sort(function(a, b) {
|
||||
if (a.name > b.name) return 1;
|
||||
if (a.name < b.name) return -1;
|
||||
return 0;
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
var link1 = {source: source, target: mid, value: link.value || 3}
|
||||
var link2 = {source: mid, target: target, value: link.value || 3}
|
||||
graph.links.push(link1)
|
||||
graph.links.push(link2)
|
||||
|
||||
var path = [source, mid, target]
|
||||
graph.paths.push(path)
|
||||
})
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
function curveNode(source, target) {
|
||||
var d = circleDistance(source.x, target.x)
|
||||
var h = ((1-(d/180)) * innerRadius) * 0.7
|
||||
var x = circleMidpoint(source.x, target.x)
|
||||
return {x: x, y: h}
|
||||
}
|
||||
|
||||
function circleMidpoint(x, y) {
|
||||
var x2 = x > y ? x : y
|
||||
var y2 = x > y ? y : x
|
||||
var a = (x2-y2)
|
||||
if (a > 180) {
|
||||
a = 360 - a
|
||||
return (x2 + a/2) % 360
|
||||
} else {
|
||||
return (y2 + a/2) % 360
|
||||
}
|
||||
}
|
||||
|
||||
function circleDistance(x, y) {
|
||||
var a = abs(x-y)
|
||||
return (a > 180) ? 360 - a : a
|
||||
}
|
||||
|
||||
function abs(x) {
|
||||
return x < 0 ? -x : x
|
||||
}
|
||||
|
||||
function projection(d) {
|
||||
var r = d.y, a = (d.x - 90 + rotate) / 180 * Math.PI;
|
||||
return [r * Math.cos(a), r * Math.sin(a)];
|
||||
}
|
||||
|
||||
function truncate(name, limit) {
|
||||
return name.substring(0, limit)
|
||||
}
|
||||
|
||||
|
||||
d3.select(self.frameElement).style("height", "100%");
|
||||
d3.select(self.frameElement).style("width", "100%");
|
||||
|
||||
</script>
|
||||
24
diagnostics/d3/d3view
Executable file
24
diagnostics/d3/d3view
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# put stdin in temp file
|
||||
file=`mktemp -t d3view`
|
||||
cat >"$file"
|
||||
|
||||
# add file to ipfs
|
||||
hash=$(ipfs add -q "$file" </dev/null | tail -n1)
|
||||
|
||||
# this viewer is the hash of go-ipfs/diagnostics/d3/viewer.html
|
||||
force="QmaY6Lq9MEhDfWUc1VfHcu9aLWSyvi4VDLvWQXLoVZ4Mau"
|
||||
chord="QmYWjPa736Bk7FhNEEtWLjaEdioSxXkYMhRT9tLi45ccm7"
|
||||
viewer="$chord"
|
||||
|
||||
# the ipfs gateway to use
|
||||
gatewayHTTP="http://ipfs.benet.ai:8080"
|
||||
gatewayIPFS="/ip4/104.236.32.22/tcp/4001/Qme7peMbkRH8qzb9TMXSoRwVmVDZz3Z4dseRXAyBwBmxA7"
|
||||
|
||||
# make sure you're reachable (no NAT yet)
|
||||
ipfs swarm connect "$gatewayIPFS" </dev/null >/dev/null
|
||||
|
||||
# output the url at the gateway
|
||||
url="$gatewayHTTP/ipfs/$viewer#$hash"
|
||||
echo "$url"
|
||||
59
diagnostics/d3/force.html
Normal file
59
diagnostics/d3/force.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
.node {
|
||||
stroke: #fff;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
.link {
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Ipfs Visualization</h1>
|
||||
<script src="http://d3js.org/d3.v3.min.js"></script>
|
||||
<script>
|
||||
var hash = window.location.hash.substring(1)
|
||||
|
||||
var width = 960,
|
||||
height = 800;
|
||||
var color = d3.scale.category20();
|
||||
var force = d3.layout.force()
|
||||
.charge(-50)
|
||||
.linkDistance(90)
|
||||
.gravity(0.01)
|
||||
.size([width, height]);
|
||||
var svg = d3.select("body").append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
d3.json(hash, function(error, graph) {
|
||||
force
|
||||
.nodes(graph.nodes)
|
||||
.links(graph.links)
|
||||
.start();
|
||||
var link = svg.selectAll(".link")
|
||||
.data(graph.links)
|
||||
.enter().append("line")
|
||||
.attr("class", "link")
|
||||
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
|
||||
var node = svg.selectAll(".node")
|
||||
.data(graph.nodes)
|
||||
.enter().append("circle")
|
||||
.attr("class", "node")
|
||||
.attr("r", function(d) {return 1.0 + Math.log(d.value)})
|
||||
.style("fill", function(d) { return color(d.group); })
|
||||
.call(force.drag);
|
||||
node.append("title")
|
||||
.text(function(d) { return d.name; });
|
||||
force.on("tick", function() {
|
||||
link.attr("x1", function(d) { return d.source.x; })
|
||||
.attr("y1", function(d) { return d.source.y; })
|
||||
.attr("x2", function(d) { return d.target.x; })
|
||||
.attr("y2", function(d) { return d.target.y; });
|
||||
node.attr("cx", function(d) { return d.x; })
|
||||
.attr("cy", function(d) { return d.y; });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user