LCOV - code coverage report
Current view: top level - pkg/lib - plot.js Coverage Total Hit
Test: cockpit Lines: 43.5 % 448 195
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2014 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6            1 : 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            1 : class Metrics_series {
      28            1 :     constructor(desc, opts, grid, plot_data, interval) {
      29            1 :         cockpit.event_target(this);
      30            1 :         this.desc = desc;
      31            1 :         this.options = opts;
      32            1 :         this.grid = grid;
      33            1 :         this.plot_data = plot_data;
      34            1 :         this.interval = interval;
      35            1 :         this.channel = null;
      36            1 :         this.chanopts_list = [];
      37            1 :     }
      38              : 
      39            0 :     stop() {
      40            0 :         if (this.channel)
      41            0 :             this.channel.close();
      42            0 :     }
      43              : 
      44            0 :     remove_series() {
      45            0 :         const pos = this.plot_data.indexOf(this.options);
      46            0 :         if (pos >= 0)
      47            0 :             this.plot_data.splice(pos, 1);
      48            0 :     }
      49              : 
      50            0 :     remove() {
      51            0 :         this.stop();
      52            0 :         this.remove_series();
      53            0 :         this.dispatchEvent("removed");
      54            0 :     }
      55              : 
      56            1 :     build_metric(n) {
      57            1 :         return { name: n, units: this.desc.units, derive: this.desc.derive };
      58            1 :     }
      59              : 
      60            1 :     check_archives() {
      61            1 :         if (this.channel.archives)
      62            0 :             this.dispatchEvent("changed");
      63            1 :     }
      64            1 : }
      65              : 
      66            1 : class Metrics_sum_series extends Metrics_series {
      67            0 :     constructor(desc, opts, grid, plot_data, interval) {
      68            0 :         super(desc, opts, grid, plot_data, interval);
      69            0 :         if (this.desc.direct) {
      70            0 :             this.chanopts_list.push({
      71            0 :                 source: 'direct',
      72            0 :                 archive_source: 'pcp-archive',
      73            0 :                 metrics: this.desc.direct.map(this.build_metric, this),
      74            0 :                 instances: this.desc.instances,
      75            0 :                 'omit-instances': this.desc['omit-instances'],
      76            0 :                 host: this.desc.host
      77            0 :             });
      78            0 :         }
      79            0 :         if (this.desc.pmcd) {
      80            0 :             this.chanopts_list.push({
      81            0 :                 source: 'pmcd',
      82            0 :                 metrics: this.desc.pmcd.map(this.build_metric, this),
      83            0 :                 instances: this.desc.instances,
      84            0 :                 'omit-instances': this.desc['omit-instances'],
      85            0 :                 host: this.desc.host
      86            0 :             });
      87            0 :         }
      88            0 :         if (this.desc.internal) {
      89            0 :             this.chanopts_list.push({
      90            0 :                 source: 'internal',
      91            0 :                 metrics: this.desc.internal.map(this.build_metric, this),
      92            0 :                 instances: this.desc.instances,
      93            0 :                 'omit-instances': this.desc['omit-instances'],
      94            0 :                 host: this.desc.host
      95            0 :             });
      96            0 :         }
      97            0 :     }
      98              : 
      99            0 :     flat_sum(val) {
     100            0 :         if (!val)
     101            0 :             return 0;
     102            0 :         if (val.length !== undefined) {
     103            0 :             let sum = 0;
     104            0 :             for (let i = 0; i < val.length; i++)
     105            0 :                 sum += this.flat_sum(val[i]);
     106            0 :             return sum;
     107            0 :         }
     108            0 :         return val;
     109            0 :     }
     110              : 
     111            0 :     reset_series() {
     112            0 :         if (this.channel)
     113            0 :             this.channel.close();
     114              : 
     115            0 :         this.channel = cockpit.metrics(this.interval, this.chanopts_list);
     116              : 
     117            0 :         const metrics_row = this.grid.add(this.channel, []);
     118            0 :         const factor = this.desc.factor || 1;
     119            0 :         const threshold = this.desc.threshold || null;
     120            0 :         const offset = this.desc.offset || 0;
     121            0 :         this.options.data = this.grid.add((row, x, n) => {
     122            0 :             for (let i = 0; i < n; i++) {
     123            0 :                 const value = offset + this.flat_sum(metrics_row[x + i]) * factor;
     124            0 :                 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            0 :                     row[x + i] = [(this.grid.beg + x + i) * this.interval, value];
     128            0 :             }
     129            0 :         });
     130              : 
     131            0 :         this.channel.addEventListener("changed", this.check_archives.bind(this));
     132            0 :         this.check_archives();
     133            0 :     }
     134            1 : }
     135              : 
     136            1 : class Metrics_stacked_instances_series extends Metrics_series {
     137            1 :     constructor(desc, opts, grid, plot_data, interval) {
     138            1 :         super(desc, opts, grid, plot_data, interval);
     139            1 :         this.instances = { };
     140            1 :         this.last_instance = null;
     141            1 :         if (this.desc.direct) {
     142            1 :             this.chanopts_list.push({
     143            1 :                 source: 'direct',
     144            1 :                 archive_source: 'pcp-archive',
     145            1 :                 metrics: [this.build_metric(this.desc.direct)],
     146            1 :                 metrics_path_names: ['a'],
     147            1 :                 instances: this.desc.instances,
     148            1 :                 'omit-instances': this.desc['omit-instances'],
     149            1 :                 host: this.desc.host
     150            1 :             });
     151            1 :         }
     152            0 :         if (this.desc.pmcd) {
     153            0 :             this.chanopts_list.push({
     154            0 :                 source: 'pmcd',
     155            0 :                 metrics: this.desc.pmcd.map(this.build_metric, this),
     156            0 :                 metrics_path_names: ['a'],
     157            0 :                 instances: this.desc.instances,
     158            0 :                 'omit-instances': this.desc['omit-instances'],
     159            0 :                 host: this.desc.host
     160            0 :             });
     161            0 :         }
     162              : 
     163            1 :         if (this.desc.internal) {
     164            1 :             this.chanopts_list.push({
     165            1 :                 source: 'internal',
     166            1 :                 metrics: [this.build_metric(this.desc.internal)],
     167            1 :                 metrics_path_names: ['a'],
     168            1 :                 instances: this.desc.instances,
     169            1 :                 'omit-instances': this.desc['omit-instances'],
     170            1 :                 host: this.desc.host
     171            1 :             });
     172            1 :         }
     173            1 :     }
     174              : 
     175            1 :     reset_series() {
     176            1 :         if (this.channel)
     177            0 :             this.channel.close();
     178            1 :         this.channel = cockpit.metrics(this.interval, this.chanopts_list);
     179            1 :         this.channel.addEventListener("changed", this.check_archives.bind(this));
     180            1 :         this.check_archives();
     181            1 :         for (const name in this.instances)
     182            0 :             this.instances[name].reset();
     183            1 :     }
     184              : 
     185            1 :     add_instance(name, selector) {
     186            1 :         if (this.instances[name])
     187            1 :             return;
     188              : 
     189            1 :         const instance_data = Object.assign({ selector }, this.options);
     190            0 :         const factor = this.desc.factor || 1;
     191            1 :         const threshold = this.desc.threshold || 0;
     192            1 :         const last = this.last_instance;
     193            1 :         let metrics_row;
     194              : 
     195            1 :         function reset() {
     196            1 :             metrics_row = this.grid.add(this.channel, ['a', name]);
     197            1 :             instance_data.data = this.grid.add((row, x, n) => {
     198            1 :                 for (let i = 0; i < n; i++) {
     199            1 :                     const value = (metrics_row[x + i] || 0) * factor;
     200            1 :                     const ts = (this.grid.beg + x + i) * this.interval;
     201            1 :                     let floor = 0;
     202              : 
     203            0 :                     if (last) {
     204            0 :                         if (last.data[x + i][1])
     205            0 :                             floor = last.data[x + i][1];
     206              :                         else
     207            0 :                             floor = last.data[x + i][2];
     208            0 :                     }
     209              : 
     210            0 :                     if (Math.abs(value) > threshold) {
     211            0 :                         row[x + i] = [ts, floor + value, floor];
     212            0 :                         if (row[x + i - 1] && row[x + i - 1][1] === null)
     213            0 :                             row[x + i - 1][1] = row[x + i - 1][2];
     214            0 :                     } else {
     215            1 :                         row[x + i] = [ts, null, floor];
     216            1 :                         if (row[x + i - 1] && row[x + i - 1][1] !== null)
     217            0 :                             row[x + i - 1][1] = row[x + i - 1][2];
     218            1 :                     }
     219            1 :                 }
     220            1 :             });
     221            1 :         }
     222              : 
     223            1 :         function remove() {
     224            1 :             this.grid.remove(metrics_row);
     225            1 :             this.grid.remove(instance_data.data);
     226            1 :             const pos = this.plot_data.indexOf(instance_data);
     227            1 :             if (pos >= 0)
     228            1 :                 this.plot_data.splice(pos, 1);
     229            1 :         }
     230              : 
     231            1 :         instance_data.reset = reset.bind(this);
     232            1 :         instance_data.remove = remove.bind(this);
     233            1 :         instance_data.name = name;
     234            1 :         this.last_instance = instance_data;
     235            1 :         this.instances[name] = instance_data;
     236            1 :         instance_data.reset();
     237            1 :         this.plot_data.push(instance_data);
     238            1 :         this.grid.sync();
     239            1 :     }
     240              : 
     241            1 :     clear_instances() {
     242            1 :         for (const i in this.instances)
     243            1 :             this.instances[i].remove();
     244            1 :         this.instances = { };
     245            1 :         this.last_instance = null;
     246            1 :     }
     247            1 : }
     248              : 
     249            1 : class Plot {
     250            1 :     constructor(element, x_range_seconds, x_stop_seconds) {
     251            1 :         cockpit.event_target(this);
     252              : 
     253            1 :         this.series = [];
     254            1 :         this.plot_data = [];
     255              : 
     256            1 :         this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
     257            1 :         this.grid = null;
     258              : 
     259            1 :         this.sync_suppressed = 0;
     260            1 :         this.archives = false;
     261              : 
     262            1 :         this.reset(x_range_seconds, x_stop_seconds);
     263            1 :     }
     264              : 
     265            1 :     refresh() {
     266            1 :         this.dispatchEvent("plot", this.plot_data);
     267            1 :     }
     268              : 
     269            1 :     start_walking() {
     270            1 :         this.grid.walk();
     271            1 :     }
     272              : 
     273            0 :     stop_walking() {
     274            0 :         this.grid.move(this.grid.beg, this.grid.end);
     275            0 :     }
     276              : 
     277            1 :     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            1 :         this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
     283              : 
     284            1 :         const x_offset = (x_stop_seconds !== undefined)
     285            0 :             ? (new Date().getTime()) - x_stop_seconds * 1000
     286            1 :             : 0;
     287              : 
     288            1 :         const beg = -Math.ceil((x_range_seconds * 1000 + x_offset) / this.interval);
     289            1 :         const end = -Math.floor(x_offset / this.interval);
     290              : 
     291            0 :         if (this.grid && this.grid.interval == this.interval) {
     292            0 :             this.grid.move(beg, end);
     293            0 :         } else {
     294            1 :             if (this.grid)
     295            0 :                 this.grid.close();
     296            1 :             this.grid = cockpit.grid(this.interval, beg, end);
     297            1 :             this.sync_suppressed++;
     298            0 :             for (let i = 0; i < this.series.length; i++) {
     299            0 :                 this.series[i].stop();
     300            0 :                 this.series[i].interval = this.interval;
     301            0 :                 this.series[i].grid = this.grid;
     302            0 :                 this.series[i].reset_series();
     303            0 :             }
     304            1 :             this.sync_suppressed--;
     305            1 :             this.sync();
     306              : 
     307            1 :             this.grid.addEventListener("notify", (event, index, count) => {
     308            1 :                 this.refresh();
     309            1 :             });
     310            1 :         }
     311            1 :     }
     312              : 
     313            1 :     sync() {
     314            1 :         if (this.sync_suppressed === 0)
     315            1 :             this.grid.sync();
     316            1 :     }
     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            0 :     check_archives() {
     329            0 :         if (!this.archives) {
     330            0 :             this.archives = true;
     331            0 :             this.dispatchEvent('changed');
     332            0 :         }
     333            0 :     }
     334              : 
     335            0 :     add_metrics_sum_series(desc, opts) {
     336            0 :         const sum_series = new Metrics_sum_series(desc, opts, this.grid, this.plot_data, this.interval);
     337              : 
     338            0 :         sum_series.addEventListener("removed", this.refresh.bind(this));
     339            0 :         sum_series.addEventListener("changed", this.check_archives.bind(this));
     340            0 :         sum_series.reset_series();
     341            0 :         sum_series.check_archives();
     342              : 
     343            0 :         this.series.push(sum_series);
     344            0 :         this.sync();
     345            0 :         this.plot_data.push(opts);
     346              : 
     347            0 :         return sum_series;
     348            0 :     }
     349              : 
     350            1 :     add_metrics_stacked_instances_series(desc, opts) {
     351            1 :         const stacked_series = new Metrics_stacked_instances_series(desc, opts, this.grid, this.plot_data, this.interval);
     352              : 
     353            1 :         stacked_series.addEventListener("removed", this.refresh.bind(this));
     354            1 :         stacked_series.addEventListener("changed", this.check_archives.bind(this));
     355            1 :         stacked_series.reset_series();
     356            1 :         stacked_series.check_archives();
     357              : 
     358            1 :         this.series.push(stacked_series);
     359            1 :         this.sync_suppressed++;
     360            1 :         for (const name in stacked_series.instances)
     361            0 :             stacked_series.instances[name].reset();
     362            1 :         this.sync_suppressed--;
     363            1 :         this.sync();
     364              : 
     365            1 :         return stacked_series;
     366            1 :     }
     367            1 : }
     368              : 
     369            1 : class ZoomState {
     370            0 :     constructor(reset_callback) {
     371            0 :         cockpit.event_target(this);
     372              : 
     373            0 :         this.reset_callback = reset_callback;
     374              : 
     375            0 :         this.x_range = 5 * 60;
     376            0 :         this.x_stop = undefined;
     377            0 :         this.history = [];
     378              : 
     379            0 :         this.enable_zoom_in = false;
     380            0 :         this.enable_zoom_out = true;
     381            0 :         this.enable_scroll_left = true;
     382            0 :         this.enable_scroll_right = false;
     383            0 :     }
     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            1 : }
     463              : 
     464            1 : class SinglePlotState {
     465            1 :     constructor() {
     466            1 :         this._plot = new Plot(null, 300);
     467            1 :         this._plot.start_walking();
     468            1 :     }
     469              : 
     470            0 :     plot_single(metric) {
     471            0 :         if (this._stacked_instances_series) {
     472            0 :             this._stacked_instances_series.clear_instances();
     473            0 :             this._stacked_instances_series.remove();
     474            0 :             this._stacked_instances_series = null;
     475            0 :         }
     476            0 :         if (!this._sum_series) {
     477            0 :             this._sum_series = this._plot.add_metrics_sum_series(metric, { });
     478            0 :         }
     479            0 :     }
     480              : 
     481            1 :     plot_instances(metric, insts, reset) {
     482            0 :         if (this._sum_series) {
     483            0 :             this._sum_series.remove();
     484            0 :             this._sum_series = null;
     485            0 :         }
     486            1 :         if (!this._stacked_instances_series) {
     487            1 :             this._stacked_instances_series = this._plot.add_metrics_stacked_instances_series(metric, { });
     488            1 :         } 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            1 :             if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) == -1))
     494            1 :                 this._stacked_instances_series.clear_instances();
     495            1 :         }
     496              : 
     497            1 :         for (let i = 0; i < insts.length; i++) {
     498            1 :             this._stacked_instances_series.add_instance(insts[i]);
     499            1 :         }
     500            1 :     }
     501              : 
     502            0 :     destroy() {
     503            0 :         this._plot.destroy();
     504            0 :     }
     505            1 : }
     506              : 
     507            1 : export class PlotState {
     508            1 :     constructor() {
     509            1 :         cockpit.event_target(this);
     510            1 :         this.plots = { };
     511            1 :         this.zoom_state = null;
     512            1 :     }
     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            1 :     _check_archives(plot) {
     526            0 :         if (!this.zoom_state && plot.archives) {
     527            0 :             this.zoom_state = new ZoomState(this._reset_plots.bind(this));
     528            0 :             this.dispatchEvent("changed");
     529            0 :         }
     530            1 :     }
     531              : 
     532            1 :     _get(id) {
     533            1 :         if (this.plots[id])
     534            1 :             return this.plots[id];
     535              : 
     536            1 :         const ps = new SinglePlotState();
     537            0 :         ps._plot.addEventListener("changed", () => this._check_archives(ps._plot));
     538            1 :         this._check_archives(ps._plot);
     539            1 :         ps._plot.addEventListener("plot", (event, data) => {
     540            1 :             ps.data = data;
     541            1 :             this.dispatchEvent("plot:" + id);
     542            1 :         });
     543              : 
     544            1 :         this.plots[id] = ps;
     545            1 :         return ps;
     546            1 :     }
     547              : 
     548            0 :     plot_single(id, metric) {
     549            0 :         const ps = this._get(id);
     550            0 :         ps.plot_single(metric);
     551            0 :     }
     552              : 
     553            1 :     plot_instances(id, metric, insts, reset) {
     554            1 :         this._get(id).plot_instances(metric, insts, reset);
     555            1 :     }
     556              : 
     557            1 :     data(id) {
     558            1 :         return this.plots[id] && this.plots[id].data;
     559            1 :     }
     560            1 : }
        

Generated by: LCOV version 2.0-1