4233: only redraw graph a maximum of once every 5 seconds
[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     var recreate = false;
67     var rescale = false;
68     // TODO: make this more robust: anything could go wrong in here
69     var match = logLine.match(/(.*)crunchstat:(.*)-- interval(.*)/);
70     if( match ) {
71         var series = match[2].trim().split(' ')[0];
72         if( $.inArray( series, jobGraphSeries) < 0 ) {
73             jobGraphSeries.push(series);
74             jobGraphMaxima[series] = null;
75             recreate = true;
76         }
77         var intervalData = match[3].trim().split(' ');
78         var dt = parseFloat(intervalData[0]);
79         var dsum = 0.0;
80         for(var i=2; i < intervalData.length; i += 2 ) {
81             dsum += parseFloat(intervalData[i]);
82         }
83         var datum = dsum/dt;
84         if( datum !== 0 && ( jobGraphMaxima[series] === null || jobGraphMaxima[series] < datum ) ) {
85             // use old maximum to get a scale conversion
86             var scaleConversion = jobGraphMaxima[series]/datum;
87             // set new maximum and rescale the series
88             jobGraphMaxima[series] = datum;
89             rescaleJobGraphSeries( series, scaleConversion );
90         }
91         // scale
92         // FIXME: what about negative numbers?
93         var scaledDatum = null;
94         if( jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) {
95             scaledDatum = datum/jobGraphMaxima[series]
96         } else {
97             scaledDatum = datum;
98         }
99         // more parsing
100         var preamble = match[1].trim().split(' ');
101         var timestamp = preamble[0].replace('_','T');
102         // identify x axis point
103         var found = false;
104         for( var i = jobGraphData.length - 1; i >= 0; i-- ) {
105             if( jobGraphData[i]['t'] === timestamp ) {
106                 found = true;
107                 jobGraphData[i][series] = scaledDatum;
108                 break;
109             } else if( jobGraphData[i]['t'] < timestamp  ) {
110                 // we've gone far enough back in time and this data is supposed to be sorted
111                 break;
112             }
113         }
114         // index counter from previous loop will have gone one too far, so add one
115         var insertAt = i+1;
116         if(!found) {
117             // create a new x point for this previously unrecorded timestamp
118             var entry = { 't': timestamp };
119             entry[series] = scaledDatum;
120             jobGraphData.splice( insertAt, 0, entry );
121             var shifted = [];
122             // now let's see about "scrolling" the graph, dropping entries that are too old (>3 minutes)
123             while( jobGraphData.length > 0
124                      && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 3*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) {
125                 shifted.push(jobGraphData.shift());
126             }
127             if( shifted.length > 0 ) {
128                 // from those that we dropped, are any of them maxima? if so we need to rescale
129                 jobGraphSeries.forEach( function(series) {
130                     // test that every shifted entry in this series was either not a number (in which case we don't care)
131                     // or else smaller than the scaled maximum (i.e. 1), because otherwise we just scrolled off something that was a maximum point
132                     // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series
133                     if( jobGraphMaxima[series] !== null
134                           && !shifted.every( function(e) { return( !($.isNumeric(e[series])) || e[series] < 1 ) } ) ) {
135                         // check the remaining displayed points and find the new (scaled) maximum
136                         var seriesMax = null;
137                         jobGraphData.forEach( function(entry) {
138                             if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) {
139                                 seriesMax = entry[series];
140                             }
141                         });
142                         if( seriesMax !== null && seriesMax !== 0 ) {
143                             // set new actual maximum using the new maximum as the conversion conversion and rescale the series
144                             jobGraphMaxima[series] *= seriesMax;
145                             var scaleConversion = 1/seriesMax;
146                             rescaleJobGraphSeries( series, scaleConversion );
147                         }
148                         else {
149                             // we no longer have any data points displaying for this series
150                             jobGraphMaxima[series] = null;
151                         }
152                     }
153                 });
154             }
155         }
156     }
157     return recreate;
158 }
159
160 function rescaleJobGraphSeries( series, scaleConversion ) {
161     $.each( jobGraphData, function( i, entry ) {
162         if( entry[series] !== null && entry[series] !== undefined ) {
163             entry[series] *= scaleConversion;
164         }
165     });
166 }
167
168 $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) {
169     if( eventData.properties.text ) {
170         var causeRecreate = processLogLineForChart( eventData.properties.text );
171         if( causeRecreate && !window.recreate ) {
172             window.recreate = true;
173         } else {
174             window.redraw = true;
175         }
176     }
177 } );
178
179 $(document).on('ready', function(){
180     window.recreate = false;
181     window.redraw = false;
182     setInterval( function() {
183         if( recreate ) {
184             window.recreate = false;
185             // series have changed, draw entirely new graph
186             $('#log_graph_div').html('');
187             window.jobGraph = Morris.Line({
188                 element: 'log_graph_div',
189                 data: jobGraphData,
190                 xkey: 't',
191                 ykeys: jobGraphSeries,
192                 labels: jobGraphSeries
193             });
194         } else if( redraw ) {
195             window.redraw = false;
196             jobGraph.setData( jobGraphData );
197         }
198     }, 5000);
199 });