ccdeck 3.37.0 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/web/assets/{BrowserWatchModal-DCHV09J8.js → BrowserWatchModal-BPObeZn1.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-oDjpHw4z.js → UsageHistoryModal-Drv13uht.js} +1 -1
- package/dist/web/assets/index-0cUs2Jed.css +1 -0
- package/dist/web/assets/index-C_h8mR-c.js +307 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +8 -1
- package/src/server/claude-jobs.mjs +152 -0
- package/src/server/lan-engine.mjs +30 -20
- package/src/server/ring-bounds.mjs +9 -1
- package/src/server/session-activity.mjs +176 -0
- package/src/server/session-enrichment.mjs +69 -4
- package/src/server/session-tracking.mjs +22 -3
- package/src/server/thermal-metrics.mjs +113 -4
- package/src/server/thermal-sampler.mjs +10 -4
- package/dist/web/assets/index-B0SOa-XB.css +0 -1
- package/dist/web/assets/index-wCbqcRJd.js +0 -306
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// decides how often the machine is asked and when one that has never answered
|
|
6
6
|
// stops being asked (sampleThermal). This file is the asking: readThermal
|
|
7
7
|
// answers once per call, per platform, and nothing here keeps state between two
|
|
8
|
-
// calls
|
|
9
|
-
//
|
|
8
|
+
// calls — the one reading that needs the call before it, Linux throttling, is
|
|
9
|
+
// handed that call's answer back by the sampler. WARN_C and CRIT_C are exported
|
|
10
|
+
// because the history's thermal series fall back to them.
|
|
10
11
|
import { readdir, readFile } from "node:fs/promises";
|
|
11
12
|
import { run } from "./metrics-run.mjs";
|
|
12
13
|
|
|
@@ -31,6 +32,17 @@ import { run } from "./metrics-run.mjs";
|
|
|
31
32
|
// bands are the hardware's rather than ones invented here.
|
|
32
33
|
// /sys/class/thermal/thermal_zone*/ is the coarser fallback.
|
|
33
34
|
//
|
|
35
|
+
// Throttling comes from the kernel's own count of it, and for years
|
|
36
|
+
// it was not read at all: this branch answered `throttle: null`
|
|
37
|
+
// unconditionally, so a Linux machine never drew the row however
|
|
38
|
+
// hard it was being held back. Measured on an i5-12600H mini PC
|
|
39
|
+
// whose package sat at 90°C under a load of 41 — the panel showed
|
|
40
|
+
// the 90 and nothing else, while the kernel had logged 59 hours of
|
|
41
|
+
// package throttling in 17 days of uptime and was adding six seconds
|
|
42
|
+
// of it every ten. See readThrottleTime: Intel only, since the
|
|
43
|
+
// counters come from Intel's thermal-interrupt driver, and no row
|
|
44
|
+
// on a machine without them.
|
|
45
|
+
//
|
|
34
46
|
// macOS No CPU degrees without root, verified: `powermetrics --samplers
|
|
35
47
|
// smc` answers "powermetrics must be invoked as the superuser", and
|
|
36
48
|
// `ioreg -c AppleSMC -r -d 1` publishes no temperature key at all to
|
|
@@ -260,6 +272,91 @@ export async function readThermalZones(root = "/sys/class/thermal", deps = {}) {
|
|
|
260
272
|
return [known ?? found.reduce((a, b) => (b.celsius > a.celsius ? b : a))];
|
|
261
273
|
}
|
|
262
274
|
|
|
275
|
+
/**
|
|
276
|
+
* How long each CPU package has spent thermally throttled since boot, in
|
|
277
|
+
* milliseconds, keyed by package — or null on a machine that does not count it.
|
|
278
|
+
*
|
|
279
|
+
* Intel's thermal-interrupt driver publishes, per CPU,
|
|
280
|
+
* `cpuN/thermal_throttle/package_throttle_total_time_ms`: the time the
|
|
281
|
+
* package's thermal monitor has held the clock down, summed over every event.
|
|
282
|
+
* Measured on a live machine, ten seconds apart:
|
|
283
|
+
*
|
|
284
|
+
* cpu0 package_throttle_total_time_ms 212885052 → 212890804
|
|
285
|
+
* cpu15 package_throttle_total_time_ms 212885101 → 212891179
|
|
286
|
+
*
|
|
287
|
+
* Every CPU of a package carries its own copy of the package's count, and the
|
|
288
|
+
* copies differ by the few events each one saw late — so the package's figure
|
|
289
|
+
* is the largest of them, which is also the one that survives a CPU going
|
|
290
|
+
* offline. The `core_*` files beside them are one core's own throttling and are
|
|
291
|
+
* not read: the CPU row is the package, and a throttle row about one core of it
|
|
292
|
+
* would be a different reading under the same label.
|
|
293
|
+
*
|
|
294
|
+
* Absent on AMD and on every virtual machine, and on a kernel older than 5.12,
|
|
295
|
+
* which counts events but not their time. A count of events is not a share of
|
|
296
|
+
* anything, so those machines draw no row rather than a guess.
|
|
297
|
+
*/
|
|
298
|
+
export async function readThrottleTime(root = "/sys/devices/system/cpu", deps = {}) {
|
|
299
|
+
const dir = deps.readdir ?? readdir;
|
|
300
|
+
const file = deps.readFile ?? readFile;
|
|
301
|
+
const read = async path => { try { return String(await file(path, "utf8")).trim(); } catch { return null; } };
|
|
302
|
+
let cpus;
|
|
303
|
+
try { cpus = (await dir(root)).filter(n => /^cpu\d+$/.test(n)); } catch { return null; }
|
|
304
|
+
const held = {};
|
|
305
|
+
for (const cpu of cpus) {
|
|
306
|
+
const ms = Number(await read(`${root}/${cpu}/thermal_throttle/package_throttle_total_time_ms`) ?? NaN);
|
|
307
|
+
if (!Number.isFinite(ms) || ms < 0) continue;
|
|
308
|
+
const pkg = (await read(`${root}/${cpu}/topology/physical_package_id`)) ?? "0";
|
|
309
|
+
if (held[pkg] == null || ms > held[pkg]) held[pkg] = ms;
|
|
310
|
+
}
|
|
311
|
+
return Object.keys(held).length ? held : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The share of the time between two counts that the CPU spent throttled, as
|
|
316
|
+
* `{ timeHeld }`, or null when there are not two counts to compare.
|
|
317
|
+
*
|
|
318
|
+
* A SHARE OF TIME, NOT OF SPEED, and the shape says so rather than borrowing
|
|
319
|
+
* macOS's `speedLimit`. `pmset` reports how much of the clock the thermal
|
|
320
|
+
* manager allows; the kernel reports how long the clock was held down, and by
|
|
321
|
+
* how much it was held is not in the file. Writing 60% of the time as "held to
|
|
322
|
+
* 40% of full speed" would be a number this module made up.
|
|
323
|
+
*
|
|
324
|
+
* The worst package wins, for the reason the hottest sensor does: the question
|
|
325
|
+
* is whether this machine is being held back, and one package that is answers
|
|
326
|
+
* it. Any throttling at all is at least 1%, because a few milliseconds of it
|
|
327
|
+
* round to zero and the panel paints zero as "never held back".
|
|
328
|
+
*
|
|
329
|
+
* The kernel adds an event's time when the event ENDS, so one still running at
|
|
330
|
+
* a sample lands in the next. Events here run 5ms on average and 119ms at the
|
|
331
|
+
* longest, measured, so that is a sample's worth of lag at most — and the 100
|
|
332
|
+
* cap is for the one long event that lands whole in a single sample.
|
|
333
|
+
*/
|
|
334
|
+
export function throttleFromTime(prev, next) {
|
|
335
|
+
if (!prev || !next) return null;
|
|
336
|
+
const elapsed = next.atMs - prev.atMs;
|
|
337
|
+
if (!(elapsed > 0)) return null;
|
|
338
|
+
let worst = null;
|
|
339
|
+
for (const [pkg, ms] of Object.entries(next.held)) {
|
|
340
|
+
const before = prev.held[pkg];
|
|
341
|
+
if (before == null) continue;
|
|
342
|
+
const share = (Math.max(0, ms - before) / elapsed) * 100;
|
|
343
|
+
if (worst == null || share > worst) worst = share;
|
|
344
|
+
}
|
|
345
|
+
if (worst == null) return null;
|
|
346
|
+
return { timeHeld: worst > 0 ? Math.min(100, Math.max(1, Math.round(worst))) : 0 };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The share of the CPU the thermal manager took away, whichever way this
|
|
351
|
+
* platform measures it: the speed `pmset` says is no longer allowed, or the
|
|
352
|
+
* time the kernel says the clock was held down. The one number the chart, the
|
|
353
|
+
* strip and the row's bar all draw — mirrored by `heldShare` in machine-live.ts.
|
|
354
|
+
*/
|
|
355
|
+
export function heldShare(throttle) {
|
|
356
|
+
const held = "timeHeld" in throttle ? throttle.timeHeld : 100 - throttle.speedLimit;
|
|
357
|
+
return Math.max(0, Math.min(100, held));
|
|
358
|
+
}
|
|
359
|
+
|
|
263
360
|
/**
|
|
264
361
|
* GPU degrees out of `ioreg -r -k PerformanceStatistics`.
|
|
265
362
|
*
|
|
@@ -458,13 +555,25 @@ export function darwinThermal({ gpuC = null, throttle = null, macmon = {} } = {}
|
|
|
458
555
|
* collapsing them would let a throttle percentage be drawn under a °C heading
|
|
459
556
|
* — the thing the label rule exists to prevent. `swapLabel` earned that rule
|
|
460
557
|
* once already.
|
|
558
|
+
*
|
|
559
|
+
* `since` is the reading before this one. Only Linux uses it: its throttle is a
|
|
560
|
+
* difference between two counts, so the count rides along on the reading as
|
|
561
|
+
* `throttleCount` and the sampler hands it back on the next call. The first
|
|
562
|
+
* reading after a start therefore has no throttle yet, the same way `cpu` is
|
|
563
|
+
* null until two samples exist.
|
|
461
564
|
*/
|
|
462
|
-
export async function readThermal(platform = process.platform) {
|
|
565
|
+
export async function readThermal(platform = process.platform, since = null) {
|
|
463
566
|
if (platform === "linux") {
|
|
464
567
|
const sensors = await readHwmon();
|
|
465
568
|
const celsius = pickThermalRows(sensors);
|
|
466
569
|
const rows = celsius.length ? celsius : await readThermalZones();
|
|
467
|
-
|
|
570
|
+
const held = await readThrottleTime();
|
|
571
|
+
// performance.now rather than Date.now: a clock that NTP steps backwards
|
|
572
|
+
// between two samples would otherwise divide by a negative ten seconds.
|
|
573
|
+
const throttleCount = held ? { atMs: performance.now(), held } : null;
|
|
574
|
+
const throttle = throttleFromTime(since?.throttleCount, throttleCount);
|
|
575
|
+
if (!rows.length && !throttle) return null;
|
|
576
|
+
return { celsius: rows, throttle, ...(throttleCount ? { throttleCount } : {}) };
|
|
468
577
|
}
|
|
469
578
|
|
|
470
579
|
if (platform === "darwin") {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// thermalSnapshot on /api/system, and re-exports sampleThermal and
|
|
11
11
|
// THROTTLE_LABEL, which is where the suite reaches them.
|
|
12
12
|
import { pointsOf, record } from "./metrics-history.mjs";
|
|
13
|
-
import { readThermal } from "./thermal-metrics.mjs";
|
|
13
|
+
import { heldShare, readThermal } from "./thermal-metrics.mjs";
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Thermal: how often this machine is asked whether it is getting hot, and when
|
|
@@ -60,7 +60,7 @@ function recordThermal(reading, nowMs = Date.now()) {
|
|
|
60
60
|
// Stored as the share TAKEN AWAY, the same way the panel draws it, so the
|
|
61
61
|
// chart and the row cannot disagree about which direction is bad.
|
|
62
62
|
if (reading.throttle) {
|
|
63
|
-
record(`thermal:${THROTTLE_LABEL}`,
|
|
63
|
+
record(`thermal:${THROTTLE_LABEL}`, heldShare(reading.throttle), nowMs);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -114,7 +114,9 @@ export async function sampleThermal(deps = {}) {
|
|
|
114
114
|
const read = deps.read ?? readThermal;
|
|
115
115
|
thermalInFlight = true;
|
|
116
116
|
try {
|
|
117
|
-
|
|
117
|
+
// The last reading goes back in because Linux throttling is the difference
|
|
118
|
+
// between its count and this one's. See readThermal.
|
|
119
|
+
const next = await read(process.platform, thermal);
|
|
118
120
|
if (next) { thermal = next; thermalMisses = 0; thermalEverAnswered = true; recordThermal(next); }
|
|
119
121
|
else if (++thermalMisses >= THERMAL_GIVE_UP) {
|
|
120
122
|
// DROP THE LAST READING. It used to be kept, and that is a number from
|
|
@@ -181,7 +183,11 @@ export function lastThermal() {
|
|
|
181
183
|
/** The thermal row /api/system answers: the last reading, with whether the
|
|
182
184
|
* machine has been held back since the deck started, or null without one. */
|
|
183
185
|
export function thermalSnapshot() {
|
|
184
|
-
|
|
186
|
+
if (!thermal) return null;
|
|
187
|
+
// The raw kernel count is the sampler's, for the next difference, and not a
|
|
188
|
+
// reading: the route answers what the panel draws.
|
|
189
|
+
const { throttleCount, ...shown } = thermal;
|
|
190
|
+
return { ...shown, heldBack: heldBackSoFar() };
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
/** The ten-second timer, made where startSystemMetrics in system-metrics.mjs
|