4233: do not scale the 'cpu' series stats, and divide them by cpu count
[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             if( isJobSeriesRescalable(series) ) {
86                 // use old maximum to get a scale conversion
87                 var scaleConversion = jobGraphMaxima[series]/datum;
88                 // set new maximum and rescale the series
89                 jobGraphMaxima[series] = datum;
90                 rescaleJobGraphSeries( series, scaleConversion );
91             }
92             // and special calculation for cpus
93             if( series === 'cpu' ) {
94                 var cpuCountMatch = match[2].match(/(\d+) cpus/);
95                 if( cpuCountMatch ) {
96                     datum = datum / cpuCountMatch[1];
97                 }
98             }
99         }
100         // scale
101         // FIXME: what about negative numbers?
102         var scaledDatum = null;
103         if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null && jobGraphMaxima[series] !== 0 ) {
104             scaledDatum = datum/jobGraphMaxima[series]
105         } else {
106             scaledDatum = datum;
107         }
108         // more parsing
109         var preamble = match[1].trim().split(' ');
110         var timestamp = preamble[0].replace('_','T');
111         // identify x axis point
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                 break;
118             } else if( jobGraphData[i]['t'] < timestamp  ) {
119                 // we've gone far enough back in time and this data is supposed to be sorted
120                 break;
121             }
122         }
123         // index counter from previous loop will have gone one too far, so add one
124         var insertAt = i+1;
125         if(!found) {
126             // create a new x point for this previously unrecorded timestamp
127             var entry = { 't': timestamp };
128             entry[series] = scaledDatum;
129             jobGraphData.splice( insertAt, 0, entry );
130             var shifted = [];
131             // now let's see about "scrolling" the graph, dropping entries that are too old (>3 minutes)
132             while( jobGraphData.length > 0
133                      && (Date.parse( jobGraphData[0]['t'] ).valueOf() + 3*60000 < Date.parse( jobGraphData[jobGraphData.length-1]['t'] ).valueOf()) ) {
134                 shifted.push(jobGraphData.shift());
135             }
136             if( shifted.length > 0 ) {
137                 // from those that we dropped, are any of them maxima? if so we need to rescale
138                 jobGraphSeries.forEach( function(series) {
139                     // test that every shifted entry in this series was either not a number (in which case we don't care)
140                     // or else smaller than the scaled maximum (i.e. 1), because otherwise we just scrolled off something that was a maximum point
141                     // and so we need to recalculate a new maximum point by looking at all remaining displayed points in the series
142                     if( isJobSeriesRescalable(series) && jobGraphMaxima[series] !== null
143                           && !shifted.every( function(e) { return( !($.isNumeric(e[series])) || e[series] < 1 ) } ) ) {
144                         // check the remaining displayed points and find the new (scaled) maximum
145                         var seriesMax = null;
146                         jobGraphData.forEach( function(entry) {
147                             if( $.isNumeric(entry[series]) && (seriesMax === null || entry[series] > seriesMax)) {
148                                 seriesMax = entry[series];
149                             }
150                         });
151                         if( seriesMax !== null && seriesMax !== 0 ) {
152                             // set new actual maximum using the new maximum as the conversion conversion and rescale the series
153                             jobGraphMaxima[series] *= seriesMax;
154                             var scaleConversion = 1/seriesMax;
155                             rescaleJobGraphSeries( series, scaleConversion );
156                         }
157                         else {
158                             // we no longer have any data points displaying for this series
159                             jobGraphMaxima[series] = null;
160                         }
161                     }
162                 });
163             }
164         }
165     }
166     return recreate;
167 }
168
169 function rescaleJobGraphSeries( series, scaleConversion ) {
170     if( isJobSeriesRescalable() ) {
171         $.each( jobGraphData, function( i, entry ) {
172             if( entry[series] !== null && entry[series] !== undefined ) {
173                 entry[series] *= scaleConversion;
174             }
175         });
176     }
177 }
178
179 // that's right - we never do this for the 'cpu' series, which will always be between 0 and 1 anyway
180 function isJobSeriesRescalable( series ) {
181     return series != 'cpu';
182 }
183
184 $(document).on('arv-log-event', '#log_graph_div', function(event, eventData) {
185     if( eventData.properties.text ) {
186         var causeRecreate = processLogLineForChart( eventData.properties.text );
187         if( causeRecreate && !window.recreate ) {
188             window.recreate = true;
189         } else {
190             window.redraw = true;
191         }
192     }
193 } );
194
195 $(document).on('ready', function(){
196     window.recreate = false;
197     window.redraw = false;
198     setInterval( function() {
199         if( recreate ) {
200             window.recreate = false;
201             // series have changed, draw entirely new graph
202             $('#log_graph_div').html('');
203             window.jobGraph = Morris.Line({
204                 element: 'log_graph_div',
205                 data: jobGraphData,
206                 ymax: 1.0,
207                 xkey: 't',
208                 ykeys: jobGraphSeries,
209                 labels: jobGraphSeries
210             });
211         } else if( redraw ) {
212             window.redraw = false;
213             jobGraph.setData( jobGraphData );
214         }
215     }, 5000);
216 });