Line data Source code
1 : /*
2 : * Copyright (C) 2014 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 151 : import cockpit from 'cockpit';
7 :
8 : /* The public API here is not general and a bit weird. It can only do
9 : * what Cockpit itself needs right now, and what can be easily
10 : * implemented without touching the older Metrics_series classes.
11 : *
12 : * The basic idea is that you create a global PlotState object and
13 : * keep it alive for the lifetime of the page. Then you instantiate
14 : * ZoomControl and SvgPlot components as needed.
15 : *
16 : * To control what to plot, you call some methods on the PlotState
17 : * object:
18 : *
19 : * - plot_state.plot_single(id, metric_options)
20 : *
21 : * - plot_state.plot_instances(id, metric_options, instances, reset)
22 : *
23 : * You need to figure out the rest of the details from the existing
24 : * users, unfortunately.
25 : */
26 :
27 151 : class Metrics_series {
28 150 : constructor(desc, opts, grid, plot_data, interval) {
29 150 : cockpit.event_target(this);
30 150 : this.desc = desc;
31 150 : this.options = opts;
32 150 : this.grid = grid;
33 150 : this.plot_data = plot_data;
34 150 : this.interval = interval;
35 150 : this.channel = null;
36 150 : this.chanopts_list = [];
37 150 : }
38 :
39 1 : stop() {
40 1 : if (this.channel)
41 1 : this.channel.close();
42 1 : }
43 :
44 1 : remove_series() {
45 1 : const pos = this.plot_data.indexOf(this.options);
46 1 : if (pos >= 0)
47 0 : this.plot_data.splice(pos, 1);
48 1 : }
49 :
50 1 : remove() {
51 1 : this.stop();
52 1 : this.remove_series();
53 1 : this.dispatchEvent("removed");
54 1 : }
55 :
56 150 : build_metric(n) {
57 150 : return { name: n, units: this.desc.units, derive: this.desc.derive };
58 150 : }
59 :
60 150 : check_archives() {
61 150 : if (this.channel.archives)
62 49 : this.dispatchEvent("changed");
63 150 : }
64 151 : }
65 :
66 151 : class Metrics_sum_series extends Metrics_series {
67 2 : constructor(desc, opts, grid, plot_data, interval) {
68 2 : super(desc, opts, grid, plot_data, interval);
69 2 : if (this.desc.direct) {
70 2 : this.chanopts_list.push({
71 2 : source: 'direct',
72 2 : archive_source: 'pcp-archive',
73 2 : metrics: this.desc.direct.map(this.build_metric, this),
74 2 : instances: this.desc.instances,
75 2 : 'omit-instances': this.desc['omit-instances'],
76 2 : host: this.desc.host
77 2 : });
78 2 : }
79 1 : if (this.desc.pmcd) {
80 1 : this.chanopts_list.push({
81 1 : source: 'pmcd',
82 1 : metrics: this.desc.pmcd.map(this.build_metric, this),
83 1 : instances: this.desc.instances,
84 1 : 'omit-instances': this.desc['omit-instances'],
85 1 : host: this.desc.host
86 1 : });
87 1 : }
88 2 : if (this.desc.internal) {
89 2 : this.chanopts_list.push({
90 2 : source: 'internal',
91 2 : metrics: this.desc.internal.map(this.build_metric, this),
92 2 : instances: this.desc.instances,
93 2 : 'omit-instances': this.desc['omit-instances'],
94 2 : host: this.desc.host
95 2 : });
96 2 : }
97 2 : }
98 :
99 2 : flat_sum(val) {
100 2 : if (!val)
101 2 : return 0;
102 2 : if (val.length !== undefined) {
103 2 : let sum = 0;
104 2 : for (let i = 0; i < val.length; i++)
105 2 : sum += this.flat_sum(val[i]);
106 2 : return sum;
107 2 : }
108 2 : return val;
109 2 : }
110 :
111 2 : reset_series() {
112 2 : if (this.channel)
113 0 : this.channel.close();
114 :
115 2 : this.channel = cockpit.metrics(this.interval, this.chanopts_list);
116 :
117 2 : const metrics_row = this.grid.add(this.channel, []);
118 2 : const factor = this.desc.factor || 1;
119 1 : const threshold = this.desc.threshold || null;
120 2 : const offset = this.desc.offset || 0;
121 2 : this.options.data = this.grid.add((row, x, n) => {
122 2 : for (let i = 0; i < n; i++) {
123 2 : const value = offset + this.flat_sum(metrics_row[x + i]) * factor;
124 2 : if (threshold !== null)
125 0 : row[x + i] = [(this.grid.beg + x + i) * this.interval, Math.abs(value) > threshold ? value : null, threshold];
126 : else
127 1 : row[x + i] = [(this.grid.beg + x + i) * this.interval, value];
128 2 : }
129 2 : });
130 :
131 2 : this.channel.addEventListener("changed", this.check_archives.bind(this));
132 2 : this.check_archives();
133 2 : }
134 151 : }
135 :
136 151 : class Metrics_stacked_instances_series extends Metrics_series {
137 149 : constructor(desc, opts, grid, plot_data, interval) {
138 149 : super(desc, opts, grid, plot_data, interval);
139 149 : this.instances = { };
140 149 : this.last_instance = null;
141 149 : if (this.desc.direct) {
142 149 : this.chanopts_list.push({
143 149 : source: 'direct',
144 149 : archive_source: 'pcp-archive',
145 149 : metrics: [this.build_metric(this.desc.direct)],
146 149 : metrics_path_names: ['a'],
147 149 : instances: this.desc.instances,
148 149 : 'omit-instances': this.desc['omit-instances'],
149 149 : host: this.desc.host
150 149 : });
151 149 : }
152 20 : if (this.desc.pmcd) {
153 20 : this.chanopts_list.push({
154 20 : source: 'pmcd',
155 20 : metrics: this.desc.pmcd.map(this.build_metric, this),
156 20 : metrics_path_names: ['a'],
157 20 : instances: this.desc.instances,
158 20 : 'omit-instances': this.desc['omit-instances'],
159 20 : host: this.desc.host
160 20 : });
161 20 : }
162 :
163 149 : if (this.desc.internal) {
164 149 : this.chanopts_list.push({
165 149 : source: 'internal',
166 149 : metrics: [this.build_metric(this.desc.internal)],
167 149 : metrics_path_names: ['a'],
168 149 : instances: this.desc.instances,
169 149 : 'omit-instances': this.desc['omit-instances'],
170 149 : host: this.desc.host
171 149 : });
172 149 : }
173 149 : }
174 :
175 149 : reset_series() {
176 149 : if (this.channel)
177 20 : this.channel.close();
178 149 : this.channel = cockpit.metrics(this.interval, this.chanopts_list);
179 149 : this.channel.addEventListener("changed", this.check_archives.bind(this));
180 149 : this.check_archives();
181 149 : for (const name in this.instances)
182 20 : this.instances[name].reset();
183 149 : }
184 :
185 149 : add_instance(name, selector) {
186 149 : if (this.instances[name])
187 149 : return;
188 :
189 149 : const instance_data = Object.assign({ selector }, this.options);
190 119 : const factor = this.desc.factor || 1;
191 47 : const threshold = this.desc.threshold || 0;
192 149 : const last = this.last_instance;
193 149 : let metrics_row;
194 :
195 149 : function reset() {
196 149 : metrics_row = this.grid.add(this.channel, ['a', name]);
197 149 : instance_data.data = this.grid.add((row, x, n) => {
198 149 : for (let i = 0; i < n; i++) {
199 149 : const value = (metrics_row[x + i] || 0) * factor;
200 149 : const ts = (this.grid.beg + x + i) * this.interval;
201 149 : let floor = 0;
202 :
203 148 : if (last) {
204 148 : if (last.data[x + i][1])
205 130 : floor = last.data[x + i][1];
206 : else
207 148 : floor = last.data[x + i][2];
208 148 : }
209 :
210 131 : if (Math.abs(value) > threshold) {
211 131 : row[x + i] = [ts, floor + value, floor];
212 131 : if (row[x + i - 1] && row[x + i - 1][1] === null)
213 131 : row[x + i - 1][1] = row[x + i - 1][2];
214 131 : } else {
215 149 : row[x + i] = [ts, null, floor];
216 149 : if (row[x + i - 1] && row[x + i - 1][1] !== null)
217 116 : row[x + i - 1][1] = row[x + i - 1][2];
218 149 : }
219 149 : }
220 149 : });
221 149 : }
222 :
223 8 : function remove() {
224 8 : this.grid.remove(metrics_row);
225 8 : this.grid.remove(instance_data.data);
226 8 : const pos = this.plot_data.indexOf(instance_data);
227 8 : if (pos >= 0)
228 8 : this.plot_data.splice(pos, 1);
229 8 : }
230 :
231 149 : instance_data.reset = reset.bind(this);
232 149 : instance_data.remove = remove.bind(this);
233 149 : instance_data.name = name;
234 149 : this.last_instance = instance_data;
235 149 : this.instances[name] = instance_data;
236 149 : instance_data.reset();
237 149 : this.plot_data.push(instance_data);
238 149 : this.grid.sync();
239 149 : }
240 :
241 8 : clear_instances() {
242 8 : for (const i in this.instances)
243 8 : this.instances[i].remove();
244 8 : this.instances = { };
245 8 : this.last_instance = null;
246 8 : }
247 151 : }
248 :
249 151 : class Plot {
250 150 : constructor(element, x_range_seconds, x_stop_seconds) {
251 150 : cockpit.event_target(this);
252 :
253 150 : this.series = [];
254 150 : this.plot_data = [];
255 :
256 150 : this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
257 150 : this.grid = null;
258 :
259 150 : this.sync_suppressed = 0;
260 150 : this.archives = false;
261 :
262 150 : this.reset(x_range_seconds, x_stop_seconds);
263 150 : }
264 :
265 150 : refresh() {
266 150 : this.dispatchEvent("plot", this.plot_data);
267 150 : }
268 :
269 150 : start_walking() {
270 150 : this.grid.walk();
271 150 : }
272 :
273 0 : stop_walking() {
274 0 : this.grid.move(this.grid.beg, this.grid.end);
275 0 : }
276 :
277 150 : reset(x_range_seconds, x_stop_seconds) {
278 : // Fill the plot with about 1000 samples, but don't sample
279 : // faster than once per second.
280 : //
281 : // TODO - do this based on the actual size of the plot.
282 150 : this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
283 :
284 150 : const x_offset = (x_stop_seconds !== undefined)
285 20 : ? (new Date().getTime()) - x_stop_seconds * 1000
286 150 : : 0;
287 :
288 150 : const beg = -Math.ceil((x_range_seconds * 1000 + x_offset) / this.interval);
289 150 : const end = -Math.floor(x_offset / this.interval);
290 :
291 20 : if (this.grid && this.grid.interval == this.interval) {
292 20 : this.grid.move(beg, end);
293 20 : } else {
294 150 : if (this.grid)
295 20 : this.grid.close();
296 150 : this.grid = cockpit.grid(this.interval, beg, end);
297 150 : this.sync_suppressed++;
298 20 : for (let i = 0; i < this.series.length; i++) {
299 20 : this.series[i].stop();
300 20 : this.series[i].interval = this.interval;
301 20 : this.series[i].grid = this.grid;
302 20 : this.series[i].reset_series();
303 20 : }
304 150 : this.sync_suppressed--;
305 150 : this.sync();
306 :
307 150 : this.grid.addEventListener("notify", (event, index, count) => {
308 150 : this.refresh();
309 150 : });
310 150 : }
311 150 : }
312 :
313 150 : sync() {
314 150 : if (this.sync_suppressed === 0)
315 150 : this.grid.sync();
316 150 : }
317 :
318 0 : destroy() {
319 0 : this.grid.close();
320 0 : for (let i = 0; i < this.series.length; i++)
321 0 : this.series[i].stop();
322 :
323 0 : this.options = { };
324 0 : this.series = [];
325 0 : this.plot_data = [];
326 0 : }
327 :
328 32 : check_archives() {
329 32 : if (!this.archives) {
330 32 : this.archives = true;
331 32 : this.dispatchEvent('changed');
332 32 : }
333 32 : }
334 :
335 2 : add_metrics_sum_series(desc, opts) {
336 2 : const sum_series = new Metrics_sum_series(desc, opts, this.grid, this.plot_data, this.interval);
337 :
338 2 : sum_series.addEventListener("removed", this.refresh.bind(this));
339 2 : sum_series.addEventListener("changed", this.check_archives.bind(this));
340 2 : sum_series.reset_series();
341 2 : sum_series.check_archives();
342 :
343 2 : this.series.push(sum_series);
344 2 : this.sync();
345 2 : this.plot_data.push(opts);
346 :
347 2 : return sum_series;
348 2 : }
349 :
350 149 : add_metrics_stacked_instances_series(desc, opts) {
351 149 : const stacked_series = new Metrics_stacked_instances_series(desc, opts, this.grid, this.plot_data, this.interval);
352 :
353 149 : stacked_series.addEventListener("removed", this.refresh.bind(this));
354 149 : stacked_series.addEventListener("changed", this.check_archives.bind(this));
355 149 : stacked_series.reset_series();
356 149 : stacked_series.check_archives();
357 :
358 149 : this.series.push(stacked_series);
359 149 : this.sync_suppressed++;
360 149 : for (const name in stacked_series.instances)
361 20 : stacked_series.instances[name].reset();
362 149 : this.sync_suppressed--;
363 149 : this.sync();
364 :
365 149 : return stacked_series;
366 149 : }
367 151 : }
368 :
369 151 : class ZoomState {
370 32 : constructor(reset_callback) {
371 32 : cockpit.event_target(this);
372 :
373 32 : this.reset_callback = reset_callback;
374 :
375 32 : this.x_range = 5 * 60;
376 32 : this.x_stop = undefined;
377 32 : this.history = [];
378 :
379 32 : this.enable_zoom_in = false;
380 32 : this.enable_zoom_out = true;
381 32 : this.enable_scroll_left = true;
382 32 : this.enable_scroll_right = false;
383 32 : }
384 :
385 0 : reset() {
386 0 : const plot_min_x_range = 5 * 60;
387 :
388 0 : if (this.x_range < plot_min_x_range) {
389 0 : this.x_stop += (plot_min_x_range - this.x_range) / 2;
390 0 : this.x_range = plot_min_x_range;
391 0 : }
392 0 : if (this.x_stop >= (new Date()).getTime() / 1000 - 10)
393 0 : this.x_stop = undefined;
394 :
395 0 : this.reset_callback(this.x_range, this.x_stop);
396 :
397 0 : this.enable_zoom_in = (this.x_range > plot_min_x_range);
398 0 : this.enable_scroll_right = (this.x_stop !== undefined);
399 :
400 0 : this.dispatchEvent("changed");
401 0 : }
402 :
403 0 : set_range(x_range) {
404 0 : this.history = [];
405 0 : this.x_range = x_range;
406 0 : this.reset();
407 0 : }
408 :
409 0 : zoom_in(x_range, x_stop) {
410 0 : this.history.push(this.x_range);
411 0 : this.x_range = x_range;
412 0 : this.x_stop = x_stop;
413 0 : this.reset();
414 0 : }
415 :
416 0 : zoom_out() {
417 0 : const plot_zoom_steps = [
418 0 : 5 * 60,
419 0 : 60 * 60,
420 0 : 6 * 60 * 60,
421 0 : 24 * 60 * 60,
422 0 : 7 * 24 * 60 * 60,
423 0 : 30 * 24 * 60 * 60,
424 0 : 365 * 24 * 60 * 60
425 0 : ];
426 :
427 0 : let r = this.history.pop();
428 0 : if (r === undefined) {
429 0 : let i;
430 0 : for (i = 0; i < plot_zoom_steps.length - 1; i++) {
431 0 : if (plot_zoom_steps[i] > this.x_range)
432 0 : break;
433 0 : }
434 0 : r = plot_zoom_steps[i];
435 0 : }
436 0 : if (this.x_stop !== undefined)
437 0 : this.x_stop += (r - this.x_range) / 2;
438 0 : this.x_range = r;
439 0 : this.reset();
440 0 : }
441 :
442 0 : goto_now() {
443 0 : this.x_stop = undefined;
444 0 : this.reset();
445 0 : }
446 :
447 0 : scroll_left() {
448 0 : const step = this.x_range / 10;
449 0 : if (this.x_stop === undefined)
450 0 : this.x_stop = (new Date()).getTime() / 1000;
451 0 : this.x_stop -= step;
452 0 : this.reset();
453 0 : }
454 :
455 0 : scroll_right() {
456 0 : const step = this.x_range / 10;
457 0 : if (this.x_stop !== undefined) {
458 0 : this.x_stop += step;
459 0 : this.reset();
460 0 : }
461 0 : }
462 151 : }
463 :
464 151 : class SinglePlotState {
465 150 : constructor() {
466 150 : this._plot = new Plot(null, 300);
467 150 : this._plot.start_walking();
468 150 : }
469 :
470 2 : plot_single(metric) {
471 1 : if (this._stacked_instances_series) {
472 1 : this._stacked_instances_series.clear_instances();
473 1 : this._stacked_instances_series.remove();
474 1 : this._stacked_instances_series = null;
475 1 : }
476 2 : if (!this._sum_series) {
477 2 : this._sum_series = this._plot.add_metrics_sum_series(metric, { });
478 2 : }
479 2 : }
480 :
481 149 : plot_instances(metric, insts, reset) {
482 20 : if (this._sum_series) {
483 20 : this._sum_series.remove();
484 20 : this._sum_series = null;
485 20 : }
486 149 : if (!this._stacked_instances_series) {
487 149 : this._stacked_instances_series = this._plot.add_metrics_stacked_instances_series(metric, { });
488 43 : } else if (reset) {
489 : // We can't remove individual instances, only clear the
490 : // whole thing (because of limitations of Metrics_stacked_instances_series above).
491 : // So we do that, but only when there is at least one instance
492 : // that needs to be removed. That avoids a lot of events and React warnings.
493 25 : if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) == -1))
494 26 : this._stacked_instances_series.clear_instances();
495 43 : }
496 :
497 149 : for (let i = 0; i < insts.length; i++) {
498 149 : this._stacked_instances_series.add_instance(insts[i]);
499 149 : }
500 149 : }
501 :
502 0 : destroy() {
503 0 : this._plot.destroy();
504 0 : }
505 151 : }
506 :
507 151 : export class PlotState {
508 151 : constructor() {
509 151 : cockpit.event_target(this);
510 151 : this.plots = { };
511 151 : this.zoom_state = null;
512 151 : }
513 :
514 0 : _reset_plots(x_range, x_stop) {
515 0 : for (const id in this.plots) {
516 0 : const p = this.plots[id]._plot;
517 0 : p.stop_walking();
518 0 : p.reset(x_range, x_stop);
519 0 : p.refresh();
520 0 : if (x_stop === undefined)
521 0 : p.start_walking();
522 0 : }
523 0 : }
524 :
525 150 : _check_archives(plot) {
526 49 : if (!this.zoom_state && plot.archives) {
527 49 : this.zoom_state = new ZoomState(this._reset_plots.bind(this));
528 49 : this.dispatchEvent("changed");
529 49 : }
530 150 : }
531 :
532 150 : _get(id) {
533 150 : if (this.plots[id])
534 145 : return this.plots[id];
535 :
536 150 : const ps = new SinglePlotState();
537 32 : ps._plot.addEventListener("changed", () => this._check_archives(ps._plot));
538 150 : this._check_archives(ps._plot);
539 150 : ps._plot.addEventListener("plot", (event, data) => {
540 150 : ps.data = data;
541 150 : this.dispatchEvent("plot:" + id);
542 150 : });
543 :
544 150 : this.plots[id] = ps;
545 150 : return ps;
546 150 : }
547 :
548 2 : plot_single(id, metric) {
549 2 : const ps = this._get(id);
550 2 : ps.plot_single(metric);
551 2 : }
552 :
553 149 : plot_instances(id, metric, insts, reset) {
554 149 : this._get(id).plot_instances(metric, insts, reset);
555 149 : }
556 :
557 133 : data(id) {
558 133 : return this.plots[id] && this.plots[id].data;
559 133 : }
560 151 : }
|