|
|
@@ -36,7 +36,7 @@ import { systemLiveJs } from "../system-live.ts"; |
| 36 | 36 |
import { RECENT_BYTES, recentEncodings } from "../../http/compress.ts"; |
| 37 | 37 |
import { KEPT_PAGE_BYTES, keptPageStats } from "./kept_page.ts"; |
| 38 | 38 |
import { COLUMN_BYTES, PARTS_BYTES, keptPartsStats, treeCacheStats } from "./tree_cache.ts"; |
| 39 | |
import { badge, card, chartBox, empty, meter, pill, row } from "../ui.ts"; |
| 39 |
import { badge, chartBox, meter, pill, row } from "../ui.ts"; |
| 40 | 40 |
|
| 41 | 41 |
|
| 42 | 42 |
const memoryOf = (g: { rssBytes: number; capBytes: number | null; capBy: "kernel" | "watch" | null }): Html => |
|
|
@@ -68,6 +68,7 @@ const figureRow = (name: string, figure: string, data: Record<string, string> = |
| 68 | 68 |
const KIND_ORDER = ["forge", "crew", "check", "app", "host"] as const; |
| 69 | 69 |
function procTable(v: SystemView): Html { |
| 70 | 70 |
const groups = KIND_ORDER.flatMap((k) => v.groups.filter((g) => g.kind === k)); |
| 71 |
if (groups.length === 0) return html`<p class="muted">No processes were read.</p>`; |
| 71 | 72 |
return html`<div class="table-scroll"><table class="table" data-dense aria-label="Processes by what launched them"> |
| 72 | 73 |
<thead><tr><th scope="col">pid</th><th scope="col">command</th><th scope="col" class="num">cpu</th><th scope="col" class="num">mem</th><th scope="col" class="num">age</th></tr></thead> |
| 73 | 74 |
${groups.map( |
|
|
@@ -91,17 +92,14 @@ function memorySection(ctx: Ctx): Html { |
| 91 | 92 |
if (!canSeeMemory(ctx.auth.user)) return html``; |
| 92 | 93 |
const hours = timePhase(ctx.request, "memory", () => memoryHours(ctx.app.db, Date.now())); |
| 93 | 94 |
const series = (title: string, pick: (h: (typeof hours)[number]) => number) => |
| 94 | |
card( |
| 95 | |
chartBox("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title), |
| 96 | |
{ head: html`<h2>${title}</h2>` }, |
| 97 | |
); |
| 98 | |
return hours.length === 0 |
| 99 | |
? empty({ icon: "activity", title: "No samples yet", note: "The forge samples its memory every minute; the first hour appears shortly." }) |
| 95 |
html`<div><h3 class="small">${title}</h3>${chartBox("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title)}</div>`; |
| 96 |
return html`<section class="settings-block" id="sys-memory"><h2>Forge memory</h2>${hours.length === 0 |
| 97 |
? html`<p class="muted">No samples yet: the forge samples its memory every minute, so the first hour appears shortly.</p>` |
| 100 | 98 |
: html`<div class="plots">${series("RSS (MiB, hourly mean)", (h) => h.rssMean)}${series("JS heap (MiB, hourly mean)", (h) => h.heapMean)}</div> |
| 101 | 99 |
<div class="table-scroll"><table class="table" aria-label="Forge memory by hour"> |
| 102 | 100 |
<thead><tr><th scope="col">Hour (UTC)</th><th scope="col" class="num">RSS mean</th><th scope="col" class="num">RSS max</th><th scope="col" class="num">Heap mean</th><th scope="col" class="num">Heap max</th></tr></thead> |
| 103 | 101 |
<tbody>${hours.map((h) => html`<tr><td class="mono">${hhmm(h.at)}</td><td class="num mono">${mib(h.rssMean)}</td><td class="num mono">${mib(h.rssMax)}</td><td class="num mono">${mib(h.heapMean)}</td><td class="num mono">${mib(h.heapMax)}</td></tr>`)}</tbody> |
| 104 | |
</table></div>`; |
| 102 |
</table></div>`}</section>`; |
| 105 | 103 |
} |
| 106 | 104 |
|
| 107 | 105 |
/** One in-process cache as it stands: what it is for, its byte cap, and `ViewCache.stats()`. */ |
|
|
@@ -126,40 +124,36 @@ export const pageCaches = (): CacheRow[] => [ |
| 126 | 124 |
/** `pageCaches`' rows as a table; the hit rate is hits over hits and misses since the process started. */ |
| 127 | 125 |
export function cachesSection(rows: readonly CacheRow[]): Html { |
| 128 | 126 |
const rate = (r: CacheRow) => (r.hits + r.misses === 0 ? "—" : `${((100 * r.hits) / (r.hits + r.misses)).toFixed(1)}%`); |
| 129 | |
return card( |
| 130 | |
html`<p class="small muted">In-process caches behind strangers' code pages, since this process started. Bytes are what each counts against its cap.</p> |
| 127 |
return html`<section class="settings-block"><h2>Code page caches</h2> |
| 128 |
<p class="small muted">Counts since this process started.</p> |
| 131 | 129 |
<div class="table-scroll"><table class="table" aria-label="Code page caches"> |
| 132 | 130 |
<thead><tr><th scope="col">Cache</th><th scope="col">Holds</th><th scope="col" class="num">Entries</th><th scope="col" class="num">Bytes</th><th scope="col" class="num">Cap</th><th scope="col" class="num">Hits</th><th scope="col" class="num">Misses</th><th scope="col" class="num">Hit rate</th></tr></thead> |
| 133 | 131 |
<tbody>${rows.map((r) => html`<tr data-cache="${r.name}" data-bytes="${String(r.bytes)}"><td>${r.name}</td><td class="small muted">${r.holds}</td><td class="num mono">${String(r.entries)}</td><td class="num mono">${mib(r.bytes)}</td><td class="num mono">${mib(r.capBytes)}</td><td class="num mono">${String(r.hits)}</td><td class="num mono">${String(r.misses)}</td><td class="num mono">${rate(r)}</td></tr>`)}</tbody> |
| 134 | |
</table></div>`, |
| 135 | |
{ head: html`<h2>Code page caches</h2>` }, |
| 136 | |
); |
| 132 |
</table></div></section>`; |
| 137 | 133 |
} |
| 138 | 134 |
|
| 139 | 135 |
/** The last hour's stalls by route, most milliseconds first; the rows are `stallHour`'s. */ |
| 140 | 136 |
export function stallSection(rows: readonly RouteStall[]): Html { |
| 141 | |
return card( |
| 142 | |
html`<p class="small muted">Milliseconds the main thread was held past 250 ms in the last hour, charged in full to every route running across the stall (<code>none</code>: no request was). As of this page's load; <code>/metrics</code> has the series.</p> |
| 143 | |
<div class="table-scroll"><table class="table" aria-label="Main-thread stalls by route, last hour"> |
| 137 |
return html`<section class="settings-block"><h2>Main-thread stalls, last hour</h2> |
| 138 |
<p class="small muted">Past 250 ms, as of this page's load; <code>/metrics</code> has the series.</p>${ |
| 139 |
rows.length === 0 |
| 140 |
? html`<p class="muted">No stalls in the last hour.</p>` |
| 141 |
: html`<div class="table-scroll"><table class="table" aria-label="Main-thread stalls by route, last hour"> |
| 144 | 142 |
<thead><tr><th scope="col">Route</th><th scope="col" class="num">Stall ms</th><th scope="col" class="num">Stalls</th></tr></thead> |
| 145 | 143 |
<tbody>${rows.map((r) => html`<tr data-route="${r.route}"><td class="mono">${r.route}</td><td class="num mono">${String(Math.round(r.ms))}</td><td class="num mono">${String(r.count)}</td></tr>`)}</tbody> |
| 146 | |
</table></div>`, |
| 147 | |
{ head: html`<h2>Main-thread stalls, last hour</h2>` }, |
| 148 | |
); |
| 144 |
</table></div>`}</section>`; |
| 149 | 145 |
} |
| 150 | 146 |
|
| 151 | 147 |
/** Who read the telemetry routes in the last 10 minutes and how fast (#9069); `readerPaces`' rows. */ |
| 152 | 148 |
export function readersSection(rows: readonly ReaderPace[]): Html { |
| 153 | 149 |
const secs = (ms: number | null): string => (ms === null ? "—" : `${(ms / 1000).toFixed(1)} s`); |
| 154 | |
return card( |
| 150 |
return html`<section class="settings-block"><h2>Telemetry readers</h2>${ |
| 155 | 151 |
rows.length === 0 |
| 156 | 152 |
? html`<p class="muted">No telemetry reads in the last 10 minutes.</p>` |
| 157 | 153 |
: html`<div class="table-scroll"><table class="table" aria-label="Telemetry readers, last 10 minutes"> |
| 158 | 154 |
<thead><tr><th scope="col">Route</th><th scope="col">Reader</th><th scope="col" class="num">Reads, 10 min</th><th scope="col" class="num">Mean interval</th><th scope="col"></th></tr></thead> |
| 159 | 155 |
<tbody>${rows.map((r) => html`<tr data-route="${r.route}" data-reader="${r.reader}"><td class="mono">${r.route}</td><td class="mono">${r.reader}</td><td class="num mono">${String(r.reads)}</td><td class="num mono">${secs(r.meanIntervalMs)}</td><td>${r.tooFast ? badge("faster than the flow copy changes", { tone: "danger" }) : null}</td></tr>`)}</tbody> |
| 160 | |
</table></div>`, |
| 161 | |
{ head: html`<h2>Telemetry readers</h2>` }, |
| 162 | |
); |
| 156 |
</table></div>`}</section>`; |
| 163 | 157 |
} |
| 164 | 158 |
|
| 165 | 159 |
/** |
|
|
@@ -193,14 +187,12 @@ export function regions(v: SystemView): Record<string, string> { |
| 193 | 187 |
: null} |
| 194 | 188 |
${v.load ? figureRow("load", v.load.map((l) => l.toFixed(2)).join(" ")) : null}`, |
| 195 | 189 |
temps: html`${v.temps.map((t) => `${t.chip} ${t.label} ${t.celsius.toFixed(0)}°C`).join(" · ")}`, |
| 196 | |
"sys-disks": card( |
| 197 | |
html`<ul class="list"> |
| 198 | |
${v.disks.map((d) => meterRow(d.mount, d.usedBytes, d.totalBytes, `${bytes(d.usedBytes)} / ${bytes(d.totalBytes)} ${d.fs}`))} |
| 190 |
"sys-disks": |
| 191 |
v.disks.length + v.io.length + v.net.length === 0 |
| 192 |
? html`<li class="muted">No disks or network were read.</li>` |
| 193 |
: html`${v.disks.map((d) => meterRow(d.mount, d.usedBytes, d.totalBytes, `${bytes(d.usedBytes)} / ${bytes(d.totalBytes)} ${d.fs}`))} |
| 199 | 194 |
${v.io.map((d) => figureRow(d.name, `R ${rateOf(d.readBytesPerSecond)} W ${rateOf(d.writeBytesPerSecond)}`, { io: d.name }))} |
| 200 | |
${v.net.map((n) => figureRow(n.name, `↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}`, { net: n.name }))} |
| 201 | |
</ul>`, |
| 202 | |
{ head: html`<h2>Disks and network</h2>`, pad: false }, |
| 203 | |
), |
| 195 |
${v.net.map((n) => figureRow(n.name, `↓ ${rateOf(n.rxBytesPerSecond)} ↑ ${rateOf(n.txBytesPerSecond)}`, { net: n.name }))}`, |
| 204 | 196 |
"sys-procs": procTable(v), |
| 205 | 197 |
}; |
| 206 | 198 |
return Object.fromEntries(Object.entries(out).map(([k, h]) => [k, h.text])); |
|
|
@@ -249,19 +241,18 @@ export function systemPage(ctx: Ctx): Response { |
| 249 | 241 |
</p> |
| 250 | 242 |
</div> |
| 251 | 243 |
<div id="sys-dup">${at("sys-dup")}</div> |
| 252 | |
${card( |
| 253 | |
html`<div id="summary" class="row-meta">${at("summary")}</div> |
| 244 |
<section class="settings-block"><h2>Machine</h2> |
| 245 |
<div id="summary" class="row-meta">${at("summary")}</div> |
| 254 | 246 |
<ul class="list" id="cores" aria-label="Per-core CPU, percent, with its last readings">${at("cores")}</ul> |
| 255 | 247 |
<ul class="list" id="mem">${at("mem")}</ul> |
| 256 | 248 |
${caps} |
| 257 | |
<p class="small mono" id="temps">${at("temps")}</p>`, |
| 258 | |
{ head: html`<h2>Machine</h2>` }, |
| 259 | |
)} |
| 260 | |
<div id="sys-disks">${at("sys-disks")}</div> |
| 261 | |
<div class="page-head"><h2>Processes</h2> |
| 262 | |
<p class="small muted"><span class="js-only">Sort by <button class="btn" data-size="sm" type="button" data-sort="cpu" aria-pressed="true"><kbd>c</kbd> CPU</button> |
| 263 | |
<button class="btn" data-size="sm" type="button" data-sort="mem" aria-pressed="false"><kbd>m</kbd> memory</button></span></p></div> |
| 264 | |
<div id="sys-procs">${at("sys-procs")}</div> |
| 249 |
<p class="small mono" id="temps">${at("temps")}</p></section> |
| 250 |
<section class="settings-block"><h2>Disks and network</h2> |
| 251 |
<ul class="list" id="sys-disks">${at("sys-disks")}</ul></section> |
| 252 |
<section class="settings-block"><h2>Processes</h2> |
| 253 |
<div class="js-only small muted">Sort by <button class="btn" data-size="sm" type="button" data-sort="cpu" aria-pressed="true"><kbd>c</kbd> CPU</button> |
| 254 |
<button class="btn" data-size="sm" type="button" data-sort="mem" aria-pressed="false"><kbd>m</kbd> memory</button></div> |
| 255 |
<div id="sys-procs">${at("sys-procs")}</div></section> |
| 265 | 256 |
<div id="sys-stalls">${stallSection(stalls)}</div> |
| 266 | 257 |
<div id="sys-readers">${readersSection(readerPaces())}</div> |
| 267 | 258 |
<div id="sys-caches">${cachesSection(pageCaches())}</div> |