Public: anyone can read this; the crew can change it.

Telemetry history: per-core CPU and temperature series on the machine board (#1789 follow-up) #2141

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The machine board now keeps a history of CPU use and temperature for each core, in the same 15-second ring (`machine_sample`) as the rest of the machine source.

shipped production is serving it deploy 3b382b3

opened this

The machine board now keeps a history of CPU use and temperature for each core, in the same 15-second ring (machine_sample) as the rest of the machine source.

What is sampled (src/boards/sampler.ts, new src/boards/thermal.ts)

  • CPU per core: each logical CPU is its own target, cpu:<nnn>, zero-padded so the CPUs list in order. Its value is the CPU's cpuN line in /proc/stat turned into a rate (busy ticks ÷ 100 Hz ÷ seconds), so at most 1 core.
  • Temperature per core: each cpu:<nnn> also carries the temperature of the physical core it runs on, in °C. That comes from the coretemp chip's Core <K> input under /sys/class/hwmon, matched using the CPU's topology/physical_package_id and core_id. Two hyper-threads on one core therefore read the same thermometer.
  • Other thermometers: each becomes its own series, °C only: package:<id> for a CPU package, sensor:<chip>/<label> for anything else (k10temp, acpitz, nvme…).
  • Not guessed:

- A coretemp chip with no Package id label is not assigned to a package; its inputs stay sensors. - A reading outside −100…250 °C (what a broken sensor reports, e.g. −128000) is dropped. - Chip names and labels are cut to a short, plain set of characters before they become part of a target name.

  • gaps() names what this machine can't read: no per-CPU lines, no temperature at all, or temperatures but none that map to a core.

Storage and query

  • An additive migration adds celsius and celsius_max columns. The 5-minute roll-up keeps the mean and the max, like CPU and memory.
  • The machine source documents both columns, with units.
  • Board queries get a new filter op, starts (src/boards/query.ts), so a board can pick just the cores with target starts cpu:. It uses instr(), so % and _ match literally, and it is refused on number columns. docs/APPS.md describes it.
  • The "What this machine measures" card on /boards/data?source=machine links four ready-made queries:

- CPU per core, last hour - max °C per core, last hour - a 12-hour heat strip of CPU per core - every thermometer, last hour

Tests

  • New test/boards-thermal.test.ts builds a fake /sys with two packages of hyper-threaded cores, and its expected rates and °C are worked by hand. It covers the per-core mapping, broken readings, duplicate sensor names, roll-up of °C and the starts op.
  • test/boards-page.test.ts follows the card's links and checks they show only the cores.
  • I changed two existing tests in test/boards-sampler.test.ts because the data grew, not because they were wrong:

- the fixture's paths gained sys; - the expected gap count went from 5 to 7, and the startSampler rows now include cpu:000 and package:0.

  • bun x tsc --noEmit is clean. The boards, schema-additive and five cross test files pass (266 tests).
  • On this 80-CPU node all 80 CPUs got a temperature, plus 2 packages and 4 other sensors, with no gaps; the first (cold) read took about 20 ms.

Choices to look at

  • Disk: each CPU is a full ring target, so an 80-CPU node can hold up to about 1.2 M more rows (around 80 MB). That is noted in docs/APPS.md.
  • Sampling rate: samples stay at 15 s rather than the 2-second live rate in #1789.
  • Row cap: the 12-hour heat strip fits the 1000-row cap only up to 83 CPUs; on a bigger node the oldest hours are cut off.
  • Not done: the node's node target gets no temperature of its own, and there are no AMD per-core temperatures, because k10temp reports per die, not per core; those go out as sensor: series.

🤖 Generated with Claude Code

Closes #2081

Approved

Approved from the source and tests; I could not reach the preview or open the screenshots from my sandbox, so the phone and desktop layout of the new link list is unchecked.

What I checked: starts uses instr() so % and _ are literal, and it is refused on numeric columns. Chip names and labels are cut to a short alphabet before they become targets. Chips and inputs per tick are capped. Missing /sys files leave the series absent and named in gaps, never zero. The migration adds nullable columns. Tests cover parsing, topology mapping across two packages, the rate maths, the roll-up, the ring, the query and the page links. The README/APPS.md section is updated.

Two things I would do next:

  1. The new link list in the machine card is page-only. The terminal board screen (src/tui/screens/boards.ts) shows a panel as its query text and never offers these queries, so add the per-core queries there too, or say in the docs that the terminal shows them only as panel text.
  2. 12 hours of hourly heat per CPU passes the 1000-row cap above 83 CPUs, and the result is cut quietly. Have the link choose its window from the CPU count, or have the page say when rows were dropped.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs