Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
[arvados.git] / apps / workbench / app / assets / javascripts / event_log.js
1 /*
2  * This js establishes a websockets connection with the API Server.
3  */
4
5 /* Subscribe to websockets event log.  Do nothing if already connected. */
6 function subscribeToEventLog () {
7     // if websockets are not supported by browser, do not subscribe for events
8     websocketsSupported = ('WebSocket' in window);
9     if (websocketsSupported == false) {
10         return;
11     }
12
13     // check if websocket connection is already stored on the window
14     event_log_disp = $(window).data("arv-websocket");
15     if (event_log_disp == null) {
16         // need to create new websocket and event log dispatcher
17         websocket_url = $('meta[name=arv-websocket-url]').attr("content");
18         if (websocket_url == null)
19             return;
20
21         event_log_disp = new WebSocket(websocket_url);
22
23         event_log_disp.onopen = onEventLogDispatcherOpen;
24         event_log_disp.onmessage = onEventLogDispatcherMessage;
25
26         // store websocket in window to allow reuse when multiple divs subscribe for events
27         $(window).data("arv-websocket", event_log_disp);
28     }
29 }
30
31 /* Send subscribe message to the websockets server.  Without any filters
32    arguments, this subscribes to all events */
33 function onEventLogDispatcherOpen(event) {
34     this.send('{"method":"subscribe"}');
35 }
36
37 /* Trigger event for all applicable elements waiting for this event */
38 function onEventLogDispatcherMessage(event) {
39     parsedData = JSON.parse(event.data);
40     object_uuid = parsedData.object_uuid;
41
42     if (!object_uuid) {
43         return;
44     }
45
46     // if there are any listeners for this object uuid or "all", trigger the event
47     matches = ".arv-log-event-listener[data-object-uuid=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuids~=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuid=\"all\"],.arv-log-event-listener[data-object-kind=\"" + parsedData.object_kind + "\"]";
48     $(matches).trigger('arv-log-event', parsedData);
49 }
50
51 /* Automatically connect if there are any elements on the page that want to
52    receive event log events. */
53 $(document).on('ajax:complete ready', function() {
54     var a = $('.arv-log-event-listener');
55     if (a.length > 0) {
56         subscribeToEventLog();
57     }
58 });
59
60 /* Assumes existence of:
61   window.jobGraphData = [];
62   window.jobGraphSeries = [];
63   window.jobGraphSortedSeries = [];
64   window.jobGraphMaxima = {};
65  */
66 function processLogLineForChart( logLine ) {
67     try {
68         var match = logLine.match(/^(\S+) (\S+) (\S+) (\S+) stderr crunchstat: (\S+) (.*)/);
69         if( !match ) {
70             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+) (.*)/);
71             if( match ) {
72                 match[1] = (new Date(match[1] + ' UTC')).toISOString().replace('Z','');
73             }
74         }
75         if( match ) {
76             var rawDetailData = '';
77             var datum = null;
78
79             // the timestamp comes first
80             var timestamp = match[1].replace('_','T') + 'Z';
81
82             // we are interested in "-- interval" recordings
83             var intervalMatch = match[6].match(/(.*) -- interval (.*)/);
84             if( intervalMatch ) {
85                 var intervalData = intervalMatch[2].trim().split(' ');
86                 var dt = parseFloat(intervalData[0]);
87                 var dsum = 0.0;
88                 for(var i=2; i < intervalData.length; i += 2 ) {
89                     dsum += parseFloat(intervalData[i]);
90                 }
91                 datum = dsum/dt;
92
93                 if( datum < 0 ) {
94                     // not interested in negative deltas
95                     return;
96                 }
97
98                 rawDetailData = intervalMatch[2];
99
100                 // for the series name use the task number (4th term) and then the first word after 'crunchstat:'
101                 var series = 'T' + match[4] + '-' + match[5];
102
103                 // special calculation for cpus
104                 if( /-cpu$/.test(series) ) {
105                     // divide the stat by the number of cpus unless the time count is less than the interval length
106                     if( dsum.toFixed(1) > dt.toFixed(1) ) {
107                         var cpuCountMatch = intervalMatch[1].match(/(\d+) cpus/);
108                         if( cpuCountMatch ) {
109                             datum = datum / cpuCountMatch[1];
110                         }
111                     }
112                 }
113
114                 addJobGraphDatum( timestamp, datum, series, rawDetailData );
115             } else {
116                 // we are also interested in memory ("mem") recordings
117                 var memoryMatch = match[6].match(/(\d+) cache (\d+) swap (\d+) pgmajfault (\d+) rss/);
118                 if( memoryMatch ) {
119                     rawDetailData = match[6];
120                     // one datapoint for rss and one for swap - only show the rawDetailData for rss
121                     addJobGraphDatum( timestamp, parseInt(memoryMatch[4]), 'T' + match[4] + "-rss", rawDetailData );
122                     addJobGraphDatum( timestamp, parseInt(memoryMatch[2]), 'T' + match[4] + "-swap", '' );
123                 } else {
124                     // not interested
125                     return;
126                 }
127             }
128
129             window.redraw = true;
130         }
131     } catch( err ) {
132         console.log( 'Ignoring error trying to process log line: ' + err);
133     }
134 }
135
136 function addJobGraphDatum(timestamp, datum, series, rawDetailData) {
137     // check for new series
138     if( $.inArray( series, jobGraphSeries ) < 0 ) {
139         var newIndex = jobGraphSeries.push(series) - 1;
140         jobGraphSortedSeries.push(newIndex);
141         jobGraphSortedSeries.sort( function(a,b) {
142             var matchA = jobGraphSeries[a].match(/^T(\d+)-(.*)/);
143             var matchB = jobGraphSeries[b].match(/^T(\d+)-(.*)/);
144             var termA = ('000000' + matchA[1]).slice(-6) + matchA[2];
145             var termB = ('000000' + matchB[1]).slice(-6) + matchB[2];
146             return termA > termB ? 1 : -1;
147         });
148         jobGraphMaxima[series] = null;
149         window.recreate = true;
150     }
151
152     if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) {
153         if( isJobSeriesRescalable(series) ) {
154             // use old maximum to get a scale conversion
155             var scaleConversion = jobGraphMaxima[series]/datum;
156             // set new maximum and rescale the series
157             jobGraphMaxima[series] = datum;
158             rescaleJobGraphSeries( series, scaleConversion );
159         }
160     }
161
162     // scale
163     var scaledDatum = null;
164     if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) {
165         scaledDatum = datum/jobGraphMaxima[series]
166     } else {
167         scaledDatum = datum;
168     }
169     // identify x axis point, searching from the end of the array (most recent)
170     var found = false;
171     for( var i = jobGraphData.length - 1; i >= 0; i-- ) {
172         if( jobGraphData[i]['t'] === timestamp ) {
173             found = true;
174             jobGraphData[i][series] = scaledDatum;
175             jobGraphData[i]['raw-'+series] = rawDetailData;
176             break;
177         } else if( jobGraphData[i]['t'] < timestamp  ) {
178             // we've gone far enough back in time and this data is supposed to be sorted
179             break;
180         }
181     }
182     // index counter from previous loop will have gone one too far, so add one
183     var insertAt = i+1;
184     if(!found) {
185         // create a new x point for this previously unrecorded timestamp
186         var entry = { 't': timestamp };
187         entry[series] = scaledDatum;
188         entry['raw-'+series] = rawDetailData;
189         jobGraphData.splice( insertAt, 0, entry );
190         var shifted = [];
191         // now let's see about "scrolling" the graph, dropping entries that are too old (>10 minutes)
192         while( jobGraphData.length > 0
193                  && (Date.parse( jobGraphData[0]['t'] ) + 10*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] )) ) {
194             shifted.push(jobGraphData.shift());
195         }
196         if( shifted.length > 0 ) {
197             // from those that we dropped, were any of them maxima? if so we need to rescale
198             jobGraphSeries.forEach( function(series) {
199                 // test that every shifted entry in this series was either not a number (in which case we don't care)
200                 // or else approximately (to 2 decimal places) smaller than the scaled maximum (i.e. 1),
201                 // because otherwise we just scrolled off something that was a maximum point
202                 // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series
203                 if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null
204                       && !shifted.every( function(e) { return( !$.isNumeric(e[series]) || e[series].toFixed(2) < 1.0 ) } ) ) {
205                     // check the remaining displayed points and find the new (scaled) maximum
206                     var seriesMax = null;
207                     jobGraphData.forEach( function(entry) {
208                         if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) {
209                             seriesMax = entry[series];
210                         }
211                     });
212                     if( seriesMax !== null && seriesMax !== 0 ) {
213                         // set new actual maximum using the new maximum as the conversion conversion and rescale the series
214                         jobGraphMaxima[series] *= seriesMax;
215                         var scaleConversion = 1/seriesMax;
216                         rescaleJobGraphSeries( series, scaleConversion );
217                     }
218                     else {
219                         // we no longer have any data points displaying for this series
220                         jobGraphMaxima[series] = null;
221                     }
222                 }
223             });
224         }
225         // add a 10 minute old null data point to keep the chart honest if the oldest point is less than 9.9 minutes old
226         if( jobGraphData.length > 0 ) {
227             var earliestTimestamp = jobGraphData[0]['t'];
228             var mostRecentTimestamp = jobGraphData[jobGraphData.length-1]['t'];
229             if( (Date.parse( earliestTimestamp ) + 9.9*60000 > Date.parse( mostRecentTimestamp )) ) {
230                 var tenMinutesBefore = (new Date(Date.parse( mostRecentTimestamp ) - 600*1000)).toISOString();
231                 jobGraphData.unshift( { 't': tenMinutesBefore } );
232             }
233         }
234     }
235
236 }
237
238 function createJobGraph(elementName) {
239     delete jobGraph;
240     var emptyGraph = false;
241     if( jobGraphData.length === 0 ) {
242         // If there is no data we still want to show an empty graph,
243         // so add an empty datum and placeholder series to fool it into displaying itself.
244         // Note that when finally a new series is added, the graph will be recreated anyway.
245         jobGraphData.push( {} );
246         jobGraphSeries.push( '' );
247         emptyGraph = true;
248     }
249     var graphteristics = {
250         element: elementName,
251         data: jobGraphData,
252         ymax: 1.0,
253         yLabelFormat: function () { return ''; },
254         xkey: 't',
255         ykeys: jobGraphSeries,
256         labels: jobGraphSeries,
257         resize: true,
258         hideHover: 'auto',
259         parseTime: true,
260         hoverCallback: function(index, options, content) {
261             var s = "<div class='morris-hover-row-label'>";
262             s += options.data[index][options.xkey];
263             s += "</div> ";
264             for( i = 0; i < jobGraphSortedSeries.length; i++ ) {
265                 var sortedIndex = jobGraphSortedSeries[i];
266                 var series = options.ykeys[sortedIndex];
267                 var datum = options.data[index][series];
268                 var point = ''
269                 point += "<div class='morris-hover-point' style='color: ";
270                 point += options.lineColors[sortedIndex % options.lineColors.length];
271                 point += "'>";
272                 var labelMatch = options.labels[sortedIndex].match(/^T(\d+)-(.*)/);
273                 point += 'Task ' + labelMatch[1] + ' ' + labelMatch[2];
274                 point += ": ";
275                 if ( datum !== undefined ) {
276                     if( isJobSeriesRescalable( series ) ) {
277                         datum *= jobGraphMaxima[series];
278                     }
279                     if( parseFloat(datum) !== 0 ) {
280                         if( /-cpu$/.test(series) ){
281                             datum = $.number(datum * 100, 1) + '%';
282                         } else if( datum < 10 ) {
283                             datum = $.number(datum, 2);
284                         } else {
285                             datum = $.number(datum);
286                         }
287                         if(options.data[index]['raw-'+series]) {
288                             datum += ' (' + options.data[index]['raw-'+series] + ')';
289                         }
290                     }
291                     point += datum;
292                 } else {
293                     continue;
294                 }
295                 point += "</div> ";
296                 s += point;
297             }
298             return s;
299         }
300     }
301     if( emptyGraph ) {
302         graphteristics['axes'] = false;
303         graphteristics['parseTime'] = false;
304         graphteristics['hideHover'] = 'always';
305     }
306     window.jobGraph = Morris.Line( graphteristics );
307     if( emptyGraph ) {
308         jobGraphData = [];
309         jobGraphSeries = [];
310     }
311 }
312
313 function rescaleJobGraphSeries( series, scaleConversion ) {
314     if( isJobSeriesRescalable() ) {
315         $.each( jobGraphData, function( i, entry ) {
316             if( entry[series] !== null && entry[series] !== undefined ) {
317                 entry[series] *= scaleConversion;
318             }
319         });
320     }
321 }
322
323 // that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway
324 function isJobSeriesRescalable( series ) {
325     return !/-cpu$/.test(series);
326 }
327
328 $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) {
329     if( eventData.properties.text ) {
330         eventData.properties.text.split('\n').forEach( function( logLine ) {
331             processLogLineForChart( logLine );
332         } );
333     }
334 } );
335
336 $(document).on('ready ajax:complete', function() {
337     $('#log_graph_div').not('.graph-is-setup').addClass('graph-is-setup').each( function( index, graph_div ) {
338         window.jobGraphData = [];
339         window.jobGraphSeries = [];
340         window.jobGraphSortedSeries = [];
341         window.jobGraphMaxima = {};
342         window.recreate = false;
343         window.redraw = false;
344
345         createJobGraph($(graph_div).attr('id'));
346         var object_uuid = $(graph_div).data('object-uuid');
347         // if there are any listeners for this object uuid or "all", we will trigger the event
348         var matches = ".arv-log-event-listener[data-object-uuid=\"" + object_uuid + "\"],.arv-log-event-listener[data-object-uuids~=\"" + object_uuid + "\"]";
349
350         $(document).trigger('ajax:send');
351         $.get('/jobs/' + $(graph_div).data('object-uuid') + '/logs.json', function(data) {
352             data.forEach( function( entry ) {
353                 $(matches).trigger('arv-log-event', entry);
354             });
355         });
356
357         setInterval( function() {
358             if( recreate ) {
359                 window.recreate = false;
360                 window.redraw = false;
361                 // series have changed, draw entirely new graph
362                 $(graph_div).html('');
363                 createJobGraph($(graph_div).attr('id'));
364             } else if( redraw ) {
365                 window.redraw = false;
366                 jobGraph.setData( jobGraphData );
367             }
368         }, 5000);
369     });
370 });