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}