github.com/uber/kraken@v0.1.4/tools/bin/visualization/static/js/app.js (about)

     1  function dragstarted(d) {
     2    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
     3    d.fx = d.x;
     4    d.fy = d.y;
     5  }
     6  
     7  function dragged(d) {
     8    d.fx = d3.event.x;
     9    d.fy = d3.event.y;
    10  }
    11  
    12  function dragended(d) {
    13    if (!d3.event.active) simulation.alphaTarget(0);
    14    d.fx = null;
    15    d.fy = null;
    16  }
    17  
    18  var origins = new Set([
    19    'ffed335bc92f6f27d33a8b5a12866328b1e70117',
    20    '598bd849259c97118c11759b9895fda171610a51',
    21    'cd178baaa144a3c86c7ee9080936c73da8b4b597',
    22  ])
    23  
    24  const bitfieldWidth = 200;
    25  
    26  const radius = 8;
    27  
    28  function bitfieldString(bitfield) {
    29    var rows = [];
    30    var cursor = 0;
    31    for (var i = 0; i < bitfield.length; i++) {
    32      if (i > 0 && i % bitfieldWidth == 0) {
    33        cursor++;
    34      }
    35      if (rows.length == cursor) {
    36        rows.push([]);
    37      }
    38      rows[cursor].push(bitfield[i]);
    39    }
    40    return 'Pieces: ' + rows.map(row => row.map(b => b ? '1' : '0').join('')).join('\n');
    41  }
    42  
    43  function connKey(sourceID, targetID) {
    44    if (sourceID < targetID) {
    45      return sourceID + ':' + targetID;
    46    }
    47    return targetID + ':' + sourceID;
    48  }
    49  
    50  class Graph {
    51    constructor(torrent, startTime) {
    52      this.torrent = torrent;
    53      this.startTime = startTime;
    54      this.curTime = startTime;
    55  
    56      this.w = window.innerWidth;
    57      this.h = window.innerHeight - 120;
    58  
    59      this.header = d3.select('#graph').append('div').attr('class', 'header');
    60      this.headerTime = this.header.append('p').append('pre');
    61      this.headerElapsed = this.header.append('p').append('pre').text('Elapsed: 0s');
    62      this.headerNumPeers = this.header.append('p').append('pre');
    63      this.headerTorrent = this.header.append('p').append('pre').text('Torrent: ' + torrent);
    64      this.headerPeerID = this.header.append('p').append('pre');
    65      this.headerBitfield = this.header.append('p').append('pre');
    66  
    67      this.svg = d3.select('#graph').append('svg')
    68        .attr('width', this.w)
    69        .attr('height', this.h);
    70  
    71      this.svg.append('g').attr('class', 'links');
    72  
    73      this.svg.append('g').attr('class', 'blinks');
    74  
    75      this.svg.append('g').attr('class', 'nodes');
    76  
    77      this.peers = [];
    78      this.conns = [];
    79      this.blacklist = [];
    80  
    81      this.peerIndexByID = {};
    82  
    83      this.connKeys = new Set();
    84  
    85      this.simulation = d3.forceSimulation(this.peers)
    86        .force('charge', d3.forceManyBody().strength(-70).distanceMax(400))
    87        .force('center', d3.forceCenter(this.w/2, this.h/2))
    88        .force('link', d3.forceLink(this.conns).id(d => d.id));
    89  
    90      this.update();
    91  
    92      this.currHighlightedPeer = null;
    93  
    94      this.simulation.on('tick', this.tick.bind(this));
    95    }
    96  
    97    checkPeer(id) {
    98      if (!(id in this.peerIndexByID)) {
    99        throw {
   100          message: 'not found',
   101          peer: id,
   102        }
   103      }
   104    }
   105  
   106    getPeer(id) {
   107      this.checkPeer(id);
   108      return this.peers[this.peerIndexByID[id]];
   109    }
   110  
   111    update() {
   112      this.simulation.nodes(this.peers);
   113      this.simulation.force('link').links(this.conns);
   114  
   115      // Draw blacklisted connection links.
   116  
   117      // Remove expired blacklist items. Loop backwards so splice still works.
   118      for (var i = this.blacklist.length - 1; i >= 0; i--) {
   119        if (this.blacklist[i].expiresAt < this.curTime) {
   120          this.blacklist.splice(i, 1);
   121        }
   122      }
   123  
   124      this.blink = this.svg.select('.blinks').selectAll('.blink').data(this.blacklist);
   125  
   126      this.blink
   127        .enter()
   128        .append('line')
   129        .attr('class', 'blink')
   130        .attr('stroke-width', 1)
   131        .attr('stroke', '#ef9d88');
   132  
   133      this.blink.exit().remove();
   134  
   135      // Draw connection links.
   136  
   137      this.link = this.svg.select('.links').selectAll('.link').data(this.conns);
   138  
   139      this.link
   140        .enter()
   141        .append('line')
   142        .attr('class', 'link')
   143        .attr('stroke-width', 1)
   144        .attr('stroke', '#999999');
   145  
   146      this.link.exit().remove();
   147  
   148      // Draw peer nodes.
   149  
   150      this.node = this.svg.select('.nodes').selectAll('.node').data(this.peers);
   151  
   152      var drag = d3.drag()
   153        .on('start', d => {
   154          if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
   155          d.fx = d.x;
   156          d.fy = d.y;
   157        })
   158        .on('drag', d => {
   159          d.fx = d3.event.x;
   160          d.fy = d3.event.y;
   161        })
   162        .on('end', d => {
   163          if (!d3.event.active) this.simulation.alphaTarget(0);
   164          d.fx = null;
   165          d.fy = null;
   166        })
   167  
   168      this.node
   169        .enter()
   170        .append('circle')
   171        .attr('class', 'node')
   172        .attr('r', radius)
   173        .attr('stroke-width', 1.5)
   174        .call(drag)
   175        .on('click', d => {
   176          this.headerPeerID.text('PeerID: ' + d.id);
   177          this.headerBitfield.text(bitfieldString(d.bitfield));
   178          if (this.currHighlightedPeer) {
   179            this.currHighlightedPeer.highlight = false;
   180          }
   181          this.currHighlightedPeer = d;
   182          d.highlight = true;
   183          this.node.attr('id', d => d.highlight ? 'highlight' : null);
   184          this.blacklist = d.blacklist;
   185          this.update();
   186        });
   187  
   188      this.node
   189        .attr('fill', d => {
   190          if (origins.has(d.id)) {
   191            return 'hsl(230, 100%, 50%)';
   192          }
   193          if (d.complete) {
   194            return 'hsl(120, 100%, 50%)';
   195          }
   196          var completed = 0;
   197          d.bitfield.forEach(b => completed += b ? 1 : 0);
   198          var percentComplete = 100.0 * completed / d.bitfield.length;
   199          return 'hsl(55, ' + Math.ceil(percentComplete) + '%, 50%)';
   200        })
   201        .each(d => {
   202          if (d.highlight) {
   203            this.headerBitfield.text(bitfieldString(d.bitfield));
   204          }
   205        });
   206  
   207      this.node.exit().remove();
   208  
   209      this.simulation.alphaTarget(0.05).restart();
   210    }
   211  
   212    addPeer(id, bitfield) {
   213      if (id in this.peerIndexByID) {
   214        throw {
   215          message: 'duplicate peer',
   216          peer: id,
   217        }
   218      }
   219      this.peerIndexByID[id] = this.peers.length;
   220      this.peers.push({
   221        type: 'peer',
   222        id: id,
   223        x: this.w / 2,
   224        y: this.h / 2,
   225        complete: false,
   226        bitfield: bitfield,
   227        highlight: false,
   228        blacklist: [],
   229      });
   230      this.headerNumPeers.text('Num peers: ' + this.peers.length);
   231    }
   232  
   233    addActiveConn(sourceID, targetID) {
   234      this.checkPeer(sourceID);
   235      this.checkPeer(targetID);
   236      var k = connKey(sourceID, targetID);
   237      if (this.connKeys.has(k)) {
   238        return;
   239      }
   240      this.conns.push({
   241        type: 'conn',
   242        source: sourceID,
   243        target: targetID,
   244      });
   245      this.connKeys.add(k)
   246    }
   247  
   248    removeActiveConn(sourceID, targetID) {
   249      var k = connKey(sourceID, targetID);
   250      if (!this.connKeys.has(k)) {
   251        return;
   252      }
   253      var removed = false;
   254      for (var i = 0; i < this.conns.length; i++) {
   255        var curK = connKey(this.conns[i].source.id, this.conns[i].target.id);
   256        if (curK == k) {
   257          this.conns.splice(i, 1);
   258          removed = true;
   259        }
   260      }
   261    }
   262  
   263    blacklistConn(sourceID, targetID, duration) {
   264      var source = this.getPeer(sourceID);
   265      var target = this.getPeer(targetID);
   266      source.blacklist.push({
   267        source: source,
   268        target: target,
   269        expiresAt: this.curTime + duration,
   270      })
   271    }
   272  
   273    receivePiece(id, piece) {
   274      this.getPeer(id).bitfield[piece] = true;
   275    }
   276  
   277    completePeer(id) {
   278      var p = this.getPeer(id);
   279      p.complete = true;
   280      for (var i = 0; i < p.bitfield.length; i++) {
   281        p.bitfield[i] = true;
   282      }
   283    }
   284  
   285    tick() {
   286      this.node
   287        .attr('cx', d => {
   288          d.x = Math.max(radius, Math.min(this.w - radius, d.x));
   289          return d.x;
   290        })
   291        .attr('cy', d => {
   292          d.y = Math.max(radius, Math.min(this.h - radius, d.y));
   293          return d.y;
   294        });
   295  
   296      this.link
   297        .attr('x1', d => d.source.x)
   298        .attr('y1', d => d.source.y)
   299        .attr('x2', d => d.target.x)
   300        .attr('y2', d => d.target.y);
   301  
   302      this.blink
   303        .attr('x1', d => d.source.x)
   304        .attr('y1', d => d.source.y)
   305        .attr('x2', d => d.target.x)
   306        .attr('y2', d => d.target.y);
   307    }
   308  
   309    setTime(t) {
   310      var d = new Date(t);
   311      this.headerTime.text(d.toString());
   312      var elapsed = (t - this.startTime) / 1000;
   313      this.headerElapsed.text('Elapsed: ' + elapsed + 's');
   314      this.curTime = t;
   315    }
   316  }
   317  
   318  d3.request('http://' + location.host + '/events').get(req => {
   319    var events = JSON.parse(req.response);
   320    var graph = new Graph(events[0].torrent, Date.parse(events[0].ts));
   321  
   322    // Maps peer id to list of events which occurred before the peer was added
   323    // to the graph. Early events are possible in cases where a connection is
   324    // added before the torrent is opened, which is valid.
   325    var earlyEvents = {};
   326  
   327    function applyEvent(event) {
   328      try {
   329        switch (event.event) {
   330          case 'add_torrent':
   331            graph.addPeer(event.self, event.bitfield);
   332            if (event.self in earlyEvents) {
   333              earlyEvents[event.self].forEach(e => applyEvent(e))
   334            }
   335            break;
   336          case 'add_active_conn':
   337            graph.addActiveConn(event.self, event.peer);
   338            break;
   339          case 'drop_active_conn':
   340            graph.removeActiveConn(event.self, event.peer);
   341            break;
   342          case 'receive_piece':
   343            graph.receivePiece(event.self, event.piece);
   344            break;
   345          case 'torrent_complete':
   346            graph.completePeer(event.self);
   347            break;
   348          case 'blacklist_conn':
   349            graph.blacklistConn(event.self, event.peer, parseInt(event.duration_ms));
   350            break;
   351        }
   352      } catch (err) {
   353        if (err.message == 'not found') {
   354          if (!(err.peer in earlyEvents)) {
   355            earlyEvents[err.peer] = [];
   356          }
   357          earlyEvents[err.peer].push(event);
   358        } else {
   359          console.log('unhandled error: ' + err);
   360        }
   361      }
   362    }
   363  
   364    // Every interval seconds, we read all events that occur within that interval
   365    // and apply them to the graph. This gives the illusion of events occuring in
   366    // real-time.
   367    const interval = 100;
   368  
   369    function readEvents(i, until) {
   370      if (i >= events.length) {
   371        return;
   372      }
   373      graph.setTime(until);
   374      while (i < events.length && Date.parse(events[i].ts) < until) {
   375        applyEvent(events[i]);
   376        i++;
   377      }
   378      graph.update();
   379      setTimeout(() => readEvents(i, until + interval), interval);
   380    }
   381  
   382    readEvents(0, Date.parse(events[0].ts) + interval);
   383  });