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 */ 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, }