File size: 3,015 Bytes
6dbdb30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63bdc81
6dbdb30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { Worker } from 'node:worker_threads';

// Observability that survives a BLOCKED event loop. When the main thread wedges
// on synchronous work (the FUSE/SQLite class of bug), in-process HTTP dies and
// main-thread timers can't fire — so nothing gets logged until/unless the loop
// recovers, and it may never. A worker thread has its OWN event loop and keeps
// watching regardless: it reports the stall to stderr (→ the Space's run logs)
// in real time, with the last activity breadcrumb and current RSS, so an
// incident is a labelled event instead of a sudden silence.
//
// Mechanism: the main thread stamps the time into a SharedArrayBuffer every
// beat and records a small integer "current activity" code; the worker reads
// both and, when the stamp goes stale, reports how long the loop has been
// unresponsive, what it was doing, and process memory (RSS is process-wide, so
// the worker can read it even while the main thread is frozen).

// Activity breadcrumbs. The worker maps code → name (order defines the code).
export const PHASE = {
  idle: 0,
  buildTraces: 1,
  readOpencode: 2,
  readHermes: 3,
  buildUsage: 4,
  readTrace: 5,
};
const PHASE_NAMES = Object.keys(PHASE);

const BEAT_MS = 1000;
const STALL_MS = 5000; // report once the loop has missed ~5 beats

let shared = null; // { hb: BigInt64Array, meta: Int32Array }
let worker = null;

export function startWatchdog() {
  if (worker || process.env.AM_NO_WATCHDOG) return;
  // BigInt64 for the heartbeat (Atomics needs an integer view; ms-since-epoch
  // overflows Int32), Int32 for the phase code — 8-byte aligned so both are
  // valid Atomics targets.
  const sab = new SharedArrayBuffer(16);
  const hb = new BigInt64Array(sab, 0, 1);
  const meta = new Int32Array(sab, 8, 2);
  Atomics.store(hb, 0, BigInt(Date.now()));
  shared = { hb, meta };
  const beat = setInterval(() => Atomics.store(hb, 0, BigInt(Date.now())), BEAT_MS);
  if (beat.unref) beat.unref();
  try {
    worker = new Worker(new URL('./watchdog-worker.js', import.meta.url), {
      workerData: { sab, beatMs: BEAT_MS, stallMs: STALL_MS, phases: PHASE_NAMES },
    });
    worker.unref();
    worker.on('error', () => {}); // the watchdog must never take down the app
  } catch { worker = null; }
}

// Cheap breadcrumb: record what the main thread is about to do, so a stall
// report can name the culprit. Returns the PREVIOUS code so callers can restore
// it — breadcrumbs then nest correctly (a build that dips into a db read shows
// the read while it runs, the build again after). A safe no-op (returns idle)
// before startWatchdog() runs.
export function mark(code) {
  return shared ? Atomics.exchange(shared.meta, 0, code | 0) : PHASE.idle;
}

// Run fn under a breadcrumb, restoring the previous one even on throw. Works for
// sync or async fn; the breadcrumb is what the worker reports if fn wedges.
export async function tracked(code, fn) {
  const prev = mark(code);
  try { return await fn(); } finally { mark(prev); }
}