var dashboard_data = {}; var dashboard_svg = {}; var date_from = new Date();
var running_data = {}; var weekly_data = {
platform_count: {}, clone_max: 0, clone_platform_max: 0, clone_count: [], clone_avg: [], boot_avg: []
}; var capacity_data = {};
var colorscale = d3.scale.category20(); var stack = d3.layout.stack().values(function(d) { return d.values; });
Date.prototype.yyyymmdd = function() {
var yyyy = this.getFullYear().toString(); var mm = (this.getMonth()+1).toString(); var dd = this.getDate().toString(); return yyyy + '-' + (mm[1] ? mm : '0' + mm[0]) + '-' + (dd[1] ? dd : '0' + dd[0]);
};
var data_url = {
'capacity': '/dashboard/stats/vmpooler/pool', 'pools' : '/api/v1/vm', 'running' : '/dashboard/stats/vmpooler/running', 'status' : '/api/v1/status', 'summary' : '/api/v1/summary'
};
//——————————————————————– // Everything below this line will be updated in-browser via tick(); //——————————————————————–
(function tick() { setTimeout(function() { // <self-update>
// Update “today's” date
date_from.setDate(date_from.getDate() - 6);
// Gather up data from multiple endpoints
$.each([
'capacity', 'pools', 'running', 'status', 'summary'
], function(index, value) {
dashboard_data[value] = (function() { var dashboard_data__live = null; var url = data_url[value]; // Get history if this is the first tick() iteration switch (value) { case 'capacity': if (! dashboard_data[value]) { url += ('?history=1'); }; break; case 'running' : if (! dashboard_data[value]) { url += ('?history=1'); }; break; case 'summary' : if (! dashboard_data[value]) { url += ('?from=' + date_from.yyyymmdd()); }; break; } $.ajax({ 'url': url, 'async': false, 'global': false, 'dataType': 'json', 'success': function(data) { dashboard_data__live = data; } }); return dashboard_data__live; })();
});
// Create an array of pool_maj
dashboard_data = {}; dashboard_data = []; dashboard_data.sort().map(function(pool) {
var pool_maj = pool.split('-', 2)[0]; if (! dashboard_data['tmp'][pool_maj]) { dashboard_data['pools_maj'].push(pool_maj); dashboard_data['tmp'][pool_maj] = 1; }
}); delete dashboard_data;
// Create a color swatch for each pool_maj
dashboard_data = {}; dashboard_data = 0; dashboard_data.sort().map(function(pool_maj) {
dashboard_data['color'][pool_maj] = colorscale(dashboard_data['tmp']); dashboard_data['tmp']++;
}); delete dashboard_data;
// dashboard-numbers // Numerical metrics (# cloning, running, ready, waiting, etc.)
$('#dashboard-numbers').empty();
var numbers_width = parseInt(d3.select('.col-md-2').style('width')) / 2; var numbers_height = 75; var numbers_data = {
label: { 'clone_total': 'cloned today', 'clone_average': 'clone time avg', 'capacity': 'capacity pct', 'total': 'total # of VMs', 'ready': 'ready & waiting', 'cloning': 'being cloned', 'booting': 'booting up', 'running': 'running tests', 'completed': 'waiting to die' }, key: { 'clone_total': dashboard_data['status']['clone']['count']['total'], 'clone_average': dashboard_data['status']['clone']['duration']['average'] + 's', 'capacity': dashboard_data['status']['capacity']['percent'], 'total': dashboard_data['status']['queue']['total'], 'ready': dashboard_data['status']['queue']['ready'], 'cloning': dashboard_data['status']['queue']['cloning'], 'booting': dashboard_data['status']['queue']['booting'], 'running': dashboard_data['status']['queue']['running'], 'completed': dashboard_data['status']['queue']['completed'] }
};
$.each([
'clone_total', 'clone_average', 'capacity', 'total', 'ready', 'cloning', 'booting', 'running', 'completed'
], function(index, value) {
dashboard_svg[value] = d3.select('#dashboard-numbers') .append('svg') .style('float', 'right') .attr('class', 'col-md-1') .attr('height', numbers_height); dashboard_svg[value] .append('text') .text( (numbers_data['label'][value]) ) .attr({ 'text-anchor': 'end', 'x': numbers_width - 5, 'y': '50', 'font-face': '\'PT Sans\', sans-serif', 'font-size': '12px', 'font-weight': 'bold', 'fill': '#666' }); dashboard_svg[value] .append('text') .text( (numbers_data['key'][value]) ) .attr({ 'text-anchor': 'end', 'x': numbers_width - 5, 'y': '36', 'font-face': '\'PT Sans\', sans-serif', 'font-weight': 'bold', 'font-size': '40px', 'letter-spacing': '-0.025em', 'fill': '#444' });
});
numbers_data = null;
// dashboard-running // By-platform graph of what's been running for the past hour; includes pool_maj legend
$('#dashboard-running').empty();
var running_width = parseInt(d3.select('.col-md-10').style('width')); var running_height = 160;
if (! running_data) {
running_data['stack'] = [];
}
// Process 'running' history
dashboard_data.sort().map(function(pool_maj) {
if (dashboard_data['running'][pool_maj]['history']) { for (var c = 0; c < dashboard_data['running'][pool_maj]['history'].length; c++) { if (! running_data['stack'][c]) { running_data['stack'][c] = {}; } running_data['stack'][c][pool_maj] = dashboard_data['running'][pool_maj]['history'][c]; } }
});
if (! running_data) {
running_data['tmp'] = []; for (var metric in running_data['stack']) { for (var c = 0; c < 8; c++) { running_data['tmp'].push(running_data['stack'][metric]); } } running_data['stack'] = running_data['tmp']; delete running_data['tmp'];
}
// Process 'running' newest values and add them to the stack
dashboard_data = {}; for (var key in dashboard_data) {
dashboard_data['tmp'][key] = dashboard_data['running'][key]['running'];
}
running_data.push(dashboard_data); delete dashboard_data;
// Calculate 'running' graph stack
running_data = stack(
dashboard_data['pools_maj'].sort().map(function(pool_maj) { return { name: pool_maj, values: running_data['stack'].map(function(d) { return { y: d[pool_maj] }; }) } })
);
// Calculate 'running' graph shapes
running_data = d3.max(
running_data['graph'], function(layer) { return d3.max(layer.values, function(d) { return d.y0 + d.y; }); }
);
var running_x = d3.scale.linear().domain([0, 500]).range([5, running_width - 20]); var running_y = d3.scale.linear().domain([0, running_data]).range([running_height, 0]);
var running_area = d3.svg.area()
.x(function(d, i) { return running_x(i); }) .y0(function(d) { return running_y(d.y0); }) .y1(function(d) { return running_y(d.y0 + d.y); });
// The 'running' SVG
var running_graph = d3.select('#dashboard-running')
.append('svg') .attr('height', running_height) .attr('class', 'col-md-10') .append('g');
dashboard_svg = running_graph.selectAll('#dashboard-running')
.data(running_data['graph']) .enter() .append('g');
// A texture defs = dashboard_svg.append('svg:defs');
defs.append('svg:pattern')
.attr('id', 'background') .attr('patternUnits', 'userSpaceOnUse') .attr('width', '500px') .attr('height', '500px') .append('svg:image') .attr('xlink:href', '/img/textured_paper.png') .attr('x', 0) .attr('y', 0) .attr('width', '500px') .attr('height', '500px');
.append('path') .attr('class', 'area') .attr('d', function(d) { return running_area(d.values); }) .attr('opacity', '0.75') .style('fill', 'url(#background)');
.append('path') .attr('class', 'area') .attr('d', function(d) { return running_area(d.values); }) .attr('opacity', '0.5') .style('fill', function(d) { return dashboard_data['color'][d.name]; });
// Legend
dashboard_data.sort().map(function(pool_maj) {
dashboard_svg['legend' + pool_maj] = d3.select('#dashboard-running') .append('svg') .attr('class', 'col-md-1') .attr('height', '20px'); dashboard_svg['legend' + pool_maj] .append('rect') .attr({ 'x': '5', 'y': '5', 'width': '10', 'height': '10', 'opacity': '0.75', 'fill': 'url(#background)' }); dashboard_svg['legend' + pool_maj] .append('rect') .attr({ 'x': '5', 'y': '5', 'width': '10', 'height': '10', 'opacity': '0.5', 'fill': function(d) { return dashboard_data['color'][pool_maj]; } }); dashboard_svg['legend' + pool_maj] .append('text') .text( (pool_maj) ) .attr({ 'x': '20', 'y': '15', 'font-face': '\'PT Sans\', sans-serif', 'font-size': '12px', 'font-weight': 'bold', 'fill': '#666' }); }
);
if (running_data.length > 500) {
running_data['stack'].shift();
}
// dashboard-weekly // Weekly graphs (daily clone count, clone/boot avgs, etc.)
$('#dashboard-weekly').empty();
var weekly_width = (parseInt(d3.select('.col-md-2').style('width')) * 2) - 10; var weekly_height = 100;
// Update based on if it's a new day, first tick() iteration, or neither
if (dashboard_data['daily'].length == 1) {
weekly_data['clone_count'].pop(); weekly_data['clone_avg'].pop(); weekly_data['boot_avg'].pop();
} else if (dashboard_data['daily'].length == 7) {
weekly_data['clone_count'] = []; weekly_data['clone_avg'] = []; weekly_data['boot_avg'] = []; weekly_data['platform_count'] = {};
}
dashboard_data['daily'].sort().map(function(day) {
weekly_data['clone_count'].push(day['clone']['count']['total']); weekly_data['clone_avg'].push(day['clone']['duration']['average']); weekly_data['boot_avg'].push(day['boot']['duration']['average']); if (day['clone']['count']['total'] > weekly_data['clone_max']) { weekly_data['clone_max'] = day['clone']['count']['total']; }
});
// Consolidate clone totals into pool_maj groups
dashboard_data.sort().map(function(pool) {
var pool_maj = pool.split('-', 2)[0]; if (! weekly_data['platform_count'][pool_maj]) { weekly_data['platform_count'][pool_maj] = 0; } if (dashboard_data['summary']['clone']['count']['pool'][pool]) { weekly_data['platform_count'][pool_maj] += dashboard_data['summary']['clone']['count']['pool'][pool]['total']; }
});
dashboard_data.sort().map(function(pool_maj) {
if (weekly_data['platform_count'][pool_maj] > weekly_data['clone_platform_max']) { weekly_data['clone_platform_max'] = weekly_data['platform_count'][pool_maj]; }
});
var weekly_x = d3.scale.linear().domain([0, 6]).range([0, weekly_width]); var weekly_y_clone_count = d3.scale.linear().domain([0, weekly_data]).range([weekly_height, 0]); var weekly_y_boot_avg = d3.scale.linear().domain([0, Math.max.apply(Math, weekly_data)]).range([weekly_height, 0]); var weekly_y_platform_count = d3.scale.linear().domain([0, weekly_data]).range([weekly_height, 0]);
var area_clone_count = d3.svg.area()
.interpolate('linear') .x(function(d, i) { return weekly_x(i); }) .y0(weekly_height) .y1(function(d) { return weekly_y_clone_count(d); });
var area_boot_avg = d3.svg.area()
.interpolate('linear') .x(function(d, i) { return weekly_x(i); }) .y0(weekly_height) .y1(function(d) { return weekly_y_boot_avg(d); });
// Create some SVGs
for (graph in graphs = ['clone_count', 'clone_boot_avg']) {
dashboard_svg[graphs[graph]] = d3.select('#dashboard-weekly') .append('svg') .attr('class', 'col-md-4') .attr('height', weekly_height); dashboard_svg[graphs[graph]] .append('g') .attr('class', 'x tick') .attr('transform', 'translate(0,' + (weekly_height) + ')') .call( d3.svg.axis() .scale(weekly_x) .ticks(7) .tickSize(-weekly_height) .outerTickSize(0) .tickFormat('') .tickSubdivide(true) .orient('bottom') );
}
dashboard_svg = d3.select('#dashboard-weekly')
.append('svg') .attr('class', 'col-md-4') .attr('height', weekly_height);
.append('g') .attr('class', 'x tick') .attr('transform', 'translate(0,' + (weekly_height) + ')') .call( d3.svg.axis() .scale(weekly_x) .ticks(0) .tickSize(-weekly_height) .orient('bottom') );
// Area shapes for clone_count and clone/boot time avgs
var area_clone_count = d3.svg.area()
.interpolate('linear') .x(function(d, i) { return weekly_x(i); }) .y0(weekly_height) .y1(function(d) { return weekly_y_clone_count(d); });
var area_boot_avg = d3.svg.area()
.interpolate('linear') .x(function(d, i) { return weekly_x(i); }) .y0(weekly_height) .y1(function(d) { return weekly_y_boot_avg(d); });
.append('path') .attr({ 'class': 'area', 'fill': 'url(#background)', 'opacity': '0.75', 'd': area_clone_count(weekly_data['clone_count']) });
.append('path') .attr({ 'class': 'area', 'fill': 'seagreen', 'opacity': '0.5', 'd': area_clone_count(weekly_data['clone_count']) });
.append('path') .attr({ 'class': 'area', 'fill': 'url(#background)', 'opacity': '0.75', 'd': area_boot_avg(weekly_data['boot_avg']) });
.append('path') .attr({ 'class': 'area', 'fill': 'crimson', 'opacity': '0.5', 'd': area_boot_avg(weekly_data['boot_avg']) });
.append('path') .attr({ 'class': 'area', 'fill': 'gold', 'opacity': '0.75', 'd': area_boot_avg(weekly_data['clone_avg']) });
// Add a bar to the platform_count raph for each pool_maj
dashboard_data = 0; dashboard_data.sort().map(function(pool_maj) {
var x = dashboard_data['tmp'] * (weekly_width / dashboard_data['pools_maj'].length); var y = weekly_y_platform_count(weekly_data['platform_count'][pool_maj]) - 1; var width = weekly_width / dashboard_data['pools_maj'].length; if (y == -1) { y = 0; } dashboard_svg['platform_count'] .append('rect') .attr({ 'x': x, 'y': y, 'width': width, 'height': weekly_height, 'fill': 'url(#background)', 'opacity': '0.75' }); dashboard_svg['platform_count'] .append('rect') .attr({ 'x': x, 'y': y, 'width': width, 'height': weekly_height, 'fill': function(d) { return dashboard_data['color'][pool_maj]; }, 'opacity': '0.5' }); dashboard_data['tmp']++;
}); delete dashboard_data;
// dashboard-pool // Many little graphs showing individual pool capacities
$('#dashboard-pool').empty();
var capacity_col_class = 'col-md-2'; var capacity_width = parseInt(d3.select('.col-md-2').style('width')); var capacity_height = 47;
if (capacity_width > 250) {
capacity_col_class = 'col-md-1'; capacity_width = parseInt(d3.select('.col-md-1').style('width'));
}
dashboard_data.sort().map(function(pool) {
var capacity_x = d3.scale.linear().domain([0, 500]).range([5, capacity_width - 5]); var capacity_y = d3.scale.linear().domain([dashboard_data['capacity'][pool]['size'], 0]).range([0, capacity_height - 15]); var capacity_area = d3.svg.area() .interpolate('basis') .x(function(d, i) { return capacity_x(i); }) .y0(capacity_height - 15) .y1(function(d) { return capacity_y(d); }); var capacity_path = d3.svg.line() .interpolate('basis') .x(function(d, i) { return capacity_x(i); }) .y(function(d) { return capacity_y(d); }); if (! capacity_data[pool]) { capacity_data[pool] = {}; } if (! capacity_data[pool]['r']) { capacity_data[pool]['r'] = []; } // Process 'capacity' history if (dashboard_data['capacity'][pool]['history']) { capacity_data[pool]['r'] = dashboard_data['capacity'][pool]['history']; } capacity_data[pool]['r'].push(dashboard_data['capacity'][pool]['ready']); var capacity_current = capacity_data[pool]['r'].slice(-1)[0]; var capacity_size = dashboard_data['capacity'][pool]['size']; var capacity_pct = Math.floor((capacity_current / capacity_size) * 100); var statuscolor = '#78a830'; if (capacity_pct < 50) { statuscolor = '#f0a800'; } if (capacity_pct < 25) { statuscolor = '#d84830'; } // Define 'capacity' SVG dashboard_svg['capacity' + pool] = d3.select('#dashboard-pool') .append('svg') .attr('class', capacity_col_class) .attr('height', capacity_height); dashboard_svg['capacity' + pool] .append('g') .attr('class', 'x tick') .attr('transform', 'translate(0,' + (capacity_height - 15) + ')') .call( d3.svg.axis() .scale(capacity_x) .ticks(4) .tickSize(-capacity_height) .outerTickSize(0) .tickFormat('') .tickSubdivide(true) .orient('bottom') ); // Draw 'capacity' path dashboard_svg['capacity' + pool] .append('path') .attr('class', 'area') .attr('fill', 'url(#background)') .attr('opacity', '0.75') .attr('d', capacity_area(capacity_data[pool]['r'])); dashboard_svg['capacity' + pool] .append('path') .attr('class', 'area') .attr('fill', statuscolor) .attr('opacity', '0.5') .attr('d', capacity_area(capacity_data[pool]['r'])); dashboard_svg['capacity' + pool] .append('path') .attr('class', 'line') .attr('stroke', statuscolor) .attr('stroke-width', '1') .attr('d', capacity_path(capacity_data[pool]['r'])); // Add labels to 'capacity' graphs dashboard_svg['capacity' + pool] .append('text') .text( (pool) ) .attr({ 'x': '10', 'y': capacity_height - 33, 'font-face': '\'PT Sans\', sans-serif', 'font-weight': 'bold', 'font-size': '12px', 'fill': '#444' }); dashboard_svg['capacity' + pool] .append('text') .text( (capacity_pct + '%') ) .attr({ 'x': '10', 'y': capacity_height - 20, 'font-face': '\'PT Sans\', sans-serif', 'font-size': '12px', 'letter-spacing': '-0.05em', 'fill': '#444' }); dashboard_svg['capacity' + pool] .append('text') .text( ('(') + (capacity_current) + ('/') + (capacity_size) + (')') ) .attr({ 'x': 45, 'y': capacity_height - 20, 'font-face': '\'PT Sans\', sans-serif', 'font-size': '12px', 'letter-spacing': '-0.05em', 'fill': '#444' }); if (capacity_data[pool]['r'].length > 500) { capacity_data[pool]['r'].shift(); }
});
// Hide the 'loading' screen
$('#loading').hide();
// Refresh!
tick(); }, 5000); })(); // </self-update>