LCOV - code coverage report
Current view: top level - pkg/lib - plot.js Coverage Total Hit
Test: cockpit Lines: 79.2 % 448 355
Test Date: 2026-07-03 14:25:32

            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           46 :             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           19 :         if (this.desc.pmcd) {
     153           19 :             this.chanopts_list.push({
     154           19 :                 source: 'pmcd',
     155           19 :                 metrics: this.desc.pmcd.map(this.build_metric, this),
     156           19 :                 metrics_path_names: ['a'],
     157           19 :                 instances: this.desc.instances,
     158           19 :                 'omit-instances': this.desc['omit-instances'],
     159           19 :                 host: this.desc.host
     160           19 :             });
     161           19 :         }
     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           19 :             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           19 :             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           46 :         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          129 :                             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          120 :                             row[x + i - 1][1] = row[x + i - 1][2];
     218          149 :                     }
     219          149 :                 }
     220          149 :             });
     221          149 :         }
     222              : 
     223            7 :         function remove() {
     224            7 :             this.grid.remove(metrics_row);
     225            7 :             this.grid.remove(instance_data.data);
     226            7 :             const pos = this.plot_data.indexOf(instance_data);
     227            7 :             if (pos >= 0)
     228            7 :                 this.plot_data.splice(pos, 1);
     229            7 :         }
     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            7 :     clear_instances() {
     242            7 :         for (const i in this.instances)
     243            7 :             this.instances[i].remove();
     244            7 :         this.instances = { };
     245            7 :         this.last_instance = null;
     246            7 :     }
     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           19 :             ? (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           19 :         if (this.grid && this.grid.interval == this.interval) {
     292           19 :             this.grid.move(beg, end);
     293           19 :         } else {
     294          150 :             if (this.grid)
     295           19 :                 this.grid.close();
     296          150 :             this.grid = cockpit.grid(this.interval, beg, end);
     297          150 :             this.sync_suppressed++;
     298           19 :             for (let i = 0; i < this.series.length; i++) {
     299           19 :                 this.series[i].stop();
     300           19 :                 this.series[i].interval = this.interval;
     301           19 :                 this.series[i].grid = this.grid;
     302           19 :                 this.series[i].reset_series();
     303           19 :             }
     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           28 :     check_archives() {
     329           28 :         if (!this.archives) {
     330           28 :             this.archives = true;
     331           28 :             this.dispatchEvent('changed');
     332           28 :         }
     333           28 :     }
     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           19 :             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           28 :     constructor(reset_callback) {
     371           28 :         cockpit.event_target(this);
     372              : 
     373           28 :         this.reset_callback = reset_callback;
     374              : 
     375           28 :         this.x_range = 5 * 60;
     376           28 :         this.x_stop = undefined;
     377           28 :         this.history = [];
     378              : 
     379           28 :         this.enable_zoom_in = false;
     380           28 :         this.enable_zoom_out = true;
     381           28 :         this.enable_scroll_left = true;
     382           28 :         this.enable_scroll_right = false;
     383           28 :     }
     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           19 :         if (this._sum_series) {
     483           19 :             this._sum_series.remove();
     484           19 :             this._sum_series = null;
     485           19 :         }
     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           26 :             if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) == -1))
     494           24 :                 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           46 :         if (!this.zoom_state && plot.archives) {
     527           46 :             this.zoom_state = new ZoomState(this._reset_plots.bind(this));
     528           46 :             this.dispatchEvent("changed");
     529           46 :         }
     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           28 :         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          131 :     data(id) {
     558          131 :         return this.plots[id] && this.plots[id].data;
     559          131 :     }
     560          151 : }
        

Generated by: LCOV version 2.0-1