2015-07-27 14:02:38 +02:00
|
|
|
|
|
|
|
/* Controllers */
|
|
|
|
|
2015-07-29 19:15:45 +02:00
|
|
|
netStatsApp.controller('StatsCtrl', function($scope, $filter, socket, _, toastr) {
|
2015-07-27 14:02:38 +02:00
|
|
|
|
|
|
|
var MAX_BINS = 40;
|
|
|
|
|
|
|
|
// Main Stats init
|
|
|
|
// ---------------
|
|
|
|
|
|
|
|
$scope.nodesTotal = 0;
|
|
|
|
$scope.nodesActive = 0;
|
|
|
|
$scope.bestBlock = 0;
|
|
|
|
$scope.lastBlock = 0;
|
|
|
|
$scope.lastDifficulty = 0;
|
|
|
|
$scope.upTimeTotal = 0;
|
|
|
|
$scope.avgBlockTime = 0;
|
|
|
|
$scope.blockPropagationAvg = 0;
|
|
|
|
$scope.avgHashrate = 0;
|
|
|
|
$scope.uncleCount = 0;
|
|
|
|
$scope.bestStats = {};
|
|
|
|
|
|
|
|
$scope.lastBlocksTime = _.fill(Array(MAX_BINS), 2);
|
|
|
|
$scope.difficultyChart = _.fill(Array(MAX_BINS), 2);
|
|
|
|
$scope.transactionDensity = _.fill(Array(MAX_BINS), 2);
|
|
|
|
$scope.gasSpending = _.fill(Array(MAX_BINS), 2);
|
|
|
|
$scope.miners = [];
|
|
|
|
|
|
|
|
|
|
|
|
$scope.nodes = [];
|
|
|
|
$scope.map = [];
|
|
|
|
$scope.blockPropagationChart = [];
|
|
|
|
$scope.uncleCountChart = _.fill(Array(MAX_BINS), 2);
|
|
|
|
$scope.coinbases = [];
|
|
|
|
|
|
|
|
$scope.latency = 0;
|
|
|
|
|
|
|
|
$scope.currentApiVersion = "0.0.16";
|
|
|
|
|
2015-07-29 19:15:45 +02:00
|
|
|
$scope.predicate = ['-pinned', '-stats.active', '-stats.block.number', 'stats.block.propagation'];
|
|
|
|
$scope.reverse = false;
|
|
|
|
$scope.pinned = [];
|
2015-07-27 14:02:38 +02:00
|
|
|
|
|
|
|
$scope.prefixPredicate = ['-pinned', '-stats.active'];
|
|
|
|
$scope.originalPredicate = ['-stats.block.number', 'stats.block.propagation'];
|
|
|
|
|
|
|
|
$scope.orderTable = function(predicate, reverse)
|
|
|
|
{
|
|
|
|
if(!_.isEqual(predicate, $scope.originalPredicate))
|
|
|
|
{
|
|
|
|
$scope.reverse = reverse;
|
|
|
|
$scope.originalPredicate = predicate;
|
|
|
|
$scope.predicate = _.union($scope.prefixPredicate, predicate);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$scope.reverse = !$scope.reverse;
|
|
|
|
|
|
|
|
if($scope.reverse === true){
|
|
|
|
_.forEach(predicate, function (value, key) {
|
|
|
|
predicate[key] = (value[0] === '-' ? value.replace('-', '') : '-' + value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.predicate = _.union($scope.prefixPredicate, predicate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.pinNode = function(id)
|
|
|
|
{
|
|
|
|
index = findIndex({id: id});
|
|
|
|
|
|
|
|
if( !_.isUndefined($scope.nodes[index]) )
|
|
|
|
{
|
|
|
|
$scope.nodes[index].pinned = !$scope.nodes[index].pinned;
|
|
|
|
|
|
|
|
if($scope.nodes[index].pinned)
|
|
|
|
{
|
|
|
|
$scope.pinned.push(id);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$scope.pinned.splice($scope.pinned.indexOf(id), 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var timeout = setInterval(function ()
|
|
|
|
{
|
|
|
|
$scope.$apply();
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
$scope.getNumber = function (num) {
|
|
|
|
return new Array(num);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Socket listeners
|
|
|
|
// ----------------
|
|
|
|
|
|
|
|
socket.on('open', function open() {
|
|
|
|
socket.emit('ready');
|
|
|
|
console.log('The connection has been opened.');
|
|
|
|
})
|
|
|
|
.on('end', function end() {
|
|
|
|
console.log('Socket connection ended.')
|
|
|
|
})
|
|
|
|
.on('error', function error(err) {
|
|
|
|
console.log(err);
|
|
|
|
})
|
|
|
|
.on('reconnecting', function reconnecting(opts) {
|
|
|
|
console.log('We are scheduling a reconnect operation', opts);
|
|
|
|
})
|
|
|
|
.on('data', function incoming(data) {
|
|
|
|
$scope.$apply(socketAction(data.action, data.data));
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on('init', function(data)
|
|
|
|
{
|
|
|
|
$scope.$apply(socketAction("init", data.nodes));
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.on('client-latency', function(data)
|
|
|
|
{
|
|
|
|
$scope.latency = data.latency;
|
|
|
|
})
|
|
|
|
|
|
|
|
function socketAction(action, data)
|
|
|
|
{
|
|
|
|
// console.log('Action: ', action);
|
|
|
|
// console.log('Data: ', data);
|
|
|
|
|
|
|
|
switch(action)
|
|
|
|
{
|
|
|
|
case "init":
|
|
|
|
$scope.nodes = data;
|
|
|
|
|
|
|
|
_.forEach($scope.nodes, function (node, index) {
|
|
|
|
// Init hashrate
|
|
|
|
if( _.isUndefined(node.stats.hashrate) )
|
|
|
|
node.stats.hashrate = 0;
|
|
|
|
|
|
|
|
// Init latency
|
|
|
|
latencyFilter(node);
|
|
|
|
|
|
|
|
// Init history
|
|
|
|
if( _.isUndefined(data.history) )
|
|
|
|
{
|
|
|
|
data.history = new Array(40);
|
|
|
|
_.fill(data.history, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init or recover pin
|
|
|
|
node.pinned = ($scope.pinned.indexOf(node.id) >= 0 ? true : false);
|
|
|
|
});
|
|
|
|
|
|
|
|
if( $scope.nodes.length > 0 )
|
|
|
|
{
|
|
|
|
toastr['success']("Got nodes list", "Got nodes!");
|
|
|
|
|
|
|
|
updateActiveNodes();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "add":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( addNewNode(data) )
|
|
|
|
toastr['success']("New node "+ $scope.nodes[findIndex({id: data.id})].info.name +" connected!", "New node!");
|
|
|
|
else
|
|
|
|
toastr['info']("Node "+ $scope.nodes[index].info.name +" reconnected!", "Node is back!");
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
// TODO: Remove when everybody updates api client to 0.0.12
|
|
|
|
case "update":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) )
|
|
|
|
{
|
|
|
|
if( !_.isUndefined($scope.nodes[index].stats.latency) )
|
|
|
|
data.stats.latency = $scope.nodes[index].stats.latency;
|
|
|
|
|
|
|
|
if( _.isUndefined(data.stats.hashrate) )
|
|
|
|
data.stats.hashrate = 0;
|
|
|
|
|
|
|
|
if( $scope.nodes[index].stats.block.number < data.stats.block.number )
|
|
|
|
{
|
|
|
|
var best = _.max($scope.nodes, function (node) {
|
|
|
|
return parseInt(node.stats.block.number);
|
|
|
|
}).stats.block;
|
|
|
|
|
|
|
|
if (data.stats.block.number > best.number) {
|
|
|
|
data.stats.block.arrived = _.now();
|
|
|
|
} else {
|
|
|
|
data.stats.block.arrived = best.arrived;
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.nodes[index].history = data.history;
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.nodes[index].stats = data.stats;
|
|
|
|
|
|
|
|
if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency )
|
|
|
|
{
|
|
|
|
$scope.nodes[index].stats.latency = data.stats.latency;
|
|
|
|
|
|
|
|
latencyFilter($scope.nodes[index]);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateBestBlock();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "block":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( index >= 0 && !_.isUndefined($scope.nodes[index]) && !_.isUndefined($scope.nodes[index].stats) )
|
|
|
|
{
|
|
|
|
if( $scope.nodes[index].stats.block.number < data.block.number )
|
|
|
|
{
|
|
|
|
var best = _.max($scope.nodes, function (node) {
|
|
|
|
return parseInt(node.stats.block.number);
|
|
|
|
}).stats.block;
|
|
|
|
|
|
|
|
if (data.block.number > best.number) {
|
|
|
|
data.block.arrived = _.now();
|
|
|
|
} else {
|
|
|
|
data.block.arrived = best.arrived;
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.nodes[index].history = data.history;
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.nodes[index].stats.block = data.block;
|
|
|
|
$scope.nodes[index].stats.propagationAvg = data.propagationAvg;
|
|
|
|
|
|
|
|
updateBestBlock();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "pending":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( !_.isUndefined(data.id) && index >= 0 )
|
|
|
|
{
|
|
|
|
var node = $scope.nodes[index];
|
|
|
|
|
|
|
|
if( !_.isUndefined(node) && !_.isUndefined(node.stats.pending) && !_.isUndefined(data.pending) )
|
|
|
|
$scope.nodes[index].stats.pending = data.pending;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "stats":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( !_.isUndefined(data.id) && index >= 0 )
|
|
|
|
{
|
|
|
|
var node = $scope.nodes[index];
|
|
|
|
|
|
|
|
if( !_.isUndefined(node) && !_.isUndefined(node.stats) )
|
|
|
|
{
|
|
|
|
$scope.nodes[index].stats.active = data.stats.active;
|
|
|
|
$scope.nodes[index].stats.mining = data.stats.mining;
|
|
|
|
$scope.nodes[index].stats.hashrate = data.stats.hashrate;
|
|
|
|
$scope.nodes[index].stats.peers = data.stats.peers;
|
|
|
|
$scope.nodes[index].stats.gasPrice = data.stats.gasPrice;
|
|
|
|
$scope.nodes[index].stats.uptime = data.stats.uptime;
|
|
|
|
|
|
|
|
if( !_.isUndefined(data.stats.latency) && _.get($scope.nodes[index], 'stats.latency', 0) !== data.stats.latency )
|
|
|
|
{
|
|
|
|
$scope.nodes[index].stats.latency = data.stats.latency;
|
|
|
|
|
|
|
|
latencyFilter($scope.nodes[index]);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateActiveNodes();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "info":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( index >= 0 )
|
|
|
|
{
|
|
|
|
$scope.nodes[index].info = data.info;
|
|
|
|
|
|
|
|
if( _.isUndefined($scope.nodes[index].pinned) )
|
|
|
|
$scope.nodes[index].pinned = false;
|
|
|
|
|
|
|
|
// Init latency
|
|
|
|
latencyFilter($scope.nodes[index]);
|
|
|
|
|
|
|
|
updateActiveNodes();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "blockPropagationChart":
|
|
|
|
$scope.blockPropagationChart = data.histogram;
|
|
|
|
$scope.blockPropagationAvg = data.avg;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "uncleCount":
|
|
|
|
$scope.uncleCount = data[0] + data[1];
|
|
|
|
data.reverse();
|
|
|
|
$scope.uncleCountChart = data;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "charts":
|
|
|
|
if( !_.isEqual($scope.avgBlockTime, data.avgBlocktime) )
|
|
|
|
$scope.avgBlockTime = data.avgBlocktime;
|
|
|
|
|
|
|
|
if( !_.isEqual($scope.avgHashrate, data.avgHashrate) )
|
|
|
|
$scope.avgHashrate = data.avgHashrate;
|
|
|
|
|
|
|
|
if( !_.isEqual($scope.lastBlocksTime, data.blocktime) && data.blocktime.length >= MAX_BINS )
|
|
|
|
$scope.lastBlocksTime = data.blocktime;
|
|
|
|
|
|
|
|
data.uncleCount.reverse();
|
|
|
|
|
|
|
|
if( !_.isEqual($scope.uncleCountChart, data.uncleCount) && data.uncleCount.length >= MAX_BINS ) {
|
|
|
|
$scope.uncleCount = data.uncleCount[data.uncleCount.length-2] + data.uncleCount[data.uncleCount.length-1];
|
|
|
|
$scope.uncleCountChart = data.uncleCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "inactive":
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( index >= 0 )
|
|
|
|
{
|
|
|
|
if( !_.isUndefined(data.stats) )
|
|
|
|
$scope.nodes[index].stats = data.stats;
|
|
|
|
|
|
|
|
toastr['error']("Node "+ $scope.nodes[index].info.name +" went away!", "Node connection was lost!");
|
|
|
|
|
|
|
|
updateActiveNodes();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "latency":
|
|
|
|
if( !_.isUndefined(data.id) && !_.isUndefined(data.latency) )
|
|
|
|
{
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( index >= 0 )
|
|
|
|
{
|
|
|
|
var node = $scope.nodes[index];
|
|
|
|
|
|
|
|
if( !_.isUndefined(node) && !_.isUndefined(node.stats) && !_.isUndefined(node.stats.latency) && node.stats.latency !== data.latency )
|
|
|
|
{
|
|
|
|
node.stats.latency = data.latency;
|
|
|
|
latencyFilter(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "client-ping":
|
|
|
|
socket.emit('client-pong', {
|
|
|
|
serverTime: data.serverTime,
|
|
|
|
clientTime: _.now()
|
|
|
|
});
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function findIndex(search)
|
|
|
|
{
|
|
|
|
return _.findIndex($scope.nodes, search);
|
|
|
|
}
|
|
|
|
|
|
|
|
function addNewNode(data)
|
|
|
|
{
|
|
|
|
var index = findIndex({id: data.id});
|
|
|
|
|
|
|
|
if( _.isUndefined(data.history) )
|
|
|
|
{
|
|
|
|
data.history = new Array(40);
|
|
|
|
_.fill(data.history, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if( index < 0 )
|
|
|
|
{
|
|
|
|
if( !_.isUndefined(data.stats) && _.isUndefined(data.stats.hashrate) )
|
|
|
|
{
|
|
|
|
data.stats.hashrate = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.pinned = false;
|
|
|
|
|
|
|
|
$scope.nodes.push(data);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.pinned = ( !_.isUndefined($scope.nodes[index].pinned) ? $scope.nodes[index].pinned : false);
|
|
|
|
|
|
|
|
if( !_.isUndefined($scope.nodes[index].history) )
|
|
|
|
{
|
|
|
|
data.history = $scope.nodes[index].history;
|
|
|
|
}
|
|
|
|
|
|
|
|
$scope.nodes[index] = data;
|
|
|
|
|
|
|
|
updateActiveNodes();
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateActiveNodes()
|
|
|
|
{
|
|
|
|
updateBestBlock();
|
|
|
|
|
|
|
|
$scope.nodesTotal = $scope.nodes.length;
|
|
|
|
|
|
|
|
$scope.nodesActive = _.filter($scope.nodes, function (node) {
|
|
|
|
forkFilter(node);
|
|
|
|
return node.stats.active == true;
|
|
|
|
}).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateBestBlock()
|
|
|
|
{
|
|
|
|
if( $scope.nodes.length )
|
|
|
|
{
|
|
|
|
var chains = {};
|
|
|
|
var maxScore = 0;
|
|
|
|
|
|
|
|
_($scope.nodes)
|
|
|
|
.map(function (item)
|
|
|
|
{
|
|
|
|
maxScore += (item.trusted ? 50 : 1);
|
|
|
|
|
|
|
|
if( _.isUndefined(chains[item.stats.block.number]) )
|
|
|
|
chains[item.stats.block.number] = [];
|
|
|
|
|
|
|
|
if( _.isUndefined(chains[item.stats.block.number][item.stats.block.fork]) )
|
|
|
|
chains[item.stats.block.number][item.stats.block.fork] = {
|
|
|
|
fork: item.stats.block.fork,
|
|
|
|
count: 0,
|
|
|
|
trusted: 0,
|
|
|
|
score: 0
|
|
|
|
};
|
|
|
|
|
|
|
|
if(item.stats.block.trusted)
|
|
|
|
chains[item.stats.block.number][item.stats.block.fork].trusted++;
|
|
|
|
else
|
|
|
|
chains[item.stats.block.number][item.stats.block.fork].count++;
|
|
|
|
|
|
|
|
chains[item.stats.block.number][item.stats.block.fork].score = chains[item.stats.block.number][item.stats.block.fork].trusted * 50 + chains[item.stats.block.number][item.stats.block.fork].count;
|
|
|
|
})
|
|
|
|
.value();
|
|
|
|
|
|
|
|
$scope.maxScore = maxScore;
|
|
|
|
$scope.chains = _.reduce(chains, function (result, item, key)
|
|
|
|
{
|
|
|
|
result[key] = _.max(item, 'score');
|
|
|
|
return result;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
var bestBlock = _.max($scope.nodes, function (node)
|
|
|
|
{
|
|
|
|
// if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
|
|
|
|
// {
|
|
|
|
return parseInt(node.stats.block.number);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return 0;
|
|
|
|
}).stats.block.number;
|
|
|
|
|
|
|
|
if( bestBlock !== $scope.bestBlock )
|
|
|
|
{
|
|
|
|
$scope.bestBlock = bestBlock;
|
|
|
|
$scope.bestStats = _.max($scope.nodes, function (node) {
|
|
|
|
return parseInt(node.stats.block.number);
|
|
|
|
}).stats;
|
|
|
|
|
|
|
|
$scope.lastBlock = $scope.bestStats.block.arrived;
|
|
|
|
$scope.lastDifficulty = $scope.bestStats.block.difficulty;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function forkFilter(node)
|
|
|
|
{
|
|
|
|
if( _.isUndefined(node.readable) )
|
|
|
|
node.readable = {};
|
|
|
|
|
|
|
|
node.readable.forkClass = 'hidden';
|
|
|
|
node.readable.forkMessage = '';
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if( $scope.chains[node.stats.block.number].fork === node.stats.block.fork && $scope.chains[node.stats.block.number].score / $scope.maxScore >= 0.5 )
|
|
|
|
{
|
|
|
|
node.readable.forkClass = 'hidden';
|
|
|
|
node.readable.forkMessage = '';
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( $scope.chains[node.stats.block.number].fork !== node.stats.block.fork )
|
|
|
|
{
|
|
|
|
node.readable.forkClass = 'text-danger';
|
|
|
|
node.readable.forkMessage = 'Wrong chain.<br/>This chain is a fork.';
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( $scope.chains[node.stats.block.number].score / $scope.maxScore < 0.5)
|
|
|
|
{
|
|
|
|
node.readable.forkClass = 'text-warning';
|
|
|
|
node.readable.forkMessage = 'May not be main chain.<br/>Waiting for more confirmations.';
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function latencyFilter(node)
|
|
|
|
{
|
|
|
|
if( _.isUndefined(node.readable) )
|
|
|
|
node.readable = {};
|
|
|
|
|
|
|
|
if( _.isUndefined(node.stats) ) {
|
|
|
|
node.readable.latencyClass = 'text-danger';
|
|
|
|
node.readable.latency = 'offline';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.stats.active === false)
|
|
|
|
{
|
|
|
|
node.readable.latencyClass = 'text-danger';
|
|
|
|
node.readable.latency = 'offline';
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (node.stats.latency <= 100)
|
|
|
|
node.readable.latencyClass = 'text-success';
|
|
|
|
|
|
|
|
if (node.stats.latency > 100 && node.stats.latency <= 1000)
|
|
|
|
node.readable.latencyClass = 'text-warning';
|
|
|
|
|
|
|
|
if (node.stats.latency > 1000)
|
|
|
|
node.readable.latencyClass = 'text-danger';
|
|
|
|
|
|
|
|
node.readable.latency = node.stats.latency + ' ms';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|