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