portal/web/stat.mjs

246 lines
6.5 KiB
JavaScript

const τ = Math.PI * 2
const Millisecond = 1
const Second = 1000 * Millisecond
/**
* Stat keeps track of CPU usage since the last update
*/
class Stat {
constructor() {
this.Stat = {}
this.LastStat = {}
}
async update() {
let resp = await fetch("proc/stat")
let stext = await resp.text()
let lines = stext.split("\n")
this.Date = new Date(resp.headers.get("Date"))
this.LastStat = this.Stat
this.Stat = {}
for (let line of lines) {
let parts = line.split(/\s+/)
let key = parts.shift()
let vals = parts.map(Number)
this.Stat[key] = vals
}
}
/**
* Compute CPU use since last update
*
* @param {Number} n CPU to look at. Leave blank for a summary of all CPUs.
* @returns Object with CPU usages (0.0-1.0)
*/
cpu(n="") {
let key = `cpu${n}`
let vals = this.Stat[key]
let prev = this.LastStat[key]
if (!vals || !prev) {
return {}
}
let total = vals.reduce((a,b)=>a+b) - prev.reduce((a,b)=>a+b)
return {
user: (vals[0] - prev[0]) / total,
nice: (vals[1] - prev[1]) / total,
sys: (vals[2] - prev[2]) / total,
idle: (vals[3] - prev[3]) / total,
wait: (vals[4] - prev[4]) / total,
irq: (vals[5] - prev[5]) / total,
softirq: (vals[6] - prev[6]) / total,
steal: (vals[7] - prev[7]) / total,
guest: (vals[8] - prev[8]) / total,
guestNice: (vals[9] - prev[9]) / total,
}
}
}
/**
* Chart defines common functionality for any chart
*/
class Chart {
constructor(element, stat) {
this.stat = stat
this.canvas = element.appendChild(document.createElement("canvas"))
this.ctx = this.canvas.getContext("2d")
this.colors = ["SkyBlue", "SeaGreen", "Gold", "Tomato"]
this.labels = ["u", "n", "s", "w"]
}
cpu() {
let cpu = this.stat.cpu()
return [cpu.user, cpu.nice, cpu.sys, cpu.wait]
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
}
/**
* PieChart makes a little CPU usage pie chart, with idle time transparent.
*/
class PieChart extends Chart{
/**
*
* @param {Element} element
* @param {Stat} stat
*/
constructor(element, stat) {
super(element, stat)
this.r = 250
this.θ = 0
this.canvas.width = this.r*2
this.canvas.height = this.r*2
}
async update() {
let values = this.cpu()
let θ = -τ/4
this.clear()
this.ctx.save()
this.ctx.translate(this.r, this.r)
this.ctx.font = `bold ${this.r/3}px sans-serif`
for (let i in values) {
let angle = values[i] * τ
this.ctx.fillStyle = this.colors[i]
this.ctx.beginPath()
this.ctx.arc(0, 0, this.r, θ, θ + angle)
this.ctx.lineTo(0, 0)
this.ctx.fill()
if (angle > τ/12) {
let mid = θ + angle/2
this.ctx.save()
this.ctx.rotate(mid)
this.ctx.translate(this.r*0.7, 0)
this.ctx.rotate(-mid)
this.ctx.fillStyle = "black"
this.ctx.textBaseline = "middle"
this.ctx.textAlign = "center"
this.ctx.fillText(this.labels[i], 0, 0)
this.ctx.restore()
}
θ += angle
}
// Put a border around it
this.ctx.strokeStyle = "silver"
this.ctx.lineWidth = 5
this.ctx.beginPath()
this.ctx.arc(0, 0, this.r - this.ctx.lineWidth/2, 0, τ)
this.ctx.stroke()
this.ctx.restore()
}
}
/**
* Area chart makes a packed, stacked bar chart of historical CPU usage.
*/
class AreaChart extends Chart {
/**
*
* @param {Element} element
* @param {Stat} stat
* @param {Number} historyLen How many historical values to keep
*/
constructor(element, stat, historyLen=300) {
super(element, stat)
this.historyLen = historyLen
this.history = Array(this.historyLen) // Make it fill in from the right
this.canvas.width = 1920
this.canvas.height = 1080
// Cartesian coordinates
this.ctx.translate(0, this.canvas.height)
this.ctx.scale(1, -1)
}
async update() {
let xStep = this.canvas.width / this.historyLen
let yStep = this.canvas.height / 1
this.history.push(this.cpu())
while (this.history.length > this.historyLen) {
this.history.shift()
}
this.clear()
for (let y = 0; y < this.canvas.height; y += 0.25*this.canvas.height) {
this.ctx.strokeStyle = "silver"
this.ctx.beginPath()
this.ctx.moveTo(0, y)
this.ctx.lineTo(this.canvas.width, y)
this.ctx.stroke()
}
for (let i in this.history) {
let h = this.history[i]
if (!h) {
continue
}
let x = i * xStep
let y = 0
for (let j in h) {
let val = h[j]
this.ctx.beginPath()
this.ctx.fillStyle = this.colors[j]
this.ctx.rect(x, y, Math.ceil(xStep), Math.ceil(yStep * val))
this.ctx.fill()
y += yStep * val
}
}
}
}
/**
* AutoStat automatically sets up any pie and area charts in the document.
*
* You can enable this automatically by providing <body dataset-autostat>
*/
class AutoStat {
constructor() {
this.stat = new Stat()
this.charts = []
for (let e of document.querySelectorAll(".pie.chart")) {
this.charts.push(new PieChart(e, this.stat))
}
for (let e of document.querySelectorAll(".area.chart")) {
this.charts.push(new AreaChart(e, this.stat))
}
setInterval(()=>this.update(), 2 * Second)
this.update()
setTimeout(()=>this.update(), 500 * Millisecond)
}
async update() {
await this.stat.update()
for (let c of this.charts) {
c.update()
}
}
}
function init() {
if (document.body.dataset.autostat != undefined) {
new AutoStat()
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
Stat,
PieChart,
AreaChart,
AutoStat,
}