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 });