mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
560 lines
17 KiB
JavaScript
560 lines
17 KiB
JavaScript
/*
|
|
* Copyright (C) 2014 Red Hat, Inc.
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
*/
|
|
|
|
import cockpit from 'cockpit';
|
|
|
|
/* The public API here is not general and a bit weird. It can only do
|
|
* what Cockpit itself needs right now, and what can be easily
|
|
* implemented without touching the older Metrics_series classes.
|
|
*
|
|
* The basic idea is that you create a global PlotState object and
|
|
* keep it alive for the lifetime of the page. Then you instantiate
|
|
* ZoomControl and SvgPlot components as needed.
|
|
*
|
|
* To control what to plot, you call some methods on the PlotState
|
|
* object:
|
|
*
|
|
* - plot_state.plot_single(id, metric_options)
|
|
*
|
|
* - plot_state.plot_instances(id, metric_options, instances, reset)
|
|
*
|
|
* You need to figure out the rest of the details from the existing
|
|
* users, unfortunately.
|
|
*/
|
|
|
|
class Metrics_series {
|
|
constructor(desc, opts, grid, plot_data, interval) {
|
|
cockpit.event_target(this);
|
|
this.desc = desc;
|
|
this.options = opts;
|
|
this.grid = grid;
|
|
this.plot_data = plot_data;
|
|
this.interval = interval;
|
|
this.channel = null;
|
|
this.chanopts_list = [];
|
|
}
|
|
|
|
stop() {
|
|
if (this.channel)
|
|
this.channel.close();
|
|
}
|
|
|
|
remove_series() {
|
|
const pos = this.plot_data.indexOf(this.options);
|
|
if (pos >= 0)
|
|
this.plot_data.splice(pos, 1);
|
|
}
|
|
|
|
remove() {
|
|
this.stop();
|
|
this.remove_series();
|
|
this.dispatchEvent("removed");
|
|
}
|
|
|
|
build_metric(n) {
|
|
return { name: n, units: this.desc.units, derive: this.desc.derive };
|
|
}
|
|
|
|
check_archives() {
|
|
if (this.channel.archives)
|
|
this.dispatchEvent("changed");
|
|
}
|
|
}
|
|
|
|
class Metrics_sum_series extends Metrics_series {
|
|
constructor(desc, opts, grid, plot_data, interval) {
|
|
super(desc, opts, grid, plot_data, interval);
|
|
if (this.desc.direct) {
|
|
this.chanopts_list.push({
|
|
source: 'direct',
|
|
archive_source: 'pcp-archive',
|
|
metrics: this.desc.direct.map(this.build_metric, this),
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
if (this.desc.pmcd) {
|
|
this.chanopts_list.push({
|
|
source: 'pmcd',
|
|
metrics: this.desc.pmcd.map(this.build_metric, this),
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
if (this.desc.internal) {
|
|
this.chanopts_list.push({
|
|
source: 'internal',
|
|
metrics: this.desc.internal.map(this.build_metric, this),
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
}
|
|
|
|
flat_sum(val) {
|
|
if (!val)
|
|
return 0;
|
|
if (val.length !== undefined) {
|
|
let sum = 0;
|
|
for (let i = 0; i < val.length; i++)
|
|
sum += this.flat_sum(val[i]);
|
|
return sum;
|
|
}
|
|
return val;
|
|
}
|
|
|
|
reset_series() {
|
|
if (this.channel)
|
|
this.channel.close();
|
|
|
|
this.channel = cockpit.metrics(this.interval, this.chanopts_list);
|
|
|
|
const metrics_row = this.grid.add(this.channel, []);
|
|
const factor = this.desc.factor || 1;
|
|
const threshold = this.desc.threshold || null;
|
|
const offset = this.desc.offset || 0;
|
|
this.options.data = this.grid.add((row, x, n) => {
|
|
for (let i = 0; i < n; i++) {
|
|
const value = offset + this.flat_sum(metrics_row[x + i]) * factor;
|
|
if (threshold !== null)
|
|
row[x + i] = [(this.grid.beg + x + i) * this.interval, Math.abs(value) > threshold ? value : null, threshold];
|
|
else
|
|
row[x + i] = [(this.grid.beg + x + i) * this.interval, value];
|
|
}
|
|
});
|
|
|
|
this.channel.addEventListener("changed", this.check_archives.bind(this));
|
|
this.check_archives();
|
|
}
|
|
}
|
|
|
|
class Metrics_stacked_instances_series extends Metrics_series {
|
|
constructor(desc, opts, grid, plot_data, interval) {
|
|
super(desc, opts, grid, plot_data, interval);
|
|
this.instances = { };
|
|
this.last_instance = null;
|
|
if (this.desc.direct) {
|
|
this.chanopts_list.push({
|
|
source: 'direct',
|
|
archive_source: 'pcp-archive',
|
|
metrics: [this.build_metric(this.desc.direct)],
|
|
metrics_path_names: ['a'],
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
if (this.desc.pmcd) {
|
|
this.chanopts_list.push({
|
|
source: 'pmcd',
|
|
metrics: this.desc.pmcd.map(this.build_metric, this),
|
|
metrics_path_names: ['a'],
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
|
|
if (this.desc.internal) {
|
|
this.chanopts_list.push({
|
|
source: 'internal',
|
|
metrics: [this.build_metric(this.desc.internal)],
|
|
metrics_path_names: ['a'],
|
|
instances: this.desc.instances,
|
|
'omit-instances': this.desc['omit-instances'],
|
|
host: this.desc.host
|
|
});
|
|
}
|
|
}
|
|
|
|
reset_series() {
|
|
if (this.channel)
|
|
this.channel.close();
|
|
this.channel = cockpit.metrics(this.interval, this.chanopts_list);
|
|
this.channel.addEventListener("changed", this.check_archives.bind(this));
|
|
this.check_archives();
|
|
for (const name in this.instances)
|
|
this.instances[name].reset();
|
|
}
|
|
|
|
add_instance(name, selector) {
|
|
if (this.instances[name])
|
|
return;
|
|
|
|
const instance_data = Object.assign({ selector }, this.options);
|
|
const factor = this.desc.factor || 1;
|
|
const threshold = this.desc.threshold || 0;
|
|
const last = this.last_instance;
|
|
let metrics_row;
|
|
|
|
function reset() {
|
|
metrics_row = this.grid.add(this.channel, ['a', name]);
|
|
instance_data.data = this.grid.add((row, x, n) => {
|
|
for (let i = 0; i < n; i++) {
|
|
const value = (metrics_row[x + i] || 0) * factor;
|
|
const ts = (this.grid.beg + x + i) * this.interval;
|
|
let floor = 0;
|
|
|
|
if (last) {
|
|
if (last.data[x + i][1])
|
|
floor = last.data[x + i][1];
|
|
else
|
|
floor = last.data[x + i][2];
|
|
}
|
|
|
|
if (Math.abs(value) > threshold) {
|
|
row[x + i] = [ts, floor + value, floor];
|
|
if (row[x + i - 1] && row[x + i - 1][1] === null)
|
|
row[x + i - 1][1] = row[x + i - 1][2];
|
|
} else {
|
|
row[x + i] = [ts, null, floor];
|
|
if (row[x + i - 1] && row[x + i - 1][1] !== null)
|
|
row[x + i - 1][1] = row[x + i - 1][2];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function remove() {
|
|
this.grid.remove(metrics_row);
|
|
this.grid.remove(instance_data.data);
|
|
const pos = this.plot_data.indexOf(instance_data);
|
|
if (pos >= 0)
|
|
this.plot_data.splice(pos, 1);
|
|
}
|
|
|
|
instance_data.reset = reset.bind(this);
|
|
instance_data.remove = remove.bind(this);
|
|
instance_data.name = name;
|
|
this.last_instance = instance_data;
|
|
this.instances[name] = instance_data;
|
|
instance_data.reset();
|
|
this.plot_data.push(instance_data);
|
|
this.grid.sync();
|
|
}
|
|
|
|
clear_instances() {
|
|
for (const i in this.instances)
|
|
this.instances[i].remove();
|
|
this.instances = { };
|
|
this.last_instance = null;
|
|
}
|
|
}
|
|
|
|
class Plot {
|
|
constructor(element, x_range_seconds, x_stop_seconds) {
|
|
cockpit.event_target(this);
|
|
|
|
this.series = [];
|
|
this.plot_data = [];
|
|
|
|
this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
|
|
this.grid = null;
|
|
|
|
this.sync_suppressed = 0;
|
|
this.archives = false;
|
|
|
|
this.reset(x_range_seconds, x_stop_seconds);
|
|
}
|
|
|
|
refresh() {
|
|
this.dispatchEvent("plot", this.plot_data);
|
|
}
|
|
|
|
start_walking() {
|
|
this.grid.walk();
|
|
}
|
|
|
|
stop_walking() {
|
|
this.grid.move(this.grid.beg, this.grid.end);
|
|
}
|
|
|
|
reset(x_range_seconds, x_stop_seconds) {
|
|
// Fill the plot with about 1000 samples, but don't sample
|
|
// faster than once per second.
|
|
//
|
|
// TODO - do this based on the actual size of the plot.
|
|
this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
|
|
|
|
const x_offset = (x_stop_seconds !== undefined)
|
|
? (new Date().getTime()) - x_stop_seconds * 1000
|
|
: 0;
|
|
|
|
const beg = -Math.ceil((x_range_seconds * 1000 + x_offset) / this.interval);
|
|
const end = -Math.floor(x_offset / this.interval);
|
|
|
|
if (this.grid && this.grid.interval === this.interval) {
|
|
this.grid.move(beg, end);
|
|
} else {
|
|
if (this.grid)
|
|
this.grid.close();
|
|
this.grid = cockpit.grid(this.interval, beg, end);
|
|
this.sync_suppressed++;
|
|
for (let i = 0; i < this.series.length; i++) {
|
|
this.series[i].stop();
|
|
this.series[i].interval = this.interval;
|
|
this.series[i].grid = this.grid;
|
|
this.series[i].reset_series();
|
|
}
|
|
this.sync_suppressed--;
|
|
this.sync();
|
|
|
|
this.grid.addEventListener("notify", (event, index, count) => {
|
|
this.refresh();
|
|
});
|
|
}
|
|
}
|
|
|
|
sync() {
|
|
if (this.sync_suppressed === 0)
|
|
this.grid.sync();
|
|
}
|
|
|
|
destroy() {
|
|
this.grid.close();
|
|
for (let i = 0; i < this.series.length; i++)
|
|
this.series[i].stop();
|
|
|
|
this.options = { };
|
|
this.series = [];
|
|
this.plot_data = [];
|
|
}
|
|
|
|
check_archives() {
|
|
if (!this.archives) {
|
|
this.archives = true;
|
|
this.dispatchEvent('changed');
|
|
}
|
|
}
|
|
|
|
add_metrics_sum_series(desc, opts) {
|
|
const sum_series = new Metrics_sum_series(desc, opts, this.grid, this.plot_data, this.interval);
|
|
|
|
sum_series.addEventListener("removed", this.refresh.bind(this));
|
|
sum_series.addEventListener("changed", this.check_archives.bind(this));
|
|
sum_series.reset_series();
|
|
sum_series.check_archives();
|
|
|
|
this.series.push(sum_series);
|
|
this.sync();
|
|
this.plot_data.push(opts);
|
|
|
|
return sum_series;
|
|
}
|
|
|
|
add_metrics_stacked_instances_series(desc, opts) {
|
|
const stacked_series = new Metrics_stacked_instances_series(desc, opts, this.grid, this.plot_data, this.interval);
|
|
|
|
stacked_series.addEventListener("removed", this.refresh.bind(this));
|
|
stacked_series.addEventListener("changed", this.check_archives.bind(this));
|
|
stacked_series.reset_series();
|
|
stacked_series.check_archives();
|
|
|
|
this.series.push(stacked_series);
|
|
this.sync_suppressed++;
|
|
for (const name in stacked_series.instances)
|
|
stacked_series.instances[name].reset();
|
|
this.sync_suppressed--;
|
|
this.sync();
|
|
|
|
return stacked_series;
|
|
}
|
|
}
|
|
|
|
class ZoomState {
|
|
constructor(reset_callback) {
|
|
cockpit.event_target(this);
|
|
|
|
this.reset_callback = reset_callback;
|
|
|
|
this.x_range = 5 * 60;
|
|
this.x_stop = undefined;
|
|
this.history = [];
|
|
|
|
this.enable_zoom_in = false;
|
|
this.enable_zoom_out = true;
|
|
this.enable_scroll_left = true;
|
|
this.enable_scroll_right = false;
|
|
}
|
|
|
|
reset() {
|
|
const plot_min_x_range = 5 * 60;
|
|
|
|
if (this.x_range < plot_min_x_range) {
|
|
this.x_stop += (plot_min_x_range - this.x_range) / 2;
|
|
this.x_range = plot_min_x_range;
|
|
}
|
|
if (this.x_stop >= (new Date()).getTime() / 1000 - 10)
|
|
this.x_stop = undefined;
|
|
|
|
this.reset_callback(this.x_range, this.x_stop);
|
|
|
|
this.enable_zoom_in = (this.x_range > plot_min_x_range);
|
|
this.enable_scroll_right = (this.x_stop !== undefined);
|
|
|
|
this.dispatchEvent("changed");
|
|
}
|
|
|
|
set_range(x_range) {
|
|
this.history = [];
|
|
this.x_range = x_range;
|
|
this.reset();
|
|
}
|
|
|
|
zoom_in(x_range, x_stop) {
|
|
this.history.push(this.x_range);
|
|
this.x_range = x_range;
|
|
this.x_stop = x_stop;
|
|
this.reset();
|
|
}
|
|
|
|
zoom_out() {
|
|
const plot_zoom_steps = [
|
|
5 * 60,
|
|
60 * 60,
|
|
6 * 60 * 60,
|
|
24 * 60 * 60,
|
|
7 * 24 * 60 * 60,
|
|
30 * 24 * 60 * 60,
|
|
365 * 24 * 60 * 60
|
|
];
|
|
|
|
let r = this.history.pop();
|
|
if (r === undefined) {
|
|
let i;
|
|
for (i = 0; i < plot_zoom_steps.length - 1; i++) {
|
|
if (plot_zoom_steps[i] > this.x_range)
|
|
break;
|
|
}
|
|
r = plot_zoom_steps[i];
|
|
}
|
|
if (this.x_stop !== undefined)
|
|
this.x_stop += (r - this.x_range) / 2;
|
|
this.x_range = r;
|
|
this.reset();
|
|
}
|
|
|
|
goto_now() {
|
|
this.x_stop = undefined;
|
|
this.reset();
|
|
}
|
|
|
|
scroll_left() {
|
|
const step = this.x_range / 10;
|
|
if (this.x_stop === undefined)
|
|
this.x_stop = (new Date()).getTime() / 1000;
|
|
this.x_stop -= step;
|
|
this.reset();
|
|
}
|
|
|
|
scroll_right() {
|
|
const step = this.x_range / 10;
|
|
if (this.x_stop !== undefined) {
|
|
this.x_stop += step;
|
|
this.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
class SinglePlotState {
|
|
constructor() {
|
|
this._plot = new Plot(null, 300);
|
|
this._plot.start_walking();
|
|
}
|
|
|
|
plot_single(metric) {
|
|
if (this._stacked_instances_series) {
|
|
this._stacked_instances_series.clear_instances();
|
|
this._stacked_instances_series.remove();
|
|
this._stacked_instances_series = null;
|
|
}
|
|
if (!this._sum_series) {
|
|
this._sum_series = this._plot.add_metrics_sum_series(metric, { });
|
|
}
|
|
}
|
|
|
|
plot_instances(metric, insts, reset) {
|
|
if (this._sum_series) {
|
|
this._sum_series.remove();
|
|
this._sum_series = null;
|
|
}
|
|
if (!this._stacked_instances_series) {
|
|
this._stacked_instances_series = this._plot.add_metrics_stacked_instances_series(metric, { });
|
|
} else if (reset) {
|
|
// We can't remove individual instances, only clear the
|
|
// whole thing (because of limitations of Metrics_stacked_instances_series above).
|
|
// So we do that, but only when there is at least one instance
|
|
// that needs to be removed. That avoids a lot of events and React warnings.
|
|
if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) === -1))
|
|
this._stacked_instances_series.clear_instances();
|
|
}
|
|
|
|
for (let i = 0; i < insts.length; i++) {
|
|
this._stacked_instances_series.add_instance(insts[i]);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this._plot.destroy();
|
|
}
|
|
}
|
|
|
|
export class PlotState {
|
|
constructor() {
|
|
cockpit.event_target(this);
|
|
this.plots = { };
|
|
this.zoom_state = null;
|
|
}
|
|
|
|
_reset_plots(x_range, x_stop) {
|
|
for (const id in this.plots) {
|
|
const p = this.plots[id]._plot;
|
|
p.stop_walking();
|
|
p.reset(x_range, x_stop);
|
|
p.refresh();
|
|
if (x_stop === undefined)
|
|
p.start_walking();
|
|
}
|
|
}
|
|
|
|
_check_archives(plot) {
|
|
if (!this.zoom_state && plot.archives) {
|
|
this.zoom_state = new ZoomState(this._reset_plots.bind(this));
|
|
this.dispatchEvent("changed");
|
|
}
|
|
}
|
|
|
|
_get(id) {
|
|
if (this.plots[id])
|
|
return this.plots[id];
|
|
|
|
const ps = new SinglePlotState();
|
|
ps._plot.addEventListener("changed", () => this._check_archives(ps._plot));
|
|
this._check_archives(ps._plot);
|
|
ps._plot.addEventListener("plot", (event, data) => {
|
|
ps.data = data;
|
|
this.dispatchEvent("plot:" + id);
|
|
});
|
|
|
|
this.plots[id] = ps;
|
|
return ps;
|
|
}
|
|
|
|
plot_single(id, metric) {
|
|
const ps = this._get(id);
|
|
ps.plot_single(metric);
|
|
}
|
|
|
|
plot_instances(id, metric, insts, reset) {
|
|
this._get(id).plot_instances(metric, insts, reset);
|
|
}
|
|
|
|
data(id) {
|
|
return this.plots[id] && this.plots[id].data;
|
|
}
|
|
}
|