Top: a graph's past holds still — columns draw the peak of their own fixed slice of history
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
be0f2a5aaf
commit
ec94cb8bff
+17
-17
@@ -10,6 +10,7 @@ import { fetchCatalog, fetchHistories, useSensorStream } from './sensors'
|
||||
import { fmtMs, nextPoint, REFRESH_OPTIONS, setRefreshMs, strideMs, useHistorySec, useRefreshMs } from './prefs'
|
||||
import { Loading, askYes, toast } from './ui'
|
||||
import { Theme, currentThemeId, themeById } from './themes'
|
||||
import { columnsOf } from './graphcols'
|
||||
import { savePrefs } from './accents'
|
||||
|
||||
const MAX = 300 // points kept per curve, as the machines keep them
|
||||
@@ -67,7 +68,7 @@ function at(g: readonly string[], t: number): string {
|
||||
|
||||
// ---- the dot graph ----
|
||||
// One dot column per sample, filled from the bottom, each dot coloured by how high it sits.
|
||||
function Graph({ values, tick, cap, max, grad, flip = false }: { values: number[]; tick: number; cap: number; max?: number; grad: GradName; flip?: boolean }) {
|
||||
function Graph({ values, tick, advance, cap, max, grad, flip = false }: { values: number[]; tick: number; advance: number; cap: number; max?: number; grad: GradName; flip?: boolean }) {
|
||||
const ref = useRef<HTMLCanvasElement>(null)
|
||||
const box = useRef<HTMLDivElement>(null)
|
||||
const pal = useContext(Pal)
|
||||
@@ -89,14 +90,9 @@ function Graph({ values, tick, cap, max, grad, flip = false }: { values: number[
|
||||
const step = 3, rows = Math.max(1, Math.floor(height / step)), cols = Math.max(1, Math.floor(w / step))
|
||||
// The box is the whole window, always: `cap` points wide (what the window holds at this
|
||||
// rate) whatever the box measures, so a full history fills it edge to edge and a machine
|
||||
// that has only just started draws in from the right, as btop does — a handful of samples
|
||||
// is not stretched into a bar chart.
|
||||
const n = Math.min(values.length, cap)
|
||||
const seen: number[] = []
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const i = Math.floor((c * cap) / cols) - (cap - n)
|
||||
if (i >= 0) seen.push(values[values.length - n + i])
|
||||
}
|
||||
// that has only just started draws in from the right, as btop does. Which points each
|
||||
// column draws — the peak of its own fixed slice of the history — is graphcols.ts.
|
||||
const seen = columnsOf(values, advance, cap, cols)
|
||||
const top = max ?? Math.max(1, ...seen)
|
||||
const stops = pal[grad]
|
||||
for (let i = 0; i < seen.length; i++) {
|
||||
@@ -110,7 +106,7 @@ function Graph({ values, tick, cap, max, grad, flip = false }: { values: number[
|
||||
g.fillRect(x, y, 2, 2)
|
||||
}
|
||||
}
|
||||
}, [values, tick, cap, w, height, max, grad, flip, pal]) // tick: the curve is appended to in place
|
||||
}, [values, tick, advance, cap, w, height, max, grad, flip, pal]) // tick: the curve is appended to in place
|
||||
return <div className="tg" ref={box} data-n={values.length}><canvas ref={ref} style={{ width: '100%', height: '100%' }} /></div>
|
||||
}
|
||||
|
||||
@@ -196,6 +192,7 @@ function useCurves(live: Values | null, t: number, refreshMs: number, historySec
|
||||
const cur = useRef<Map<string, number[]>>(new Map())
|
||||
const lastT = useRef(0)
|
||||
const binN = useRef(1)
|
||||
const adv = useRef(0) // the number of the newest point, counted since the page opened: what a graph's columns are cut by
|
||||
const [ticks, bump] = useState(0)
|
||||
const seed = useCallback(async () => {
|
||||
try {
|
||||
@@ -215,6 +212,7 @@ function useCurves(live: Values | null, t: number, refreshMs: number, historySec
|
||||
const { fresh, start } = nextPoint(t, lastT.current, strideMs(historySec, refreshMs), refreshMs)
|
||||
binN.current = fresh ? 1 : binN.current + 1
|
||||
lastT.current = start
|
||||
if (fresh) adv.current++
|
||||
const n = binN.current
|
||||
for (const [id, v] of Object.entries(live)) {
|
||||
if (typeof v !== 'number') continue
|
||||
@@ -226,7 +224,7 @@ function useCurves(live: Values | null, t: number, refreshMs: number, historySec
|
||||
}
|
||||
bump((x) => x + 1)
|
||||
}, [live, t, refreshMs, historySec])
|
||||
return [cur.current, ticks] as const
|
||||
return [cur.current, ticks, adv.current] as const
|
||||
}
|
||||
|
||||
// ---- the page ----
|
||||
@@ -248,7 +246,7 @@ export default function Top() {
|
||||
fetchCatalog().then((c) => { catalog.current = new Map(c.map((x) => [x.id, x])) }).catch(() => {}).finally(() => window.setTimeout(() => { refetching.current = false }, 10000))
|
||||
}, [values])
|
||||
const s: Sample | null = useMemo(() => (values && ready ? sampleOf(values, catalog.current) : null), [values, ready])
|
||||
const [curves, tick] = useCurves(values, s?.t ?? 0, refreshMs, historySec, stream.resumed)
|
||||
const [curves, tick, advance] = useCurves(values, s?.t ?? 0, refreshMs, historySec, stream.resumed)
|
||||
|
||||
// the process table, and the CPU model, come from the same place the Processes page reads
|
||||
const [pt, setPt] = useState<ProcTable | null>(null)
|
||||
@@ -321,6 +319,8 @@ export default function Top() {
|
||||
const iface = net[0]
|
||||
const fs = s?.fs ?? []
|
||||
const pct = (a: number, b: number) => (b > 0 ? (a / b) * 100 : 0)
|
||||
// the top of a graph's scale: the peak of what it is drawing — the window, not everything kept
|
||||
const peak = (id: string) => Math.max(1, ...(curves.get(id) ?? []).slice(-cap))
|
||||
|
||||
return (
|
||||
<Pal.Provider value={pal}>
|
||||
@@ -335,7 +335,7 @@ export default function Top() {
|
||||
</>}>
|
||||
<div className="t-cpu">
|
||||
<div className="t-cpu-graph">
|
||||
<Graph values={curves.get('cpu.usage') ?? []} tick={tick} cap={cap} max={100} grad="cpu" />
|
||||
<Graph values={curves.get('cpu.usage') ?? []} tick={tick} advance={advance} cap={cap} max={100} grad="cpu" />
|
||||
<div className="t-up">up {upfor(uptime)}</div>
|
||||
</div>
|
||||
<div className="t-cores">
|
||||
@@ -389,12 +389,12 @@ export default function Top() {
|
||||
<div className="t-net">
|
||||
<div className="t-net-graphs">
|
||||
<div className="t-net-g">
|
||||
<span className="t-scale">{rate(Math.max(1, ...(curves.get(`net.${segOf(iface.iface)}.rx`) ?? [0])))}</span>
|
||||
<Graph values={curves.get(`net.${segOf(iface.iface)}.rx`) ?? []} tick={tick} cap={cap} grad="down" />
|
||||
<span className="t-scale">{rate(peak(`net.${segOf(iface.iface)}.rx`))}</span>
|
||||
<Graph values={curves.get(`net.${segOf(iface.iface)}.rx`) ?? []} tick={tick} advance={advance} cap={cap} grad="down" />
|
||||
</div>
|
||||
<div className="t-net-g">
|
||||
<Graph values={curves.get(`net.${segOf(iface.iface)}.tx`) ?? []} tick={tick} cap={cap} grad="up" flip />
|
||||
<span className="t-scale t-scale-b">{rate(Math.max(1, ...(curves.get(`net.${segOf(iface.iface)}.tx`) ?? [0])))}</span>
|
||||
<Graph values={curves.get(`net.${segOf(iface.iface)}.tx`) ?? []} tick={tick} advance={advance} cap={cap} grad="up" flip />
|
||||
<span className="t-scale t-scale-b">{rate(peak(`net.${segOf(iface.iface)}.tx`))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="t-net-nums">
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Which points of a curve does each dot-column of a graph draw?
|
||||
//
|
||||
// A curve holds up to `cap` points (the whole history window); a box is `cols` columns wide,
|
||||
// and a narrow one — the net graph, a phone — has fewer columns than points. Taking "the
|
||||
// nearest point" for each column draws one point in three and, worse, a *different* one in
|
||||
// three every time the curve advances: a spike is there, gone, there again, and the whole
|
||||
// graph reshapes once a second. So:
|
||||
//
|
||||
// - a column draws the PEAK of all the points that fall in it, so nothing is ever skipped; and
|
||||
// - columns are cut from the data, not from the box: point number `a` (counted since the page
|
||||
// opened, `advance` being the newest) always belongs to column ⌊a·cols/cap⌋, whatever has
|
||||
// arrived since. A finished column therefore never changes — the graph only ever slides
|
||||
// left by whole columns, and only its newest column is still moving.
|
||||
//
|
||||
// With more columns than points (a wide box) each point simply spans several columns.
|
||||
// Returns one value per column, oldest first, right-aligned: a young curve has fewer columns
|
||||
// than the box and draws in from the right.
|
||||
export function columnsOf(values: number[], advance: number, cap: number, cols: number): number[] {
|
||||
const n = Math.min(values.length, cap)
|
||||
if (n === 0 || cols < 1 || cap < 1) return []
|
||||
const at = (a: number) => values[values.length - 1 - (advance - a)] // the point numbered a
|
||||
const oldest = advance - n + 1
|
||||
const out: number[] = []
|
||||
if (cols >= cap) {
|
||||
const kLast = Math.ceil(((advance + 1) * cols) / cap) - 1
|
||||
for (let k = kLast - cols + 1; k <= kLast; k++) {
|
||||
const a = Math.floor((k * cap) / cols)
|
||||
if (a >= oldest && a <= advance) out.push(at(a))
|
||||
}
|
||||
return out
|
||||
}
|
||||
const kLast = Math.floor((advance * cols) / cap)
|
||||
for (let k = kLast - cols + 1; k <= kLast; k++) {
|
||||
const a0 = Math.max(oldest, Math.ceil((k * cap) / cols)), a1 = Math.min(advance, Math.ceil(((k + 1) * cap) / cols) - 1)
|
||||
if (a1 < a0) continue
|
||||
let peak = at(a0)
|
||||
for (let a = a0 + 1; a <= a1; a++) peak = Math.max(peak, at(a))
|
||||
out.push(peak)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user