// The Console handles all of the Admin interaction with the active workers
// and job queue.
//
// Think about pulling in the DCJS framework, instead of just raw jQuery here.
// Leaving it hacked together like this just cries out for templates, dunnit?
window.Console = {
// Maximum number of data points to record and graph.
MAX_DATA_POINTS : 100,
// Milliseconds between polling the central server for updates to Job progress.
POLL_INTERVAL : 3000,
// Default speed for all animations.
ANIMATION_SPEED : 300,
// Keep this in sync with the map in cloud-crowd.rb
DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'],
// Images to preload
PRELOAD_IMAGES : ['/images/server_error.png'],
// All options for drawing the system graphs.
GRAPH_OPTIONS : {
xaxis : {mode : 'time', timeformat : '%M:%S'},
yaxis : {tickDecimals : 0},
legend : {show : false},
grid : {backgroundColor : '#7f7f7f', color : '#555', tickColor : '#666', borderWidth : 2}
},
JOBS_COLOR : '#db3a0f',
NODES_COLOR : '#1870ab',
WORKERS_COLOR : '#45a4e5',
WORK_UNITS_COLOR : '#ffba14',
// Starting the console begins polling the server.
initialize : function() {
this._jobsHistory = [];
this._nodesHistory = [];
this._workersHistory = [];
this._workUnitsHistory = [];
this._histories = [this._jobsHistory, this._nodesHistory, this._workersHistory, this._workUnitsHistory];
this._queue = $('#jobs');
this._workerInfo = $('#worker_info');
this._disconnected = $('#disconnected');
$(window).bind('resize', Console.renderGraphs);
$('#nodes .worker').live('click', Console.getWorkerInfo);
$('#workers_legend').css({background : this.WORKERS_COLOR});
$('#nodes_legend').css({background : this.NODES_COLOR});
this.getStatus();
$.each(this.PRELOAD_IMAGES, function(){ var i = new Image(); i.src = this; });
},
// Request the lastest status of all jobs and workers, re-render or update
// the DOM to reflect.
getStatus : function() {
$.ajax({url : '/status', dataType : 'json', success : function(resp) {
Console._jobs = resp.jobs;
Console._nodes = resp.nodes;
Console._workUnitCount = resp.work_unit_count;
Console._workerCount = Console.countWorkers();
Console.recordDataPoint();
if (Console._disconnected.is(':visible')) Console._disconnected.fadeOut(Console.ANIMATION_SPEED);
$('#queue').toggleClass('no_jobs', Console._jobs.length <= 0);
Console.renderJobs();
Console.renderNodes();
Console.renderGraphs();
setTimeout(Console.getStatus, Console.POLL_INTERVAL);
}, error : function(request, status, errorThrown) {
if (!Console._disconnected.is(':visible')) Console._disconnected.fadeIn(Console.ANIMATION_SPEED);
setTimeout(Console.getStatus, Console.POLL_INTERVAL);
}});
},
// Count the total number of workers in the current list of nodes.
countWorkers : function() {
var sum = 0;
for (var i=0; i < this._nodes.length; i++) sum += this._nodes[i].workers.length;
return sum;
},
// Render an individual job afresh.
renderJob : function(job) {
this._queue.append('
' + job.percent_complete + '%
#' + job.id + '
');
},
// Animate the update to an existing job in the queue.
updateJob : function(job, jobEl) {
jobEl.animate({width : job.width + '%'}, this.ANIMATION_SPEED);
var completion = $('.completion', jobEl);
if (job.percent_complete > 0) completion.removeClass('zero');
completion.animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED);
$('.percent_complete', jobEl).html(job.percent_complete + '%');
},
// Render all jobs, calculating relative widths and completions.
renderJobs : function() {
var totalUnits = 0;
var totalWidth = this._queue.width();
var jobIds = [];
$.each(this._jobs, function() {
jobIds.push(this.id);
totalUnits += this.work_units;
});
$.each($('.job'), function() {
var el = this;
if (jobIds.indexOf(parseInt(el.id.replace(/\D/g, ''), 10)) < 0) {
$(el).animate({width : '0%'}, Console.ANIMATION_SPEED - 50, 'linear', function() {
$(el).remove();
});
}
});
$.each(this._jobs, function() {
this.width = (this.work_units / totalUnits) * 100;
var jobEl = $('#job_' + this.id);
jobEl[0] ? Console.updateJob(this, jobEl) : Console.renderJob(this);
});
},
// Re-render all workers from scratch each time.
// This method is desperately in need of Javascript templates...
renderNodes : function() {
var header = $('#sidebar_header');
var nc = this._nodes.length, wc = this._workerCount;
$('.has_nodes', header).html(nc + " Node" + (nc != 1 ? 's' : '') + " / " + wc + " Worker" + (wc != 1 ? 's' : ''));
header.toggleClass('no_nodes', this._nodes.length <= 0);
$('#nodes').html($.map(this._nodes, function(node) {
var html = "";
var extra = node.status == 'busy' ? ' [busy]' : '';
html += '' + node.host + extra + '
';
html += $.map(node.workers, function(pid) {
var name = pid + '@' + node.host;
return '' + name + '
';
}).join('');
return html;
}).join(''));
},
// Record the current state and re-render all graphs.
recordDataPoint : function() {
var timestamp = (new Date()).getTime();
this._jobsHistory.push([timestamp, this._jobs.length]);
this._nodesHistory.push([timestamp, this._nodes.length]);
this._workersHistory.push([timestamp, this._workerCount]);
this._workUnitsHistory.push([timestamp, this._workUnitCount]);
$.each(this._histories, function() {
if (this.length > Console.MAX_DATA_POINTS) this.shift();
});
},
// Convert our recorded data points into a format Flot can understand.
renderGraphs : function() {
$.plot($('#work_units_graph'), [
{label : 'Work Units in Queue', color : Console.WORK_UNITS_COLOR, data : Console._workUnitsHistory}
], Console.GRAPH_OPTIONS);
$.plot($('#jobs_graph'), [
{label : 'Jobs in Queue', color : Console.JOBS_COLOR, data : Console._jobsHistory}
], Console.GRAPH_OPTIONS);
$.plot($('#workers_graph'), [
{label : 'Nodes', color : Console.NODES_COLOR, data : Console._nodesHistory},
{label : 'Workers', color : Console.WORKERS_COLOR, data : Console._workersHistory}
], Console.GRAPH_OPTIONS);
},
// Request the Worker info from the central server.
getWorkerInfo : function(e) {
e.stopImmediatePropagation();
var info = Console._workerInfo;
var row = $(this);
info.addClass('loading');
$.get('/worker/' + row.attr('rel'), null, Console.renderWorkerInfo, 'json');
info.css({top : row.offset().top, left : 325});
info.fadeIn(Console.ANIMATION_SPEED);
$(document).bind('click', Console.hideWorkerInfo);
return false;
},
// When we receieve worker info, update the bubble.
renderWorkerInfo : function(resp) {
var info = Console._workerInfo;
info.toggleClass('awake', !!resp.status);
info.removeClass('loading');
if (!resp.status) return;
$('.status', info).html(Console.DISPLAY_STATUS_MAP[resp.status]);
$('.action', info).html(resp.action);
$('.job_id', info).html(resp.job_id);
$('.work_unit_id', info).html(resp.id);
},
// Hide worker info and unbind the global hide handler.
hideWorkerInfo : function() {
$(document).unbind('click', Console.hideWorkerInfo);
Console._workerInfo.fadeOut(Console.ANIMATION_SPEED);
}
};
$(document).ready(function() { Console.initialize(); });