X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/768f5281784b419e4d5617cb34e89298d1899a59..f2652e70c70db7a9a068c5a9bc8c6ec6566a14c6:/apps/workbench/app/assets/javascripts/event_log.js diff --git a/apps/workbench/app/assets/javascripts/event_log.js b/apps/workbench/app/assets/javascripts/event_log.js index 77ed7bf3c9..2c4fe526b4 100644 --- a/apps/workbench/app/assets/javascripts/event_log.js +++ b/apps/workbench/app/assets/javascripts/event_log.js @@ -57,54 +57,230 @@ $(document).on('ajax:complete ready', function() { } }); - +/* Assumes existence of: + window.jobGraphData = []; + window.jobGraphSeries = []; + window.jobGraphMaxima = {}; + */ function processLogLineForChart( logLine ) { - // TODO: make this more robust: anything could go wrong in here - var match = logLine.match(/(.*)crunchstat:(.*)-- interval(.*)/); - if( match ) { - var series = match[2].trim().split(' ')[0]; - if( $.inArray( series, jobGraphSeries) < 0 ) { - jobGraphSeries.push(series); - } - var intervalData = match[3].trim().split(' '); - var dt = parseFloat(intervalData[0]); - var dsum = 0.0; - for(var i=2; i < intervalData.length; i += 2 ) { - dsum += parseFloat(intervalData[i]); + try { + var match = logLine.match(/(\S+) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*) -- interval (.*)/); + if( match ) { + // the timestamp comes first + var timestamp = match[1].replace('_','T'); + // for the series use the first word after 'crunchstat:' + var series = match[5]; + // and append the task number (the 4th term) + series += '-' + match[4] + if( $.inArray( series, jobGraphSeries) < 0 ) { + jobGraphSeries.push(series); + jobGraphMaxima[series] = null; + window.recreate = true; + } + var intervalData = match[7].trim().split(' '); + var dt = parseFloat(intervalData[0]); + var dsum = 0.0; + for(var i=2; i < intervalData.length; i += 2 ) { + dsum += parseFloat(intervalData[i]); + } + var datum = dsum/dt; + if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) { + if( isJobSeriesRescalable(series) ) { + // use old maximum to get a scale conversion + var scaleConversion = jobGraphMaxima[series]/datum; + // set new maximum and rescale the series + jobGraphMaxima[series] = datum; + rescaleJobGraphSeries( series, scaleConversion ); + } + // and special calculation for cpus + if( /^cpu-/.test(series) ) { + // divide the stat by the number of cpus + var cpuCountMatch = match[6].match(/(\d+) cpus/); + if( cpuCountMatch ) { + datum = datum / cpuCountMatch[1]; + } + } + } + // scale + var scaledDatum = null; + if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) { + scaledDatum = datum/jobGraphMaxima[series] + } else { + scaledDatum = datum; + } + // identify x axis point, searching from the end of the array (most recent) + var found = false; + for( var i = jobGraphData.length - 1; i >= 0; i-- ) { + if( jobGraphData[i]['t'] === timestamp ) { + found = true; + jobGraphData[i][series] = scaledDatum; + jobGraphData[i]['raw-'+series] = match[7]; + break; + } else if( jobGraphData[i]['t'] < timestamp ) { + // we've gone far enough back in time and this data is supposed to be sorted + break; + } + } + // index counter from previous loop will have gone one too far, so add one + var insertAt = i+1; + if(!found) { + // create a new x point for this previously unrecorded timestamp + var entry = { 't': timestamp }; + entry[series] = scaledDatum; + entry['raw-'+series] = match[7]; + jobGraphData.splice( insertAt, 0, entry ); + var shifted = []; + // now let's see about "scrolling" the graph, dropping entries that are too old (>10 minutes) + while( jobGraphData.length > 0 + && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 10*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) { + shifted.push(jobGraphData.shift()); + } + if( shifted.length > 0 ) { + // from those that we dropped, are any of them maxima? if so we need to rescale + jobGraphSeries.forEach( function(series) { + // test that every shifted entry in this series was either not a number (in which case we don't care) + // or else approximately (to 2 decimal places) smaller than the scaled maximum (i.e. 1), + // because otherwise we just scrolled off something that was a maximum point + // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series + if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null + && !shifted.every( function(e) { return( !$.isNumeric(e[series]) || e[series].toFixed(2) < 1.0 ) } ) ) { + // check the remaining displayed points and find the new (scaled) maximum + var seriesMax = null; + jobGraphData.forEach( function(entry) { + if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) { + seriesMax = entry[series]; + } + }); + if( seriesMax !== null && seriesMax !== 0 ) { + // set new actual maximum using the new maximum as the conversion conversion and rescale the series + jobGraphMaxima[series] *= seriesMax; + var scaleConversion = 1/seriesMax; + rescaleJobGraphSeries( series, scaleConversion ); + } + else { + // we no longer have any data points displaying for this series + jobGraphMaxima[series] = null; + } + } + }); + } + // add a 10 minute old null data point to keep the chart honest if the oldest point is less than 9.5 minutes old + if( jobGraphData.length > 0 + && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 9.5*60000 > Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) { + var tenMinutesBefore = (new Date(Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf() - 600*1000)).toISOString().replace('Z',''); + jobGraphData.unshift( { 't': tenMinutesBefore } ); + } + } + window.redraw = true; } - // TODO: why 4? what if the data is smaller than 0.0001? - var datum = (dsum/dt).toFixed(4); - var preamble = match[1].trim().split(' '); - var timestamp = preamble[0].replace('_','T'); - var xpoints = $.grep( jobGraphData, function(e){ return e['t'] === timestamp; }); - if(xpoints.length) { - // xpoints[0] is the x point that that matched the timestamp and so already existed: add the new datum - xpoints[0][series] = datum; - } else { - var entry = { 't': timestamp }; - entry[series] = datum; - jobGraphData.push( entry ); + } catch( err ) { + console.log( 'Ignoring error trying to process log line: ' + err); + } +} + +function createJobGraph(elementName) { + delete jobGraph; + var emptyGraph = false; + if( jobGraphData.length === 0 ) { + // If there is no data we still want to show an empty graph, + // so add an empty datum and placeholder series to fool it into displaying itself. + // Note that when finally a new series is added, the graph will be recreated anyway. + jobGraphData.push( {} ); + jobGraphSeries.push( '' ); + emptyGraph = true; + } + var graphteristics = { + element: elementName, + data: jobGraphData, + ymax: 1.0, + yLabelFormat: function () { return ''; }, + xkey: 't', + ykeys: jobGraphSeries, + labels: jobGraphSeries, + resize: true, + hideHover: 'auto', + parseTime: true, + hoverCallback: function(index, options, content) { + var s = "
"; + s += options.data[index][options.xkey]; + s += "
"; + for( i = 0; i < options.ykeys.length; i++ ) { + var series = options.ykeys[i]; + var datum = options.data[index][series]; + s += "
"; + s += options.labels[i]; + s += ": "; + if ( !(typeof datum === 'undefined') ) { + if( isJobSeriesRescalable( series ) ) { + datum *= jobGraphMaxima[series]; + } + if( parseFloat(datum) !== 0 ) { + if( /^cpu-/.test(series) ){ + datum = $.number(datum * 100, 1) + '%'; + } else if( datum < 10 ) { + datum = $.number(datum, 2); + } else { + datum = $.number(datum); + } + datum += ' (' + options.data[index]['raw-'+series] + ')'; + } + s += datum; + } else { + s += '-'; + } + s += "
"; + } + return s; } } + if( emptyGraph ) { + graphteristics['axes'] = false; + graphteristics['parseTime'] = false; + graphteristics['hideHover'] = 'always'; + } + window.jobGraph = Morris.Line( graphteristics ); + if( emptyGraph ) { + jobGraphData = []; + jobGraphSeries = []; + } +} + +function rescaleJobGraphSeries( series, scaleConversion ) { + if( isJobSeriesRescalable() ) { + $.each( jobGraphData, function( i, entry ) { + if( entry[series] !== null && entry[series] !== undefined ) { + entry[series] *= scaleConversion; + } + }); + } +} + +// that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway +function isJobSeriesRescalable( series ) { + return !/^cpu-/.test(series); } $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) { if( eventData.properties.text ) { - var series_length = jobGraphSeries.length; processLogLineForChart( eventData.properties.text ); - if( series_length < jobGraphSeries.length) { + } +} ); + +$(document).on('ready', function(){ + window.recreate = false; + window.redraw = false; + setInterval( function() { + if( recreate ) { + window.recreate = false; + window.redraw = false; // series have changed, draw entirely new graph $('#log_graph_div').html(''); - window.jobGraph = Morris.Line({ - element: 'log_graph_div', - data: jobGraphData, - xkey: 't', - ykeys: jobGraphSeries, - labels: jobGraphSeries - }); - } else { + createJobGraph('log_graph_div'); + } else if( redraw ) { + window.redraw = false; jobGraph.setData( jobGraphData ); } - } - -} ); \ No newline at end of file + }, 5000); +});