1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 /* Assumes existence of:
6 window.jobGraphData = [];
7 window.jobGraphSeries = [];
8 window.jobGraphSortedSeries = [];
9 window.jobGraphMaxima = {};
11 function processLogLineForChart( logLine ) {
13 var match = logLine.match(/^(\S+) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*)/);
15 match = logLine.match(/^((?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{1,2} \d\d:\d\d:\d\d \d{4}) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*)/);
17 match[1] = (new Date(match[1] + ' UTC')).toISOString().replace('Z','');
21 var rawDetailData = '';
24 // the timestamp comes first
25 var timestamp = match[1].replace('_','T') + 'Z';
27 // we are interested in "-- interval" recordings
28 var intervalMatch = match[6].match(/(.*) -- interval (.*)/);
30 var intervalData = intervalMatch[2].trim().split(' ');
31 var dt = parseFloat(intervalData[0]);
33 for(var i=2; i < intervalData.length; i += 2 ) {
34 dsum += parseFloat(intervalData[i]);
39 // not interested in negative deltas
43 rawDetailData = intervalMatch[2];
45 // for the series name use the task number (4th term) and then the first word after 'crunchstat:'
46 var series = 'T' + match[4] + '-' + match[5];
48 // special calculation for cpus
49 if( /-cpu$/.test(series) ) {
50 // divide the stat by the number of cpus unless the time count is less than the interval length
51 if( dsum.toFixed(1) > dt.toFixed(1) ) {
52 var cpuCountMatch = intervalMatch[1].match(/(\d+) cpus/);
54 datum = datum / cpuCountMatch[1];
59 addJobGraphDatum( timestamp, datum, series, rawDetailData );
61 // we are also interested in memory ("mem") recordings
62 var memoryMatch = match[6].match(/(\d+) cache (\d+) swap (\d+) pgmajfault (\d+) rss/);
64 rawDetailData = match[6];
65 // one datapoint for rss and one for swap - only show the rawDetailData for rss
66 addJobGraphDatum( timestamp, parseInt(memoryMatch[4]), 'T' + match[4] + "-rss", rawDetailData );
67 addJobGraphDatum( timestamp, parseInt(memoryMatch[2]), 'T' + match[4] + "-swap", '' );
77 console.log( 'Ignoring error trying to process log line: ' + err);
81 function addJobGraphDatum(timestamp, datum, series, rawDetailData) {
82 // check for new series
83 if( $.inArray( series, jobGraphSeries ) < 0 ) {
84 var newIndex = jobGraphSeries.push(series) - 1;
85 jobGraphSortedSeries.push(newIndex);
86 jobGraphSortedSeries.sort( function(a,b) {
87 var matchA = jobGraphSeries[a].match(/^T(\d+)-(.*)/);
88 var matchB = jobGraphSeries[b].match(/^T(\d+)-(.*)/);
89 var termA = ('000000' + matchA[1]).slice(-6) + matchA[2];
90 var termB = ('000000' + matchB[1]).slice(-6) + matchB[2];
91 return termA > termB ? 1 : -1;
93 jobGraphMaxima[series] = null;
94 window.recreate = true;
97 if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) {
98 if( isJobSeriesRescalable(series) ) {
99 // use old maximum to get a scale conversion
100 var scaleConversion = jobGraphMaxima[series]/datum;
101 // set new maximum and rescale the series
102 jobGraphMaxima[series] = datum;
103 rescaleJobGraphSeries( series, scaleConversion );
108 var scaledDatum = null;
109 if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) {
110 scaledDatum = datum/jobGraphMaxima[series]
114 // identify x axis point, searching from the end of the array (most recent)
116 for( var i = jobGraphData.length - 1; i >= 0; i-- ) {
117 if( jobGraphData[i]['t'] === timestamp ) {
119 jobGraphData[i][series] = scaledDatum;
120 jobGraphData[i]['raw-'+series] = rawDetailData;
122 } else if( jobGraphData[i]['t'] < timestamp ) {
123 // we've gone far enough back in time and this data is supposed to be sorted
127 // index counter from previous loop will have gone one too far, so add one
130 // create a new x point for this previously unrecorded timestamp
131 var entry = { 't': timestamp };
132 entry[series] = scaledDatum;
133 entry['raw-'+series] = rawDetailData;
134 jobGraphData.splice( insertAt, 0, entry );
136 // now let's see about "scrolling" the graph, dropping entries that are too old (>10 minutes)
137 while( jobGraphData.length > 0
138 && (Date.parse( jobGraphData[0]['t'] ) + 10*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] )) ) {
139 shifted.push(jobGraphData.shift());
141 if( shifted.length > 0 ) {
142 // from those that we dropped, were any of them maxima? if so we need to rescale
143 jobGraphSeries.forEach( function(series) {
144 // test that every shifted entry in this series was either not a number (in which case we don't care)
145 // or else approximately (to 2 decimal places) smaller than the scaled maximum (i.e. 1),
146 // because otherwise we just scrolled off something that was a maximum point
147 // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series
148 if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null
149 && !shifted.every( function(e) { return( !$.isNumeric(e[series]) || e[series].toFixed(2) < 1.0 ) } ) ) {
150 // check the remaining displayed points and find the new (scaled) maximum
151 var seriesMax = null;
152 jobGraphData.forEach( function(entry) {
153 if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) {
154 seriesMax = entry[series];
157 if( seriesMax !== null && seriesMax !== 0 ) {
158 // set new actual maximum using the new maximum as the conversion conversion and rescale the series
159 jobGraphMaxima[series] *= seriesMax;
160 var scaleConversion = 1/seriesMax;
161 rescaleJobGraphSeries( series, scaleConversion );
164 // we no longer have any data points displaying for this series
165 jobGraphMaxima[series] = null;
170 // add a 10 minute old null data point to keep the chart honest if the oldest point is less than 9.9 minutes old
171 if( jobGraphData.length > 0 ) {
172 var earliestTimestamp = jobGraphData[0]['t'];
173 var mostRecentTimestamp = jobGraphData[jobGraphData.length-1]['t'];
174 if( (Date.parse( earliestTimestamp ) + 9.9*60000 > Date.parse( mostRecentTimestamp )) ) {
175 var tenMinutesBefore = (new Date(Date.parse( mostRecentTimestamp ) - 600*1000)).toISOString();
176 jobGraphData.unshift( { 't': tenMinutesBefore } );
183 function createJobGraph(elementName) {
185 var emptyGraph = false;
186 if( jobGraphData.length === 0 ) {
187 // If there is no data we still want to show an empty graph,
188 // so add an empty datum and placeholder series to fool it
189 // into displaying itself. Note that when finally a new
190 // series is added, the graph will be recreated anyway.
191 jobGraphData.push( {} );
192 jobGraphSeries.push( '' );
195 var graphteristics = {
196 element: elementName,
199 yLabelFormat: function () { return ''; },
201 ykeys: jobGraphSeries,
202 labels: jobGraphSeries,
206 hoverCallback: function(index, options, content) {
208 for (var i=0; i < jobGraphSortedSeries.length; i++) {
209 var sortedIndex = jobGraphSortedSeries[i];
210 var series = options.ykeys[sortedIndex];
211 var datum = options.data[index][series];
213 point += "<div class='morris-hover-point' style='color: ";
214 point += options.lineColors[sortedIndex % options.lineColors.length];
216 var labelMatch = options.labels[sortedIndex].match(/^T(\d+)-(.*)/);
217 point += 'Task ' + labelMatch[1] + ' ' + labelMatch[2];
219 if ( datum !== undefined ) {
220 if( isJobSeriesRescalable( series ) ) {
221 datum *= jobGraphMaxima[series];
223 if( parseFloat(datum) !== 0 ) {
224 if( /-cpu$/.test(series) ){
225 datum = $.number(datum * 100, 1) + '%';
226 } else if( datum < 10 ) {
227 datum = $.number(datum, 2);
229 datum = $.number(datum);
231 if(options.data[index]['raw-'+series]) {
232 datum += ' (' + options.data[index]['raw-'+series] + ')';
243 // No Y coordinates? This isn't a real data point,
244 // it's just the placeholder we use to make sure the
245 // graph can render when empty. Don't show a tooltip.
248 return ("<div class='morris-hover-row-label'>" +
249 options.data[index][options.xkey] +
254 graphteristics['axes'] = false;
255 graphteristics['parseTime'] = false;
256 graphteristics['hideHover'] = 'always';
258 $('#' + elementName).html('');
259 window.jobGraph = Morris.Line( graphteristics );
266 function rescaleJobGraphSeries( series, scaleConversion ) {
267 if( isJobSeriesRescalable() ) {
268 $.each( jobGraphData, function( i, entry ) {
269 if( entry[series] !== null && entry[series] !== undefined ) {
270 entry[series] *= scaleConversion;
276 // that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway
277 function isJobSeriesRescalable( series ) {
278 return !/-cpu$/.test(series);
281 function processLogEventForGraph(event, eventData) {
282 if( eventData.properties.text ) {
283 eventData.properties.text.split('\n').forEach( function( logLine ) {
284 processLogLineForChart( logLine );
289 $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) {
290 processLogEventForGraph(event, eventData);
291 if (!window.jobGraphShown) {
292 // Draw immediately, instead of waiting for the 5-second
294 redrawIfNeeded.call(window, this);
298 function redrawIfNeeded(graph_div) {
299 if (!window.redraw) {
302 window.redraw = false;
304 if (window.recreate) {
305 // Series have changed: we need to draw an entirely new graph.
306 // Running createJobGraph in a show() callback ensures the div
307 // is fully shown when morris uses it to size its svg element.
308 $(graph_div).show(0, createJobGraph.bind(window, $(graph_div).attr('id')));
309 window.jobGraphShown = true;
310 window.recreate = false;
312 window.jobGraph.setData(window.jobGraphData);
316 $(document).on('ready ajax:complete', function() {
317 $('#log_graph_div').not('.graph-is-setup').addClass('graph-is-setup').each( function( index, graph_div ) {
318 window.jobGraphShown = false;
319 window.jobGraphData = [];
320 window.jobGraphSeries = [];
321 window.jobGraphSortedSeries = [];
322 window.jobGraphMaxima = {};
323 window.recreate = false;
324 window.redraw = false;
326 $.get('/jobs/' + $(graph_div).data('object-uuid') + '/logs.json', function(data) {
327 data.forEach( function( entry ) {
328 processLogEventForGraph({}, entry);
330 // Update the graph now to show the recent data points
331 // received via /logs.json (along with any new data points
332 // we received via websockets while waiting for /logs.json
334 redrawIfNeeded(graph_div);
337 setInterval(redrawIfNeeded.bind(window, graph_div), 5000);