Files

206 lines
7.0 KiB
JavaScript

'use strict';
'require baseclass';
'require view.dashboard.lib.charts as charts';
'require view.dashboard.lib.history as history';
'require view.dashboard.lib.system as system';
// The first eight fields of a /proc/stat cpu line. Guest time is already
// part of user and nice, so it must not be added again.
const CPUS = 2, MEMORY = 3;
const USER = 0, NICE = 1, SYSTEM = 2, IDLE = 3, IOWAIT = 4, IRQ = 5, SOFTIRQ = 6, STEAL = 7;
// Shares of the time between two samples, in percent. Both are in jiffies,
// so no clock is involved. Waiting for I/O is idle time. The iowait counter
// may step back, hence the clamp.
function usage(prev, cur) {
const d = cur.map((value, i) => Math.max(0, value - (prev[i] || 0)));
const total = d.reduce((sum, value) => sum + value, 0);
if (total <= 0)
return null;
const share = value => 100 * value / total;
return {
busy: share(total - d[IDLE] - d[IOWAIT]),
user: share(d[USER] + d[NICE]),
system: share(d[SYSTEM]),
iowait: share(d[IOWAIT]),
irq: share(d[IRQ]),
softirq: share(d[SOFTIRQ]),
steal: share(d[STEAL])
};
}
function progressbar(percent, text) {
return E('div', { 'class': 'cbi-progressbar', 'title': text }, [
E('div', { 'style': 'width:%.2f%%'.format(Math.max(0, Math.min(100, percent))) })
]);
}
return baseclass.extend({
widgets: [
{ id: 'cpu', slot: 'cards', title: _('CPU usage'), order: 20 },
{ id: 'memory', slot: 'cards', title: _('Memory'), order: 30 },
{ id: 'load', slot: 'cards', title: _('Load average'), order: 80, hidden: true },
{ id: 'system', slot: 'charts', title: _('System load'), order: 20 },
{ id: 'resources', slot: 'tabs', title: _('Resources'), order: 30 }
],
load() {
return Promise.all([ system.info(), history.load() ]);
},
// Per-CPU usage over the latest interval, keyed by the kernel's names so
// that a CPU going offline does not shift the others.
cpu() {
const pair = history.pair();
const result = {};
if (pair == null)
return null;
for (const name in pair[1][CPUS])
if (pair[0][CPUS][name] != null)
result[name] = usage(pair[0][CPUS][name], pair[1][CPUS][name]);
return (result.cpu != null) ? result : null;
},
memory(info) {
const mem = L.isObject(info.memory) ? info.memory : {};
if (!mem.total)
return null;
// What the kernel reckons can be had without swapping. Counting
// everything but "free" as used would include the page cache.
const available = (mem.available != null) ? mem.available : (mem.free || 0) + (mem.buffered || 0) + (mem.cached || 0);
const used = Math.max(0, mem.total - available);
return { total: mem.total, used: used, percent: 100 * used / mem.total, buffered: mem.buffered, cached: mem.cached };
},
renderCpuCard(cpu, cores, load) {
return charts.kpi({
icon: 'cpu',
title: _('CPU usage'),
value: [ cpu ? '%d%%'.format(Math.round(cpu.cpu.busy)) : '-' ],
sub: cpu ? [ N_(cores, '%d core', '%d cores').format(cores), load ? _('load %.2f').format(load[0]) : null ]
: [ E('em', {}, [ history.status() ]) ]
});
},
renderMemoryCard(mem) {
return charts.kpi({
icon: 'memory',
title: _('Memory'),
value: [ mem ? '%d%%'.format(Math.round(mem.percent)) : '-' ],
sub: [ mem ? '%1024.1mB / %1024.1mB'.format(mem.used, mem.total) : '' ]
});
},
renderLoadCard(load) {
return charts.kpi({
icon: 'load',
title: _('Load average'),
value: [ load ? '%.2f'.format(load[0]) : '-' ],
sub: load ? [ _('5 min: %.2f').format(load[1]), _('15 min: %.2f').format(load[2]) ] : []
});
},
renderChart(cores, mem) {
const busy = history.series((cur, prev) => {
const share = (prev != null && prev[CPUS].cpu != null && cur[CPUS].cpu != null) ? usage(prev[CPUS].cpu, cur[CPUS].cpu) : null;
return share ? share.busy : null;
});
const used = history.series(cur => (mem != null && cur[MEMORY] != null) ? 100 * (1 - cur[MEMORY] / mem.total) : null);
const last = points => points.length ? points[points.length - 1].v : null;
const percent = value => (value != null) ? '%d%%'.format(Math.round(value)) : '-';
const desc = [];
if (cores)
desc.push(N_(cores, '%d core', '%d cores').format(cores));
if (mem)
desc.push('%1024.0mB'.format(mem.total));
return charts.card({
title: _('System load'),
desc: desc.join(' · '),
body: busy.length ? [
charts.lines({
span: history.span(),
max: 100,
ticks: [ 0, 25, 50, 75, 100 ].map(value => ({ value: value, label: '%d%%'.format(value) })),
ariaLabel: _('System load'),
series: [ { values: busy, area: true }, { values: used } ]
}),
charts.legend([
{ className: 'dashboard-series-1', label: _('CPU'), value: percent(last(busy)) },
{ className: 'dashboard-series-2', label: _('Memory'), value: percent(last(used)) }
])
] : charts.empty(history.status())
});
},
renderTab(cpu, mem, load, swap) {
const rows = [];
const pending = () => E('em', {}, [ history.status() ]);
const bytes = (value, total) => progressbar(100 * value / total, '%1024.1mB / %1024.1mB (%d%%)'.format(value, total, Math.round(100 * value / total)));
if (load)
rows.push([ _('Load average'), '%.2f, %.2f, %.2f'.format(load[0], load[1], load[2]) ]);
const core = (title, share) => [
E('span', { 'data-tooltip': 'user %.1f · system %.1f · iowait %.1f · irq %.1f · softirq %.1f · steal %.1f'.format(
share.user, share.system, share.iowait, share.irq, share.softirq, share.steal) }, [ title ]),
progressbar(share.busy, '%.1f%%'.format(share.busy))
];
rows.push(cpu ? core(_('CPU usage'), cpu.cpu) : [ _('CPU usage'), pending() ]);
if (cpu && Object.keys(cpu).length > 2)
Object.keys(cpu).filter(name => name != 'cpu' && cpu[name] != null).forEach(name => {
rows.push(core(name.replace(/^cpu/, _('CPU') + ' '), cpu[name]));
});
if (mem) {
rows.push([ _('Memory used'), bytes(mem.used, mem.total) ]);
if (mem.buffered)
rows.push([ _('Buffered'), bytes(mem.buffered, mem.total) ]);
if (mem.cached)
rows.push([ _('Cached'), bytes(mem.cached, mem.total) ]);
}
if (swap && swap.total > 0)
rows.push([ _('Swap used'), bytes(swap.total - swap.free, swap.total) ]);
return E('table', { 'class': 'table' }, rows.map(row => E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, [ row[0] ]),
E('td', { 'class': 'td left' }, [ row[1] ])
])));
},
render([info]) {
const cpu = this.cpu();
const latest = history.samples[history.samples.length - 1];
const cores = latest ? Math.max(1, Object.keys(latest[CPUS]).length - 1) : 0;
const mem = this.memory(info);
const load = Array.isArray(info.load) ? info.load.map(value => value / 65536) : null;
return {
cards: [
{ id: 'cpu', node: () => this.renderCpuCard(cpu, cores, load) },
{ id: 'memory', node: () => this.renderMemoryCard(mem) },
{ id: 'load', node: () => this.renderLoadCard(load) }
],
charts: [ { id: 'system', node: () => this.renderChart(cores, mem) } ],
tabs: [ { id: 'resources', title: _('Resources'), content: () => this.renderTab(cpu, mem, load, info.swap) } ]
};
}
});