Spaces:
Running
Running
| import { workerData } from 'node:worker_threads'; | |
| // Runs on its own event loop, so it keeps ticking while the MAIN thread is | |
| // blocked. Reads the heartbeat + phase the main thread writes to shared memory | |
| // and logs to stderr — the one channel that survives a wedge (the Space's run | |
| // logs). See watchdog.js for the contract. | |
| const { sab, beatMs, stallMs, phases } = workerData; | |
| const hb = new BigInt64Array(sab, 0, 1); | |
| const meta = new Int32Array(sab, 8, 2); | |
| const mb = (b) => Math.round(b / 1048576); | |
| const phaseName = (c) => phases[c] || `#${c}`; | |
| const now = () => Date.now(); | |
| let stalling = false; | |
| let stallStart = 0; | |
| let lastStallLog = 0; | |
| let maxLag = 0; | |
| let lastOk = now(); | |
| setInterval(() => { | |
| const t = now(); | |
| const last = Number(Atomics.load(hb, 0)); | |
| const age = t - last; | |
| if (age > maxLag) maxLag = age; | |
| if (age > stallMs) { | |
| // Loop is stuck. Log on onset, then at most every 10s while it stays stuck, | |
| // so a long wedge leaves a trail without flooding. | |
| if (!stalling) { stalling = true; stallStart = last; lastStallLog = 0; } | |
| if (lastStallLog === 0 || t - lastStallLog >= 10000) { | |
| lastStallLog = t; | |
| console.error(`[watchdog] main loop STALLED ${Math.round(age / 1000)}s` | |
| + ` — activity=${phaseName(Atomics.load(meta, 0))} rss=${mb(process.memoryUsage().rss)}MB`); | |
| } | |
| return; | |
| } | |
| if (stalling) { | |
| stalling = false; | |
| console.error(`[watchdog] main loop recovered after ~${Math.round((t - stallStart) / 1000)}s`); | |
| } | |
| // Baseline heartbeat every 30s: a steady "ok" line means alive; a GAP in | |
| // these lines is itself the alarm, and rss/maxLag give a cheap trend. | |
| if (t - lastOk >= 30000) { | |
| lastOk = t; | |
| console.error(`[watchdog] ok rss=${mb(process.memoryUsage().rss)}MB maxLag=${maxLag}ms`); | |
| maxLag = 0; | |
| } | |
| }, beatMs); | |