diff --git a/app.js b/app.js index 8418a0f..42285fd 100644 --- a/app.js +++ b/app.js @@ -63,6 +63,9 @@ api.on('connection', function(spark) { spark.emit('ready'); client.write({action: 'add', data: info}); + + var blockPropagationChart = Nodes.blockPropagationChart(); + client.write({action: 'blockPropagationChart', data: blockPropagationChart}); } }); @@ -70,14 +73,16 @@ api.on('connection', function(spark) { { console.log('Latency: ', spark.latency); console.log('got update from ' + spark.id); - console.log(data); if(typeof data.id !== 'undefined' && typeof data.stats !== 'undefined') { data.stats.latency = spark.latency; - var stats = Nodes.update(data.id, data.stats); + var stats = Nodes.update(data.id, data.stats); client.write({action: 'update', data: stats}); + + var blockPropagationChart = Nodes.blockPropagationChart(); + client.write({action: 'blockPropagationChart', data: blockPropagationChart}); } }); @@ -116,6 +121,9 @@ client.on('connection', function(spark) { console.log(data); spark.emit('init', {nodes: Nodes.all()}); + + var blockPropagationChart = Nodes.blockPropagationChart(); + spark.write({action: 'blockPropagationChart', data: blockPropagationChart}); }); spark.on('client-pong', function(data) { diff --git a/models/collection.js b/models/collection.js index 686e1d6..55a1b3f 100644 --- a/models/collection.js +++ b/models/collection.js @@ -1,9 +1,11 @@ var _ = require('lodash'); +var Blockchain = require('./history'); var Node = require('./node'); var Collection = function Collection() { this._list = []; + this._history = new Blockchain(); this._bestBlock = null; return this; @@ -24,36 +26,14 @@ Collection.prototype.update = function(id, stats) if(!node) return false; - if(this._bestBlock === null) - { - stats.block.received = (new Date()).getTime(); - stats.block.propagation = 0; - this._bestBlock = stats.block; - } - else - { - var oldStats = node.getStats(); + var block = this._history.add(stats.block, id); + var propagationHistory = this._history.getNodePropagation(id); - if(stats.block.number !== oldStats.stats.block.number) - { - stats.block.received = (new Date()).getTime(); + stats.block.arrived = block.arrived; + stats.block.received = block.received; + stats.block.propagation = block.propagation; - if(this._bestBlock.number < stats.block.number) - { - stats.block.propagation = 0; - this._bestBlock = stats.block; - } - else - { - stats.block.propagation = stats.block.received - this._bestBlock.received; - } - } else { - stats.block.received = oldStats.stats.block.received; - stats.block.propagation = oldStats.stats.block.propagation; - } - } - - return node.setStats(stats); + return node.setStats(stats, propagationHistory); } Collection.prototype.updateLatency = function(id, latency) @@ -118,4 +98,9 @@ Collection.prototype.all = function() return this._list; } +Collection.prototype.blockPropagationChart = function() +{ + return this._history.getBlockPropagation(); +} + module.exports = Collection; \ No newline at end of file diff --git a/models/history.js b/models/history.js new file mode 100644 index 0000000..1a20517 --- /dev/null +++ b/models/history.js @@ -0,0 +1,159 @@ +var _ = require('lodash'); + +var MAX_HISTORY = 1008; +var MAX_PROPAGATION = 36; +var MAX_BLOCK_PROPAGATION = 96; + +var History = function History(data) +{ + // this._items = new Array(MAX_HISTORY); + this._items = []; + + var item = { + height: 0, + block: { + number: 0, + hash: '0x?', + arrived: 0, + received: 0, + propagation: 0, + difficulty: 0, + gasUsed: 0, + transactions: [], + uncles: [] + }, + propagTimes: [] + }; + + // _.fill(this._items, item); +} + +History.prototype.add = function(block, id) +{ + var historyBlock = this.search(block.number); + + var now = (new Date()).getTime(); + block.arrived = now; + block.received = now; + block.propagation = 0; + + if(historyBlock) + { + var propIndex = _.findIndex(historyBlock.propagTimes, {node: id}); + + if(propIndex === -1) + { + block.arrived = historyBlock.block.arrived; + block.received = now; + block.propagation = now - historyBlock.block.received; + + historyBlock.propagTimes.push({node: id, received: now, propagation: block.propagation}); + } + else + { + block.arrived = historyBlock.block.arrived; + block.received = historyBlock.propagTimes[propIndex].received; + block.propagation = historyBlock.propagTimes[propIndex].propagation; + } + } + else + { + var item = { + height: block.number, + block: block, + propagTimes: [] + } + + item.propagTimes.push({node: id, received: now, propagation: block.propagation}); + console.log('item: ', item); + this._save(item); + } + this.getNodePropagation(id); + + return block; +} + +History.prototype._save = function(block) +{ + this._items.push(block); + + if(this._items.length > MAX_HISTORY){ + this._items.shift(); + } +} + +History.prototype.search = function(number) +{ + var index = _.findIndex(this._items, {height: number}); + + if(index < 0) + return false; + + return this._items[index]; +} + +History.prototype.bestBlock = function(obj) +{ + return _.max(this._items, 'height'); +} + +History.prototype.getNodePropagation = function(id) +{ + var propagation = new Array(MAX_PROPAGATION); + var bestBlock = this.bestBlock().height; + + _.fill(propagation, -1); + + var sorted = _(this._items) + .sortByOrder('height', false) + .slice(0, MAX_PROPAGATION) + .reverse() + .forEach(function(n, key) + { + var index = MAX_PROPAGATION - 1 - bestBlock + n.height; + + if(index > 0) + { + propagation[index] = _.result(_.find(n.propagTimes, 'node', id), 'propagation', -1); + } + }) + .value(); + + return propagation; +} + +History.prototype.getBlockPropagation = function() +{ + var propagation = new Array(MAX_BLOCK_PROPAGATION); + var bestBlock = this.bestBlock().height; + var i = 0; + + _.fill(propagation, -1); + + var sorted = _(this._items) + .sortByOrder('height', false) + .slice(0, MAX_PROPAGATION) + .reverse() + .forEach(function(n, key) + { + if(i < MAX_BLOCK_PROPAGATION) + { + _.forEach(n.propagTimes, function(p, i) + { + propagation.push({block: n.height, propagation: _.result(p, 'propagation', -1)}); + propagation.shift(); + i++; + }); + } + }) + .value(); + + return propagation; +} + +History.prototype.history = function() +{ + return _.chain(this._items).sortBy('number').reverse().value(); +} + +module.exports = History; diff --git a/models/node.js b/models/node.js index 146d1ce..e972a69 100644 --- a/models/node.js +++ b/models/node.js @@ -1,4 +1,5 @@ var geoip = require('geoip-lite'); +var _ = require('lodash'); var MAX_HISTORY = 36; @@ -31,13 +32,13 @@ var Node = function Node(data) uptime: 0, lastUpdate: 0 }; - this.blockHistory = []; + this.history = new Array(MAX_HISTORY); this.uptime = { started: null, history: [] }; - this.initBlockHistory(); + _.fill(this.history, -1); if(this.id === null) { this.uptime.started = (new Date()).getTime(); @@ -90,41 +91,14 @@ Node.prototype.setInfo = function(data) Node.prototype.getInfo = function() { - return {id: this.id, info: this.info, geo: this.geo, stats: this.stats, history: this.blockHistory}; + return {id: this.id, info: this.info, geo: this.geo, stats: this.stats, history: this.history}; } -Node.prototype.initBlockHistory = function() -{ - for(var i=0; i < MAX_HISTORY; i++) - { - this.blockHistory.push({ - number: 0, - received: 0, - propagation: 0 - }); - } -} - -Node.prototype.setStats = function(stats) +Node.prototype.setStats = function(stats, history) { if(typeof stats !== 'undefined' && typeof stats.block !== 'undefined' && typeof stats.block.number !== 'undefined') { - stats.block.hash = stats.block.hash.replace('0x', ''); - - if(stats.block.number > this.stats.block.number) - { - if(this.blockHistory.length === MAX_HISTORY ) - this.blockHistory.shift(); - - var history = { - number: stats.block.number, - received: stats.block.received, - propagation: stats.block.propagation - }; - - this.blockHistory.push(history); - } - + this.history = history; this.stats = stats; return this.getStats(); @@ -147,7 +121,7 @@ Node.prototype.setLatency = function(latency) Node.prototype.getStats = function() { - return {id: this.id, stats: this.stats, history: this.blockHistory}; + return {id: this.id, stats: this.stats, history: this.history}; } Node.prototype.setState = function(active) diff --git a/public/css/style.css b/public/css/style.css index ce033a5..3cbe054 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -193,25 +193,88 @@ div.small-title-miner { opacity: .8; } -.hoverinfo { +table i { + -webkit-font-smoothing: subpixel-antialiased; + -moz-font-smoothing: subpixel-antialiased; +} + +table th, +table td { + border-color: #222 !important; +} + +table td { + line-height: 18px; +} + +table th { + color: #888; +} + +table th i { + font-size: 20px; +} +table td i { position: relative; - width: auto; - left: -50%; - text-align: center; - color: #333; - border: none !important; - box-shadow: none !important; - border-radius: 3px !important; - padding: 5px !important; - line-height: 14px !important; + line-height: 16px; +} +table td i:before { + position: absolute; + top: 10px; + left: 5px; } -.hoverinfo .propagationBox { - top: 3px; +.table>tbody>tr>td, +.table>thead>tr>th { + padding: 5px; } -.jqstooltip { +.th-nodename { + width: 400px; +} +.th-latency { + width: 100px; +} + +.th-blockhash { + width: 150px; +} + +.th-blocktime { + width: 110px; +} + +.th-peerPropagationChart { + width: 140px; +} + +.nodeInfo .tooltip .tooltip-inner { + max-width: 400px; + text-align: left; + font-size: 12px; +} + +#mapHolder { + display: block; + position: relative; + padding-bottom: 56.25%; + height: 0; + overflow: hidden; + max-width: 100%; + height: auto; + margin: 10px auto; +} + +#mapHolder > svg { + right: 0; + bottom: 0; + width: 100%; + height: 100%; + display: inline-block; + position: absolute; + top: 0; + left: 0; } .jqsfield { @@ -241,127 +304,19 @@ div.small-title-miner { border-bottom-color: #fff; } -table i { - -webkit-font-smoothing: subpixel-antialiased; - -moz-font-smoothing: subpixel-antialiased; -} - -table th, -table td { - border-color: #222 !important; -} - -table th { - color: #888; -} - -table th i { - font-size: 20px; -} -table td i { +.hoverinfo { position: relative; - line-height: 16px; -} -table td i:before { - position: absolute; - top: 10px; - left: 5px; + width: auto; + left: -50%; + text-align: center; + color: #333; + border: none !important; + box-shadow: none !important; + border-radius: 3px !important; + padding: 5px !important; + line-height: 14px !important; } -#mapHolder { - display: block; - position: relative; - padding-bottom: 56.25%; - height: 0; - overflow: hidden; - max-width: 100%; - height: auto; - margin: 10px auto; -} - -#mapHolder > svg { - right: 0; - bottom: 0; - width: 100%; - height: 100%; - display: inline-block; - position: absolute; - top: 0; - left: 0; -} - -.nodeInfo .tooltip .tooltip-inner { - max-width: 400px; - text-align: left; - font-size: 12px; -} - -.table>tbody>tr>td, -.table>thead>tr>th { - padding: 5px; -} - -.th-nodename { - width: 400px; -} - -.th-latency { - width: 100px; -} - -.th-blockhash { - width: 150px; -} - -.th-blocktime { - width: 110px; -} - -.th-peerPropagationChart { - width: 140px; -} - -@media only screen and (max-width: 639px) { - /*.big-info { - padding-bottom: 15px; - padding-top: 15px; - border: 1px solid rgba(255,255,255,0.05); - } - - .big-info .icon-full-width i { - width: 70px; - height: 60px; - font-size: 60px; - line-height: 60px; - margin-right: 20px; - } - - .big-info span { - opacity: 0.7; - } - - .big-info span.small-title { - font-size: 13px; - line-height: 14px; - letter-spacing: 1px; - padding-top: 0px; - } - - .big-info span.big-details { - display: block; - font-weight: 200; - font-size: 46px; - line-height: 50px; - letter-spacing: -4px; - }*/ -} - -@media only screen and (max-width: 479px) { - /*.stat-holder { - width: 100%; - }*/ -} - -[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - display: none !important; -} +.hoverinfo .propagationBox { + top: 3px; +} \ No newline at end of file diff --git a/public/js/controllers.js b/public/js/controllers.js index 6139267..00bc09b 100644 --- a/public/js/controllers.js +++ b/public/js/controllers.js @@ -91,8 +91,15 @@ function StatsCtrl($scope, $filter, socket, _, toastr) { switch(action) { case "init": $scope.nodes = data; + $scope.$apply(); + + _.forEach($scope.nodes, function(node, index) { + makePeerPropagationChart($scope.nodes[index]); + }); + + if($scope.nodes.length > 0) + toastr['success']("Got nodes list", "Got nodes!"); - if($scope.nodes.length > 0) toastr['success']("Got nodes list", "Got nodes!"); break; case "add": @@ -100,30 +107,42 @@ function StatsCtrl($scope, $filter, socket, _, toastr) { toastr['success']("New node "+ $scope.nodes[findIndex({id: data.id})].info.name +" connected!", "New node!"); else toastr['info']("Node "+ $scope.nodes[findIndex({id: data.id})].info.name +" reconnected!", "Node is back!"); + break; case "update": var index = findIndex({id: data.id}); $scope.nodes[index].stats = data.stats; $scope.nodes[index].history = data.history; - makePeerPropagationChart(index); + makePeerPropagationChart($scope.nodes[index]); + break; case "info": $scope.nodes[findIndex({id: data.id})].info = data.info; + + break; + + case "blockPropagationChart": + $scope.blockPropagationChart = data; + makeBlockPropagationChart(); + break; case "inactive": $scope.nodes[findIndex({id: data.id})].stats = data.stats; toastr['error']("Node "+ $scope.nodes[findIndex({id: data.id})].info.name +" went away!", "Node connection was lost!"); + break; case "latency": $scope.nodes[findIndex({id: data.id})].stats.latency = data.latency; + break; case "client-ping": socket.emit('client-pong'); + break; } @@ -135,14 +154,12 @@ function StatsCtrl($scope, $filter, socket, _, toastr) { return _.findIndex($scope.nodes, search); } - function makePeerPropagationChart(index) + function makePeerPropagationChart(node) { - $scope.nodes[index].propagation = _.map($scope.nodes[index].history, function(block) { - return block.propagation; - }); - - jQuery('.' + $scope.nodes[index].id).sparkline($scope.nodes[index].propagation, { + jQuery('.' + node.id).sparkline(node.history, { type: 'bar', + negBarColor: '#7f7f7f', + zeroAxis: false, height: 18, barWidth : 2, barSpacing : 1, @@ -157,6 +174,30 @@ function StatsCtrl($scope, $filter, socket, _, toastr) { }); } + function makeBlockPropagationChart() + { + jQuery('.spark-blockpropagation').sparkline(_.map($scope.blockPropagationChart, function(history) { + if(typeof history.propagation === 'undefined') + return -1; + + return history.propagation; + }), { + type: 'bar', + negBarColor: '#7f7f7f', + zeroAxis: false, + barWidth : 2, + barSpacing : 1, + tooltipSuffix: ' ms', + colorMap: jQuery.range_map({ + '0:1': '#10a0de', + '1:1000': '#7bcc3a', + '1001:3000': '#FFD162', + '3001:7000': '#ff8a00', + '7001:': '#F74B4B' + }) + }); + } + function addNewNode(data) { var index = findIndex({id: data.id}); @@ -168,8 +209,7 @@ function StatsCtrl($scope, $filter, socket, _, toastr) { } $scope.nodes[index] = data; - $scope.nodes[index].history = data.history; - makePeerPropagationChart(index); + makePeerPropagationChart($scope.nodes[index]); return false; } diff --git a/public/js/filters.js b/public/js/filters.js index 53a695d..a4fc293 100644 --- a/public/js/filters.js +++ b/public/js/filters.js @@ -86,6 +86,9 @@ angular.module('netStatsApp.filters', []) }) .filter('hashFilter', function() { return function(hash) { + if(hash.substr(0,2) === '0x') + hash = hash.substr(2,64); + return hash.substr(0, 8) + '...' + hash.substr(56, 8); } }) diff --git a/views/index.jade b/views/index.jade index ab79b67..0eacaad 100644 --- a/views/index.jade +++ b/views/index.jade @@ -58,24 +58,29 @@ block content div.clearfix div.row - div.col-xs-4.stats-boxes(style="padding-top: 30px;") + div.col-xs-6.stats-boxes(style="padding-top: 30px;") div.row - div.col-xs-6.stat-holder + div.col-xs-4.stat-holder div.big-info.chart span.small-title block time span.big-details.spark-blocktimes - div.col-xs-6.stat-holder + div.col-xs-4.stat-holder + div.big-info.chart + span.small-title block propagation + span.big-details.spark-blockpropagation + + div.col-xs-4.stat-holder div.big-info.chart span.small-title difficulty span.big-details.spark-difficulty - div.col-xs-6.stat-holder + div.col-xs-4.stat-holder div.big-info.chart span.small-title transactions span.big-details.spark-transactions - div.col-xs-6.stat-holder + div.col-xs-4.stat-holder div.big-info.chart span.small-title gas spending span.big-details.spark-gasspending @@ -91,13 +96,6 @@ block content div.block(ng-repeat="i in getNumber(miner.blocks) track by $index", class="{{miner.blocks | minerBlocksClass}}") div.clearfix - div.col-xs-2.stats-boxes(style="padding-top: 30px;") - div.row - //- div.col-xs-12.stat-holder - //- div.big-info.chart - //- span.small-title miners - //- span.big-details test - div.col-xs-4 div.col-xs-12 nodemap#mapHolder(data="map") @@ -149,7 +147,7 @@ block content td(class="{{ node.stats | blockClass : bestBlock }}") span.small {{node.stats.block.hash | hashFilter}} td(style="padding-left: 14px;") {{node.stats.block.transactions.length || 0}} - td(class="{{ node.stats.block.received | timeClass : node.stats.active }}") {{node.stats.block.received | blockTimeFilter }} + td(class="{{ node.stats.block.arrived | timeClass : node.stats.active }}") {{node.stats.block.arrived | blockTimeFilter }} td(class="{{ node.stats | propagationTimeClass : bestBlock }}") {{node.stats.block.propagation | blockPropagationFilter}} div.propagationBox td.peerPropagationChart(class="{{node.id}}") diff --git a/views/layout.jade b/views/layout.jade index fe441cb..43d9574 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -3,6 +3,7 @@ html(ng-app="netStatsApp") head meta(name="viewport", content="width=device-width, initial-scale=1.0, maximum-scale=1.0") title= title + style(type="text/css") [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; } link(rel='stylesheet', href='//fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700') link(rel='stylesheet', href='/css/bootstrap.min.css') link(rel='stylesheet', href='/css/toastr.min.css')