Merge pull request #80 from cubedro/develop

Added block propagation history
This commit is contained in:
Marian OANCΞA 2015-04-17 12:10:56 +03:00
commit 07b2466d67
9 changed files with 343 additions and 220 deletions

12
app.js
View File

@ -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) {

View File

@ -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;

159
models/history.js Normal file
View File

@ -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;

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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}}")

View File

@ -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')