treewide: import utility classes explicitly
[oweals/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / bandwidth.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require request';
6 'require ui';
7 'require rpc';
8 'require network';
9
10 var callLuciRealtimeStats = rpc.declare({
11         object: 'luci',
12         method: 'getRealtimeStats',
13         params: [ 'mode', 'device' ],
14         expect: { result: [] }
15 });
16
17 var graphPolls = [],
18     pollInterval = 3;
19
20 Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
21
22 function rate(n, br) {
23         n = (n || 0).toFixed(2);
24         return [ '%1024.2mbit/s'.format(n * 8), br ? E('br') : ' ', '(%1024.2mB/s)'.format(n) ]
25 }
26
27 return view.extend({
28         load: function() {
29                 return Promise.all([
30                         this.loadSVG(L.resource('bandwidth.svg')),
31                         network.getDevices()
32                 ]);
33         },
34
35         updateGraph: function(ifname, svg, lines, cb) {
36                 var G = svg.firstElementChild;
37
38                 var view = document.querySelector('#view');
39
40                 var width  = view.offsetWidth - 2;
41                 var height = 300 - 2;
42                 var step   = 5;
43
44                 var data_wanted = Math.floor(width / step);
45
46                 var data_values = [],
47                     line_elements = [];
48
49                 for (var i = 0; i < lines.length; i++)
50                         if (lines[i] != null)
51                                 data_values.push([]);
52
53                 var info = {
54                         line_current: [],
55                         line_average: [],
56                         line_peak:    []
57                 };
58
59                 /* prefill datasets */
60                 for (var i = 0; i < data_values.length; i++)
61                         for (var j = 0; j < data_wanted; j++)
62                                         data_values[i][j] = 0;
63
64                 /* plot horizontal time interval lines */
65                 for (var i = width % (step * 60); i < width; i += step * 60) {
66                         var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
67                                 line.setAttribute('x1', i);
68                                 line.setAttribute('y1', 0);
69                                 line.setAttribute('x2', i);
70                                 line.setAttribute('y2', '100%');
71                                 line.setAttribute('style', 'stroke:black;stroke-width:0.1');
72
73                         var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
74                                 text.setAttribute('x', i + 5);
75                                 text.setAttribute('y', 15);
76                                 text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
77                                 text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
78
79                         G.appendChild(line);
80                         G.appendChild(text);
81                 }
82
83                 info.interval = pollInterval;
84                 info.timeframe = data_wanted / 60;
85
86                 graphPolls.push({
87                         ifname: ifname,
88                         svg:    svg,
89                         lines:  lines,
90                         cb:     cb,
91                         info:   info,
92                         width:  width,
93                         height: height,
94                         step:   step,
95                         values: data_values,
96                         timestamp: 0,
97                         fill: 1
98                 });
99         },
100
101         pollData: function() {
102                 poll.add(L.bind(function() {
103                         var tasks = [];
104
105                         for (var i = 0; i < graphPolls.length; i++) {
106                                 var ctx = graphPolls[i];
107                                 tasks.push(L.resolveDefault(callLuciRealtimeStats('interface', ctx.ifname), []));
108                         }
109
110                         return Promise.all(tasks).then(L.bind(function(datasets) {
111                                 for (var gi = 0; gi < graphPolls.length; gi++) {
112                                         var ctx = graphPolls[gi],
113                                             data = datasets[gi],
114                                             values = ctx.values,
115                                             lines = ctx.lines,
116                                             info = ctx.info;
117
118                                         var data_scale = 0;
119                                         var data_wanted = Math.floor(ctx.width / ctx.step);
120                                         var last_timestamp = NaN;
121
122                                         for (var i = 0, di = 0; di < lines.length; di++) {
123                                                 if (lines[di] == null)
124                                                         continue;
125
126                                                 var multiply = (lines[di].multiply != null) ? lines[di].multiply : 1,
127                                                     offset = (lines[di].offset != null) ? lines[di].offset : 0;
128
129                                                 for (var j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
130                                                         /* skip overlapping entries */
131                                                         if (data[j][0] <= ctx.timestamp)
132                                                                 continue;
133
134                                                         if (i == 0) {
135                                                                 ctx.fill++;
136                                                                 last_timestamp = data[j][0];
137                                                         }
138
139                                                         if (lines[di].counter) {
140                                                                 /* normalize difference against time interval */
141                                                                 if (j > 0) {
142                                                                         var time_delta = data[j][0] - data[j - 1][0];
143                                                                         if (time_delta) {
144                                                                                 info.line_current[i] = (data[j][di + 1] * multiply - data[j - 1][di + 1] * multiply) / time_delta;
145                                                                                 info.line_current[i] -= Math.min(info.line_current[i], offset);
146                                                                                 values[i].push(info.line_current[i]);
147                                                                         }
148                                                                 }
149                                                         }
150                                                         else {
151                                                                 info.line_current[i] = data[j][di + 1] * multiply;
152                                                                 info.line_current[i] -= Math.min(info.line_current[i], offset);
153                                                                 values[i].push(info.line_current[i]);
154                                                         }
155                                                 }
156
157                                                 i++;
158                                         }
159
160                                         /* cut off outdated entries */
161                                         ctx.fill = Math.min(ctx.fill, data_wanted);
162
163                                         for (var i = 0; i < values.length; i++) {
164                                                 var len = values[i].length;
165                                                 values[i] = values[i].slice(len - data_wanted, len);
166
167                                                 /* find peaks, averages */
168                                                 info.line_peak[i] = NaN;
169                                                 info.line_average[i] = 0;
170
171                                                 for (var j = 0; j < values[i].length; j++) {
172                                                         info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
173                                                         info.line_average[i] += values[i][j];
174                                                 }
175
176                                                 info.line_average[i] = info.line_average[i] / ctx.fill;
177                                         }
178
179                                         info.peak = Math.max.apply(Math, info.line_peak);
180
181                                         /* remember current timestamp, calculate horizontal scale */
182                                         if (!isNaN(last_timestamp))
183                                                 ctx.timestamp = last_timestamp;
184
185                                         var size = Math.floor(Math.log2(info.peak)),
186                                             div = Math.pow(2, size - (size % 10)),
187                                             mult = info.peak / div,
188                                             mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
189
190                                         info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
191
192                                         data_scale = ctx.height / info.peak;
193
194                                         /* plot data */
195                                         for (var i = 0, di = 0; di < lines.length; di++) {
196                                                 if (lines[di] == null)
197                                                         continue;
198
199                                                 var el = ctx.svg.firstElementChild.getElementById(lines[di].line),
200                                                     pt = '0,' + ctx.height,
201                                                     y = 0;
202
203                                                 if (!el)
204                                                         continue;
205
206                                                 for (var j = 0; j < values[i].length; j++) {
207                                                         var x = j * ctx.step;
208
209                                                         y = ctx.height - Math.floor(values[i][j] * data_scale);
210                                                         //y -= Math.floor(y % (1 / data_scale));
211
212                                                         pt += ' ' + x + ',' + y;
213                                                 }
214
215                                                 pt += ' ' + ctx.width + ',' + y + ' ' + ctx.width + ',' + ctx.height;
216
217                                                 el.setAttribute('points', pt);
218
219                                                 i++;
220                                         }
221
222                                         info.label_25 = 0.25 * info.peak;
223                                         info.label_50 = 0.50 * info.peak;
224                                         info.label_75 = 0.75 * info.peak;
225
226                                         if (typeof(ctx.cb) == 'function')
227                                                 ctx.cb(ctx.svg, info);
228                                 }
229                         }, this));
230                 }, this), pollInterval);
231         },
232
233         loadSVG: function(src) {
234                 return request.get(src).then(function(response) {
235                         if (!response.ok)
236                                 throw new Error(response.statusText);
237
238                         return E('div', {
239                                 'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
240                         }, E(response.text()));
241                 });
242         },
243
244         render: function(data) {
245                 var svg = data[0],
246                     devs = data[1];
247
248                 var v = E('div', {}, E('div'));
249
250                 for (var i = 0; i < devs.length; i++) {
251                         var ifname = devs[i].getName();
252
253                         if (!ifname)
254                                 continue;
255
256                         var csvg = svg.cloneNode(true);
257
258                         v.firstElementChild.appendChild(E('div', { 'data-tab': ifname, 'data-tab-title': ifname }, [
259                                 csvg,
260                                 E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
261                                 E('br'),
262
263                                 E('div', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
264                                         E('div', { 'class': 'tr' }, [
265                                                 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('Inbound:') ])),
266                                                 E('div', { 'class': 'td', 'id': 'rx_bw_cur' }, rate(0, true)),
267
268                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
269                                                 E('div', { 'class': 'td', 'id': 'rx_bw_avg' }, rate(0, true)),
270
271                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
272                                                 E('div', { 'class': 'td', 'id': 'rx_bw_peak' }, rate(0, true))
273                                         ]),
274                                         E('div', { 'class': 'tr' }, [
275                                                 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('Outbound:') ])),
276                                                 E('div', { 'class': 'td', 'id': 'tx_bw_cur' }, rate(0, true)),
277
278                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
279                                                 E('div', { 'class': 'td', 'id': 'tx_bw_avg' }, rate(0, true)),
280
281                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
282                                                 E('div', { 'class': 'td', 'id': 'tx_bw_peak' }, rate(0, true))
283                                         ])
284                                 ])
285                         ]));
286
287                         this.updateGraph(ifname, csvg, [ { line: 'rx', counter: true }, null, { line: 'tx', counter: true } ], function(svg, info) {
288                                 var G = svg.firstElementChild, tab = svg.parentNode;
289
290                                 G.getElementById('label_25').firstChild.data = rate(info.label_25).join('');
291                                 G.getElementById('label_50').firstChild.data = rate(info.label_50).join('');
292                                 G.getElementById('label_75').firstChild.data = rate(info.label_75).join('');
293
294                                 tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
295
296                                 dom.content(tab.querySelector('#rx_bw_cur'), rate(info.line_current[0], true));
297                                 dom.content(tab.querySelector('#rx_bw_avg'), rate(info.line_average[0], true));
298                                 dom.content(tab.querySelector('#rx_bw_peak'), rate(info.line_peak[0], true));
299
300                                 dom.content(tab.querySelector('#tx_bw_cur'), rate(info.line_current[1], true));
301                                 dom.content(tab.querySelector('#tx_bw_avg'), rate(info.line_average[1], true));
302                                 dom.content(tab.querySelector('#tx_bw_peak'), rate(info.line_peak[1], true));
303                         });
304                 }
305
306                 ui.tabs.initTabGroup(v.firstElementChild.childNodes);
307
308                 this.pollData();
309
310                 return v;
311         },
312
313         handleSaveApply: null,
314         handleSave: null,
315         handleReset: null
316 });