portal

Landing page for your homelab
git clone https://git.woozle.org/neale/portal.git

portal / web
Neale Pickett  ·  2023-03-13

stat.mjs

  1const τ = Math.PI * 2
  2const Millisecond = 1
  3const Second = 1000 * Millisecond
  4
  5/**
  6 * Stat keeps track of CPU usage since the last update
  7 */
  8class Stat {
  9    constructor() {
 10        this.Stat = {}
 11        this.LastStat = {}
 12    }
 13    
 14    async update() {
 15        let resp = await fetch("proc/stat")
 16        let stext = await resp.text()
 17        let lines = stext.split("\n")
 18        this.Date = new Date(resp.headers.get("Date"))
 19        this.LastStat = this.Stat
 20        this.Stat = {}
 21        for (let line of lines) {
 22            let parts = line.split(/\s+/)
 23            let key = parts.shift()
 24            let vals = parts.map(Number)
 25            this.Stat[key] = vals
 26        }
 27    }
 28
 29    /**
 30     * Compute CPU use since last update
 31     * 
 32     * @param {Number} n CPU to look at. Leave blank for a summary of all CPUs.
 33     * @returns Object with CPU usages (0.0-1.0)
 34     */
 35    cpu(n="") {
 36        let key = `cpu${n}`
 37        let vals = this.Stat[key]
 38        let prev = this.LastStat[key]
 39        if (!vals || !prev) {
 40            return {}
 41        }
 42        let total = vals.reduce((a,b)=>a+b) - prev.reduce((a,b)=>a+b)
 43        return {
 44            user: (vals[0] - prev[0]) / total,
 45            nice: (vals[1] - prev[1]) / total,
 46            sys: (vals[2] - prev[2]) / total,
 47            idle: (vals[3] - prev[3]) / total,
 48            wait: (vals[4] - prev[4]) / total,
 49            irq: (vals[5] - prev[5]) / total,
 50            softirq: (vals[6] - prev[6]) / total,
 51            steal: (vals[7] - prev[7]) / total,
 52            guest: (vals[8] - prev[8]) / total,
 53            guestNice: (vals[9] - prev[9]) / total,
 54        }
 55    }
 56}
 57
 58/**
 59 * Chart defines common functionality for any chart
 60 */
 61class Chart {
 62    constructor(element, stat) {
 63        this.stat = stat
 64        this.canvas = element.appendChild(document.createElement("canvas"))
 65        this.ctx = this.canvas.getContext("2d")
 66        this.colors = ["SkyBlue", "SeaGreen", "Gold", "Tomato"]
 67        this.labels = ["u", "n", "s", "w"]
 68    }
 69
 70    cpu() {
 71        let cpu = this.stat.cpu()
 72        return [cpu.user, cpu.nice, cpu.sys, cpu.wait + cpu.irq + cpu.softirq]
 73    }
 74
 75    clear() {        
 76        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
 77    }
 78}
 79
 80/**
 81 * PieChart makes a little CPU usage pie chart, with idle time transparent.
 82 */
 83class PieChart extends Chart{
 84    /**
 85     * 
 86     * @param {Element} element 
 87     * @param {Stat} stat 
 88     */
 89    constructor(element, stat) {
 90        super(element, stat)
 91        this.r = 250
 92        this.θ = 0
 93        this.canvas.width = this.r*2
 94        this.canvas.height = this.r*2
 95    }
 96
 97    async update() {
 98        let values = this.cpu()
 99        let θ = -τ/4
100
101        this.clear()
102
103        this.ctx.save()
104        this.ctx.translate(this.r, this.r)
105        this.ctx.font = `bold ${this.r/3}px sans-serif`
106        for (let i in values) {
107            let angle = values[i] * τ
108
109            this.ctx.fillStyle = this.colors[i]
110            this.ctx.beginPath()
111            this.ctx.arc(0, 0, this.r, θ, θ + angle)
112            this.ctx.lineTo(0, 0)
113            this.ctx.fill()
114
115            if (angle > τ/12) {
116                let mid = θ + angle/2
117                this.ctx.save()
118                this.ctx.rotate(mid)
119                this.ctx.translate(this.r*0.7, 0)
120                this.ctx.rotate(-mid)
121                this.ctx.fillStyle = "black"
122                this.ctx.textBaseline = "middle"
123                this.ctx.textAlign = "center"
124                this.ctx.fillText(this.labels[i], 0, 0)
125                this.ctx.restore()
126            }
127            θ += angle
128        }
129        // Put a border around it
130        this.ctx.strokeStyle = "silver"
131        this.ctx.lineWidth = 5
132        this.ctx.beginPath()
133        this.ctx.arc(0, 0, this.r - this.ctx.lineWidth/2, 0, τ)
134        this.ctx.stroke()
135
136        this.ctx.restore()
137    }
138}
139
140/**
141 * Area chart makes a packed, stacked bar chart of historical CPU usage.
142 */
143class AreaChart extends Chart {
144    /**
145     * 
146     * @param {Element} element 
147     * @param {Stat} stat 
148     * @param {Number} historyLen How many historical values to keep
149     */
150    constructor(element, stat, historyLen=300) {
151        super(element, stat)
152        this.historyLen = historyLen
153        this.history = Array(this.historyLen) // Make it fill in from the right
154        this.canvas.width = 1920
155        this.canvas.height = 1080
156
157        // Cartesian coordinates
158        this.ctx.translate(0, this.canvas.height)
159        this.ctx.scale(1, -1)
160    }
161
162    async update() {
163        let xStep = this.canvas.width / this.historyLen
164        let yStep = this.canvas.height / 1
165
166        this.history.push(this.cpu())
167        while (this.history.length > this.historyLen) {
168            this.history.shift()
169        }
170
171        this.clear()
172        for (let y = 0; y < this.canvas.height; y += 0.25*this.canvas.height) {
173            this.ctx.strokeStyle = "silver"
174            this.ctx.beginPath()
175            this.ctx.moveTo(0, y)
176            this.ctx.lineTo(this.canvas.width, y)
177            this.ctx.stroke()
178        }
179        for (let i in this.history) {
180            let h = this.history[i]
181            if (!h) {
182                continue
183            }
184            let x = i * xStep
185            let y = 0
186            for (let j in h) {
187                let val = h[j]
188                this.ctx.beginPath()
189                this.ctx.fillStyle = this.colors[j]
190                this.ctx.rect(x, y, Math.ceil(xStep), Math.ceil(yStep * val))
191                this.ctx.fill()
192                y += yStep * val
193            }
194        }
195    }
196}
197
198/**
199 * AutoStat automatically sets up any pie and area charts in the document.
200 *
201 * You can enable this automatically by providing <body dataset-autostat>
202 */
203class AutoStat {
204    constructor() {
205        this.stat = new Stat()
206        this.charts = []
207        for (let e of document.querySelectorAll(".pie.chart")) {
208            this.charts.push(new PieChart(e, this.stat))
209        }
210        for (let e of document.querySelectorAll(".area.chart")) {
211            this.charts.push(new AreaChart(e, this.stat))
212        }
213
214        setInterval(()=>this.update(), 2 * Second)
215        this.update()
216        setTimeout(()=>this.update(), 500 * Millisecond)
217    }
218
219    async update() {
220        await this.stat.update()
221        for (let c of this.charts) {
222            c.update()
223        }
224    }
225}
226
227function init() {
228    if (document.body.dataset.autostat != undefined) {
229        new AutoStat()
230    }
231}
232
233if (document.readyState === "loading") {
234    document.addEventListener("DOMContentLoaded", init)
235} else {
236    init()
237}
238
239
240export {
241    Stat,
242    PieChart,
243    AreaChart,
244    AutoStat,
245}