238 lines
6.3 KiB
JavaScript
238 lines
6.3 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, false)
|
||
|
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
|
||
|
}
|
||
|
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 = 600
|
||
|
this.canvas.height = 500
|
||
|
|
||
|
// 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, xStep, 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,
|
||
|
}
|