542bb56d68211a8ec30171d88afd87ca1c42845a
[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+) (.*) -- interval (.*)/);
69         if( match ) {
70             // the timestamp comes first
71             var timestamp = match[1].replace('_','T');
72             // for the series use the task number (4th term) and then the first word after 'crunchstat:'
73             var series = 'T' + match[4] + '-' + match[5];
74             if( $.inArray( series, jobGraphSeries) < 0 ) {
75                 var newIndex = jobGraphSeries.push(series) - 1;
76                 jobGraphSortedSeries.push(newIndex);
77                 jobGraphSortedSeries.sort( function(a,b) {
78                     var matchA = jobGraphSeries[a].match(/^T(\d+)-(.*)/);
79                     var matchB = jobGraphSeries[b].match(/^T(\d+)-(.*)/);
80                     var termA = ('000000' + matchA[1]).slice(-6) + matchA[2];
81                     var termB = ('000000' + matchB[1]).slice(-6) + matchB[2];
82                     return termA > termB;
83                 });
84                 jobGraphMaxima[series] = null;
85                 window.recreate = true;
86             }
87             var intervalData = match[7].trim().split(' ');
88             var dt = parseFloat(intervalData[0]);
89             var dsum = 0.0;
90             for(var i=2; i < intervalData.length; i += 2 ) {
91                 dsum += parseFloat(intervalData[i]);
92             }
93             var datum = dsum/dt;
94             if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) {
95                 if( isJobSeriesRescalable(series) ) {
96                     // use old maximum to get a scale conversion
97                     var scaleConversion = jobGraphMaxima[series]/datum;
98                     // set new maximum and rescale the series
99                     jobGraphMaxima[series] = datum;
100                     rescaleJobGraphSeries( series, scaleConversion );
101                 }
102                 // and special calculation for cpus
103                 if( /-cpu$/.test(series) ) {
104                     // divide the stat by the number of cpus
105                     var cpuCountMatch = match[6].match(/(\d+) cpus/);
106                     if( cpuCountMatch ) {
107                         datum = datum / cpuCountMatch[1];
108                     }
109                 }
110             }
111             // scale
112             var scaledDatum = null;
113             if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) {
114                 scaledDatum = datum/jobGraphMaxima[series]
115             } else {
116                 scaledDatum = datum;
117             }
118             // identify x axis point, searching from the end of the array (most recent)
119             var found = false;
120             for( var i = jobGraphData.length - 1; i >= 0; i-- ) {
121                 if( jobGraphData[i]['t'] === timestamp ) {
122                     found = true;
123                     jobGraphData[i][series] = scaledDatum;
124                     jobGraphData[i]['raw-'+series] = match[7];
125                     break;
126                 } else if( jobGraphData[i]['t'] < timestamp  ) {
127                     // we've gone far enough back in time and this data is supposed to be sorted
128                     break;
129                 }
130             }
131             // index counter from previous loop will have gone one too far, so add one
132             var insertAt = i+1;
133             if(!found) {
134                 // create a new x point for this previously unrecorded timestamp
135                 var entry = { 't': timestamp };
136                 entry[series] = scaledDatum;
137                 entry['raw-'+series] = match[7];
138                 jobGraphData.splice( insertAt, 0, entry );
139                 var shifted = [];
140                 // now let's see about "scrolling" the graph, dropping entries that are too old (>10 minutes)
141                 while( jobGraphData.length > 0
142                          && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 10*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) {
143                     shifted.push(jobGraphData.shift());
144                 }
145                 if( shifted.length > 0 ) {
146                     // from those that we dropped, are any of them maxima? if so we need to rescale
147                     jobGraphSeries.forEach( function(series) {
148                         // test that every shifted entry in this series was either not a number (in which case we don't care)
149                         // or else approximately (to 2 decimal places) smaller than the scaled maximum (i.e. 1),
150                         // because otherwise we just scrolled off something that was a maximum point
151                         // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series
152                         if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null
153                               && !shifted.every( function(e) { return( !$.isNumeric(e[series]) || e[series].toFixed(2) < 1.0 ) } ) ) {
154                             // check the remaining displayed points and find the new (scaled) maximum
155                             var seriesMax = null;
156                             jobGraphData.forEach( function(entry) {
157                                 if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) {
158                                     seriesMax = entry[series];
159                                 }
160                             });
161                             if( seriesMax !== null && seriesMax !== 0 ) {
162                                 // set new actual maximum using the new maximum as the conversion conversion and rescale the series
163                                 jobGraphMaxima[series] *= seriesMax;
164                                 var scaleConversion = 1/seriesMax;
165                                 rescaleJobGraphSeries( series, scaleConversion );
166                             }
167                             else {
168                                 // we no longer have any data points displaying for this series
169                                 jobGraphMaxima[series] = null;
170                             }
171                         }
172                     });
173                 }
174                 // add a 10 minute old null data point to keep the chart honest if the oldest point is less than 9.5 minutes old
175                 if( jobGraphData.length > 0
176                       && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 9.5*60000 > Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) {
177                     var tenMinutesBefore = (new Date(Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf() - 600*1000)).toISOString().replace('Z','');
178                     jobGraphData.unshift( { 't': tenMinutesBefore } );
179                 }
180             }
181             window.redraw = true;
182         }
183     } catch( err ) {
184         console.log( 'Ignoring error trying to process log line: ' + err);
185     }
186 }
187
188 function createJobGraph(elementName) {
189     delete jobGraph;
190     var emptyGraph = false;
191     if( jobGraphData.length === 0 ) {
192         // If there is no data we still want to show an empty graph,
193         // so add an empty datum and placeholder series to fool it into displaying itself.
194         // Note that when finally a new series is added, the graph will be recreated anyway.
195         jobGraphData.push( {} );
196         jobGraphSeries.push( '' );
197         emptyGraph = true;
198     }
199     var graphteristics = {
200         element: elementName,
201         data: jobGraphData,
202         ymax: 1.0,
203         yLabelFormat: function () { return ''; },
204         xkey: 't',
205         ykeys: jobGraphSeries,
206         labels: jobGraphSeries,
207         resize: true,
208         hideHover: 'auto',
209         parseTime: true,
210         hoverCallback: function(index, options, content) {
211             var s = "<div class='morris-hover-row-label'>";
212             s += options.data[index][options.xkey];
213             s += "</div> ";
214             for( i = 0; i < jobGraphSortedSeries.length; i++ ) {
215                 var sortedIndex = jobGraphSortedSeries[i];
216                 var series = options.ykeys[sortedIndex];
217                 var datum = options.data[index][series];
218                 s += "<div class='morris-hover-point' style='color: ";
219                 s += options.lineColors[sortedIndex];
220                 s += "'>";
221                 var labelMatch = options.labels[sortedIndex].match(/^T(\d+)-(.*)/);
222                 s += 'Task ' + labelMatch[1] + ' ' + labelMatch[2];
223                 s += ": ";
224                 if ( !(typeof datum === 'undefined') ) {
225                     if( isJobSeriesRescalable( series ) ) {
226                         datum *= jobGraphMaxima[series];
227                     }
228                     if( parseFloat(datum) !== 0 ) {
229                         if( /-cpu$/.test(series) ){
230                             datum = $.number(datum * 100, 1) + '%';
231                         } else if( datum < 10 ) {
232                             datum = $.number(datum, 2);
233                         } else {
234                             datum = $.number(datum);
235                         }
236                         datum += ' (' + options.data[index]['raw-'+series] + ')';
237                     }
238                     s += datum;
239                 } else {
240                     s += '-';
241                 }
242                 s += "</div> ";
243             }
244             return s;
245         }
246     }
247     if( emptyGraph ) {
248         graphteristics['axes'] = false;
249         graphteristics['parseTime'] = false;
250         graphteristics['hideHover'] = 'always';
251     }
252     window.jobGraph = Morris.Line( graphteristics );
253     if( emptyGraph ) {
254         jobGraphData = [];
255         jobGraphSeries = [];
256     }
257 }
258
259 function rescaleJobGraphSeries( series, scaleConversion ) {
260     if( isJobSeriesRescalable() ) {
261         $.each( jobGraphData, function( i, entry ) {
262             if( entry[series] !== null && entry[series] !== undefined ) {
263                 entry[series] *= scaleConversion;
264             }
265         });
266     }
267 }
268
269 // that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway
270 function isJobSeriesRescalable( series ) {
271     return !/-cpu$/.test(series);
272 }
273
274 $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) {
275     if( eventData.properties.text ) {
276         processLogLineForChart( eventData.properties.text );
277     }
278 } );
279
280 $(document).on('ready', function(){
281     window.recreate = false;
282     window.redraw = false;
283     setInterval( function() {
284         if( recreate ) {
285             window.recreate = false;
286             window.redraw = false;
287             // series have changed, draw entirely new graph
288             $('#log_graph_div').html('');
289             createJobGraph('log_graph_div');
290         } else if( redraw ) {
291             window.redraw = false;
292             jobGraph.setData( jobGraphData );
293         }
294     }, 5000);
295 });