From c40de5067b1debc8ecf7df482bf870cf108baa4b Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Tue, 10 Nov 2026 12:19:49 -0800 Subject: [PATCH] Top: each CPU's threads are counted from 0 under it on a two-socket machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel numbers the whole machine's threads in one run and deals them out a socket at a time, twice: nexus's first CPU listed C0–C9 and C20–C29, its second C10–C19 and C30–C39. Each CPU now lists its own twenty as C0–C19; the kernel's number is in the row's tooltip. Co-Authored-By: Claude --- web/package.json | 2 +- web/scripts/test-sockets.mjs | 34 ++++++++++++++++++++++++++++++++++ web/src/Top.tsx | 18 +++++++++++------- web/src/sockets.ts | 17 +++++++++++++++++ 4 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 web/scripts/test-sockets.mjs create mode 100644 web/src/sockets.ts diff --git a/web/package.json b/web/package.json index 5e2d67d..717c339 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "icons": "node scripts/icons.mjs", - "test": "node scripts/test-graphcols.mjs && node scripts/test-css-imports.mjs && node scripts/test-resolve-path.mjs" + "test": "node scripts/test-graphcols.mjs && node scripts/test-sockets.mjs && node scripts/test-css-imports.mjs && node scripts/test-resolve-path.mjs" }, "dependencies": { "@xterm/addon-fit": "^0.10.0", diff --git a/web/scripts/test-sockets.mjs b/web/scripts/test-sockets.mjs new file mode 100644 index 0000000..c8b5d10 --- /dev/null +++ b/web/scripts/test-sockets.mjs @@ -0,0 +1,34 @@ +// node scripts/test-sockets.mjs — src/sockets.ts: each CPU's threads, counted under it. +import { buildSync } from 'esbuild' +import { pathToFileURL } from 'node:url' +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const out = join(mkdtempSync(join(tmpdir(), 'sockets-')), 'sockets.mjs') +buildSync({ entryPoints: [new URL('../src/sockets.ts', import.meta.url).pathname], outfile: out, format: 'esm', logLevel: 'error' }) +const { socketThreads } = await import(pathToFileURL(out).href) + +const fails = [] +const eq = (got, want, what) => { if (JSON.stringify(got) !== JSON.stringify(want)) fails.push(`${what} — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`) } +const range = (a, b) => Array.from({ length: b - a }, (_, i) => a + i) + +// nexus: 2 × E5-2680 v2, as /sys/devices/system/cpu/cpu*/topology/physical_package_id reads +const nexus = [...Array(10).fill(0), ...Array(10).fill(1), ...Array(10).fill(0), ...Array(10).fill(1)] +const got = socketThreads(nexus) +eq(got.map((s) => s.pkg), [0, 1], 'two sockets, in order') +eq(got[0].cpus, [...range(0, 10), ...range(20, 30)], "CPU 1's threads") +eq(got[1].cpus, [...range(10, 20), ...range(30, 40)], "CPU 2's threads") +eq(got.map((s) => s.cpus.length), [20, 20], 'twenty threads each') +// every thread is under exactly one CPU +eq(got.flatMap((s) => s.cpus).sort((a, b) => a - b), range(0, 40), 'every thread once') +// a thread's number under its CPU runs 0…19 on both +for (const s of got) eq(s.cpus.map((_, n) => n), range(0, 20), `CPU ${s.pkg + 1} counts from 0`) + +// a board that numbers each socket's threads together, and one whose socket ids are not 0 and 1 +eq(socketThreads([0, 0, 0, 0, 1, 1, 1, 1]), [{ pkg: 0, cpus: [0, 1, 2, 3] }, { pkg: 1, cpus: [4, 5, 6, 7] }], 'contiguous numbering') +eq(socketThreads([3, 1, 3, 1]), [{ pkg: 1, cpus: [1, 3] }, { pkg: 3, cpus: [0, 2] }], 'socket ids sorted') +eq(socketThreads([]), [], 'nothing') + +if (fails.length) { console.error('sockets: FAIL\n ' + fails.join('\n ')); process.exit(1) } +console.log('sockets: ok') diff --git a/web/src/Top.tsx b/web/src/Top.tsx index eaecf5a..a947b00 100644 --- a/web/src/Top.tsx +++ b/web/src/Top.tsx @@ -11,6 +11,7 @@ import { fmtMs, nextPoint, REFRESH_OPTIONS, setRefreshMs, strideMs, useHistorySe import { Loading, askYes, toast } from './ui' import { Theme, currentThemeId, themeById } from './themes' import { columnsAt } from './graphcols' +import { socketThreads } from './sockets' import { savePrefs } from './accents' const MAX = 300 // points kept per curve, as the machines keep them @@ -370,7 +371,10 @@ export default function Top() { // On a two-socket board each CPU has its own sensor chip, both counting from "Core 0", so a // thread's core is looked up on its own socket's chip. const pkgs = pt?.core_pkgs?.length === cores.length ? pt.core_pkgs : undefined - const sockets = pkgs ? [...new Set(pkgs)].sort((a, b) => a - b) : [] + // each CPU's own threads, counted from 0 under it (sockets.ts): the kernel's one numbering + // of the whole machine puts cpu 0–9 and 20–29 on the first and 10–19 and 30–39 on the second + const socks = pkgs ? socketThreads(pkgs) : [] + const sockets = socks.map((x) => x.pkg) const coreTemp = (i: number) => { const id = pt?.core_ids?.[i] ?? i, pk = pkgs?.[i]; return s?.temps.find((t) => new RegExp(`core ${id}\\b`, 'i').test(t.label) && (pk === undefined || (t.pkg ?? 0) === pk))?.c } const sockTemp = (pk: number) => s?.temps.find((t) => t.chip === 'coretemp' && new RegExp(`^package id ${pk}$`, 'i').test(t.label))?.c const sockUse = (pk: number) => { const v = cores.filter((_, i) => pkgs?.[i] === pk); return v.length ? v.reduce((a, b) => a + b, 0) / v.length : 0 } @@ -425,7 +429,7 @@ export default function Top() { {Math.round(cpu)}% {anyTemp && <>{cpuTemp !== undefined && }{cpuTemp !== undefined ? `${Math.round(cpuTemp)}°C` : ''}} } - {(sockets.length > 1 ? sockets : [undefined]).map((pk) => ( + {(sockets.length > 1 ? socks : [{ pkg: undefined, cpus: cores.map((_, i) => i) }]).map(({ pkg: pk, cpus }) => ( {pk !== undefined && (() => { const u = sockUse(pk), t = sockTemp(pk), w = pt?.cpu_power_pkgs_w?.[pk], open = !folded.includes(pk) @@ -439,13 +443,13 @@ export default function Top() { ) })()} - {cores.map((v, i) => { - if (pk !== undefined && (pkgs?.[i] !== pk || folded.includes(pk))) return null - const t = coreTemp(i) + {(pk !== undefined && folded.includes(pk) ? [] : cpus).map((i, n) => { + // n is the thread's number under its own CPU, i the kernel's for the whole machine + const t = coreTemp(i), v = cores[i] ?? 0 return (
- C{i} - {braille(recent.get(`cpu.core.${i}.usage`) ?? [], 9)} + C{n} + {braille(recent.get(`cpu.core.${i}.usage`) ?? [], 9)} {Math.round(v)}% {anyTemp && <>{t !== undefined && }{t !== undefined ? `${Math.round(t)}°C` : ''}}
diff --git a/web/src/sockets.ts b/web/src/sockets.ts new file mode 100644 index 0000000..df5eaf2 --- /dev/null +++ b/web/src/sockets.ts @@ -0,0 +1,17 @@ +// Which threads belong to which CPU, on a machine with more than one. +// +// The kernel numbers every thread of the machine in one run, and on a two-socket board with +// hyper-threading it deals them out a socket at a time and then goes round again: cpu 0–9 are +// CPU 1's cores, 10–19 CPU 2's, 20–29 the second threads of CPU 1's, 30–39 of CPU 2's. Listed +// under its CPU by that number, a 20-thread processor reads "C0…C9, C20…C29" and the one +// beside it "C10…C19, C30…C39" — neither looks like twenty of anything. So each CPU's threads +// are counted from 0 under it, in the kernel's order, the way a one-socket machine's are: the +// first half are its cores, the second half their second threads. +// +// `pkgs[i]` is the socket logical CPU `i` is on. The answer is, for each socket (ascending), +// the logical CPUs on it (ascending); a thread's number under its CPU is its place in that list. +export function socketThreads(pkgs: number[]): { pkg: number; cpus: number[] }[] { + const by = new Map() + pkgs.forEach((pk, i) => { const l = by.get(pk); if (l) l.push(i); else by.set(pk, [i]) }) + return [...by.entries()].sort((a, b) => a[0] - b[0]).map(([pkg, cpus]) => ({ pkg, cpus })) +}