@maiyunnet/kebab 9.21.0 → 9.21.1

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/sys/monitor.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2026-02-07
4
- * Last: 2026-08-22
5
- * --- 性能监控库,用于检测 CPU/内存骤升并记录可疑请求、堆栈、CPU Profile、堆快照 ---
4
+ * Last: 2026-10-05
5
+ * --- 性能监控库,用于确认 CPU/内存持续超阈值,保存相关请求、资源采样和诊断文件 ---
6
6
  * --- 包含 Worker 看门狗线程,用于在事件循环完全阻塞时实时检测并记录 ---
7
7
  */
8
8
  import * as os from 'os';
@@ -24,13 +24,13 @@ let memThreshold = 0;
24
24
  let eloopThreshold = 500;
25
25
  /** --- 是否在内存超阈值时自动采集堆快照 --- */
26
26
  let heapSnapshotEnabled = false;
27
- /** --- 监控间隔,单位 ms --- */
28
- const INTERVAL = 5_000;
29
- /** --- 连续超阈值次数达到此值才记录日志,防止瞬间波动 --- */
30
- const SPIKE_COUNT = 2;
27
+ /** --- 监控间隔,单位 ms;主线程被阻塞时由 Worker 独立采样 --- */
28
+ const INTERVAL = 1_000;
29
+ /** --- 同一指标持续超阈值的确认时间,单位 ms --- */
30
+ let spikeDuration = 10_000;
31
31
  /** --- CPU Profile 采集时长,单位 ms --- */
32
32
  const PROFILE_DURATION = 5_000;
33
- /** --- 两次诊断采集的最小间隔,防止频繁写磁盘,单位 ms --- */
33
+ /** --- 同一持续异常重复采集的间隔,单位 ms;新事件或指标复发不受此限制 --- */
34
34
  const DIAGNOSTIC_COOLDOWN = 60_000;
35
35
  /** --- 单次快照或日志最多输出的活跃请求详情数 --- */
36
36
  const MAX_ACTIVE_REQUEST_DETAILS = 100;
@@ -51,33 +51,43 @@ let lastSnapshotCpuTime = 0;
51
51
  let lastSnapshotOsCpus = null;
52
52
  /** --- 事件循环延迟直方图 --- */
53
53
  let eloopHistogram = null;
54
- /** --- 连续超阈值计数 --- */
55
- let spikeCounter = 0;
54
+ /** --- 各指标首次超阈值的单调时钟时间,不能跨指标累计 --- */
55
+ let spikeSince = {
56
+ 'cpu': null, 'mem': null, 'eloop': null,
57
+ };
56
58
  /** --- 请求自增计数器 --- */
57
59
  let requestCounter = 0;
58
- /** --- 上次诊断采集时间 --- */
59
- let lastDiagnosticTime = 0;
60
60
  /** --- 是否正在采集诊断数据 --- */
61
61
  let diagnosing = false;
62
62
  /** --- 是否正在进行 CPU Profile 采集 --- */
63
63
  let profiling = false;
64
+ /** --- 同步 Report/HeapSnapshot 会影响本采样窗口,完成后重新建立检测基准 --- */
65
+ let diagnosticOverhead = false;
64
66
  /**
65
67
  * --- 检测当前是否处于调试模式(以 --inspect 启动或 IDE 调试器已连接) ---
68
+ * @returns 是否处于调试模式
66
69
  */
67
70
  function isDebugMode() {
68
71
  return !!inspector.url();
69
72
  }
73
+ /**
74
+ * --- 获取单调时钟毫秒数,避免系统校时影响 CPU 比例和心跳 ---
75
+ * @returns 单调时钟毫秒数
76
+ */
77
+ function clockMs() {
78
+ return Number(process.hrtime.bigint() / 1000000n);
79
+ }
70
80
  // --- 看门狗相关 ---
71
81
  /** --- 看门狗 Worker 实例 --- */
72
82
  let watchdog = null;
73
- /** --- 心跳共享内存(Uint32: 秒级时间戳) --- */
83
+ /** --- 心跳共享内存,协议见 monitor/watchdog.ts --- */
74
84
  let heartbeatBuffer = null;
75
85
  /** --- 心跳共享内存视图 --- */
76
86
  let heartbeatView = null;
77
- /** --- 看门狗检测阈值,主线程超过此秒数未更新心跳则认为阻塞,单位秒 --- */
78
- const WATCHDOG_THRESHOLD = 15;
87
+ /** --- 主线程持续未更新心跳的确认时间,单位 ms --- */
88
+ let watchdogThreshold = 5_000;
79
89
  /** --- 看门狗检测间隔,单位 ms --- */
80
- const WATCHDOG_INTERVAL = 5_000;
90
+ const WATCHDOG_INTERVAL = 1_000;
81
91
  /** --- 看门狗告警冷却时间,持续阻塞时不会每次都写日志,单位秒 --- */
82
92
  const WATCHDOG_COOLDOWN = 30;
83
93
  /** --- 看门狗异常退出后的重启延迟,单位 ms --- */
@@ -86,29 +96,52 @@ const WATCHDOG_RESTART_DELAY = 5_000;
86
96
  let watchdogRestartTimer = null;
87
97
  /** --- 活跃请求池 --- */
88
98
  const activeRequests = new Map();
99
+ /** --- 有界保存最近完成的请求,避免采集时请求已经移出活跃池 --- */
100
+ const recentRequests = [];
101
+ /** --- 最近一分钟的资源采样,不包含请求参数或请求体 --- */
102
+ const sampleHistory = [];
103
+ let lastWatchdogDiagnostic = null;
104
+ const watchdogIncidents = new Map();
105
+ /** --- 当前持续异常及其上次重型诊断时间 --- */
106
+ let incident = null;
107
+ let lastDiagnosticTime = 0;
108
+ let lastEventSaveTime = 0;
109
+ /** --- 串行写入事件摘要,避免初次保存覆盖恢复或采集结果 --- */
110
+ let eventWrites = Promise.resolve();
89
111
  /**
90
112
  * --- 启动性能监控 ---
91
113
  * @param opt 可选的阈值配置
114
+ * @returns 是否成功启动,配置无效时返回 false
92
115
  */
93
116
  export function start(opt) {
94
117
  if (timer) {
95
- return;
118
+ return true;
96
119
  }
97
120
  const nextCpuThreshold = opt?.cpu ?? 80;
98
121
  const nextMemThreshold = opt?.mem ?? 0;
99
122
  const nextEloopThreshold = opt?.eloop ?? 500;
123
+ const nextDuration = opt?.duration ?? 10_000;
124
+ const nextBlocked = opt?.blocked ?? 5_000;
100
125
  if (!Number.isFinite(nextCpuThreshold) || nextCpuThreshold <= 0) {
101
- throw new RangeError('Monitor CPU threshold must be greater than 0.');
126
+ return false;
102
127
  }
103
128
  if (!Number.isFinite(nextMemThreshold) || nextMemThreshold < 0) {
104
- throw new RangeError('Monitor memory threshold cannot be negative.');
129
+ return false;
105
130
  }
106
131
  if (!Number.isFinite(nextEloopThreshold) || nextEloopThreshold <= 0) {
107
- throw new RangeError('Monitor event loop threshold must be greater than 0.');
132
+ return false;
133
+ }
134
+ if (!Number.isFinite(nextDuration) || nextDuration < INTERVAL) {
135
+ return false;
136
+ }
137
+ if (!Number.isFinite(nextBlocked) || nextBlocked < INTERVAL) {
138
+ return false;
108
139
  }
109
140
  cpuThreshold = nextCpuThreshold;
110
141
  memThreshold = nextMemThreshold;
111
142
  eloopThreshold = nextEloopThreshold;
143
+ spikeDuration = nextDuration;
144
+ watchdogThreshold = nextBlocked;
112
145
  heapSnapshotEnabled = opt?.heapSnapshot ?? false;
113
146
  if (memThreshold === 0) {
114
147
  // --- 同时考虑容器/系统约束和 V8 堆上限,避免默认阈值高于进程实际可用范围 ---
@@ -120,7 +153,7 @@ export function start(opt) {
120
153
  memThreshold = Math.floor(Math.min(systemLimit * 0.8, heapLimit * 0.9) / 1024 / 1024);
121
154
  }
122
155
  const cpuUsage = process.cpuUsage();
123
- const cpuTime = Date.now();
156
+ const cpuTime = clockMs();
124
157
  const osCpus = os.cpus();
125
158
  lastCheckCpuUsage = cpuUsage;
126
159
  lastCheckCpuTime = cpuTime;
@@ -128,13 +161,19 @@ export function start(opt) {
128
161
  lastSnapshotCpuUsage = cpuUsage;
129
162
  lastSnapshotCpuTime = cpuTime;
130
163
  lastSnapshotOsCpus = osCpus;
131
- spikeCounter = 0;
164
+ spikeSince = { 'cpu': null, 'mem': null, 'eloop': null };
165
+ sampleHistory.length = 0;
166
+ recentRequests.length = 0;
167
+ incident = null;
168
+ lastWatchdogDiagnostic = null;
169
+ watchdogIncidents.clear();
170
+ diagnosticOverhead = false;
132
171
  lastDiagnosticTime = 0;
133
- // --- 初始化心跳共享内存(索引 0: 秒级时间戳, 索引 1: 调试模式标志) ---
134
- heartbeatBuffer = new SharedArrayBuffer(8);
135
- heartbeatView = new Uint32Array(heartbeatBuffer);
136
- Atomics.store(heartbeatView, 0, Math.floor(Date.now() / 1000));
137
- Atomics.store(heartbeatView, 1, isDebugMode() ? 1 : 0);
172
+ // --- 0: 单调时钟心跳, 1: 调试标志, 2: Inspector 占用, 3: 主线程诊断开销, 4: 已采集阻塞的心跳 ---
173
+ heartbeatBuffer = new SharedArrayBuffer(40);
174
+ heartbeatView = new BigInt64Array(heartbeatBuffer);
175
+ Atomics.store(heartbeatView, 0, BigInt(cpuTime));
176
+ Atomics.store(heartbeatView, 1, isDebugMode() ? 1n : 0n);
138
177
  // --- 启用事件循环延迟直方图 ---
139
178
  eloopHistogram = perfHooks.monitorEventLoopDelay({ 'resolution': 20 });
140
179
  eloopHistogram.enable();
@@ -142,11 +181,20 @@ export function start(opt) {
142
181
  // --- 启动看门狗线程 ---
143
182
  startWatchdog();
144
183
  lCore.debug(`[MONITOR] [PARENT] [${process.pid}] Started, CPU Threshold: ${cpuThreshold}%, MEM Threshold: ${memThreshold}MB, ELOOP Threshold: ${eloopThreshold}ms`);
184
+ return true;
145
185
  }
146
186
  /**
147
187
  * --- 停止性能监控 ---
188
+ * @returns 无返回值
148
189
  */
149
190
  export function stop() {
191
+ watchdogIncidents.clear();
192
+ if (incident) {
193
+ incident.endedAt = Date.now();
194
+ incident.endReason = 'stopped';
195
+ saveIncident(incident);
196
+ incident = null;
197
+ }
150
198
  if (timer) {
151
199
  clearInterval(timer);
152
200
  timer = null;
@@ -166,19 +214,22 @@ export function stop() {
166
214
  watchdogRestartTimer = null;
167
215
  }
168
216
  activeRequests.clear();
217
+ recentRequests.length = 0;
218
+ sampleHistory.length = 0;
169
219
  lastCheckCpuUsage = null;
170
220
  lastCheckCpuTime = 0;
171
221
  lastCheckOsCpus = null;
172
222
  lastSnapshotCpuUsage = null;
173
223
  lastSnapshotCpuTime = 0;
174
224
  lastSnapshotOsCpus = null;
175
- spikeCounter = 0;
225
+ spikeSince = { 'cpu': null, 'mem': null, 'eloop': null };
176
226
  heartbeatBuffer = null;
177
227
  heartbeatView = null;
178
228
  }
179
229
  /**
180
230
  * --- 启动看门狗 Worker 线程,独立事件循环监控主线程心跳 ---
181
231
  * --- 实现代码见 monitor/watchdog.ts ---
232
+ * @returns 无返回值
182
233
  */
183
234
  function startWatchdog() {
184
235
  if (!heartbeatBuffer) {
@@ -190,18 +241,35 @@ function startWatchdog() {
190
241
  'workerData': {
191
242
  'buffer': heartbeatBuffer,
192
243
  'logDir': kebab.LOG_CWD,
244
+ 'logFormat': lCore.globalConfig.logFormat ?? 'jsonl',
193
245
  'pid': process.pid,
194
- 'threshold': WATCHDOG_THRESHOLD,
246
+ 'threshold': watchdogThreshold,
195
247
  'cooldown': WATCHDOG_COOLDOWN,
196
248
  'interval': WATCHDOG_INTERVAL,
197
249
  'profileDuration': PROFILE_DURATION,
250
+ 'requests': Array.from(activeRequests.entries()),
198
251
  },
199
252
  });
200
253
  watchdog = worker;
201
254
  // --- 不阻止进程退出 ---
202
255
  worker.unref();
256
+ worker.on('message', (message) => {
257
+ if (watchdog !== worker || message.type !== 'diagnostic') {
258
+ return;
259
+ }
260
+ lastWatchdogDiagnostic = message;
261
+ const pending = watchdogIncidents.get(message.heartbeat);
262
+ if (pending) {
263
+ pending.result.status = `watchdog-${message.status}`;
264
+ pending.result.files = [`${message.dir}/blocked-event.json`, ...message.files];
265
+ pending.result.errors.push(...message.errors);
266
+ watchdogIncidents.delete(message.heartbeat);
267
+ saveIncident(pending.event);
268
+ }
269
+ });
203
270
  worker.on('error', (err) => {
204
- lCore.debug('[MONITOR] Watchdog error', err);
271
+ lCore.log({}, `WATCHDOG_ERROR ${lText.stringifyError(err)}`, '-monitor');
272
+ lCore.display('[MONITOR] Watchdog error', err);
205
273
  });
206
274
  worker.on('exit', (exitCode) => {
207
275
  if (exitCode !== 0 && timer) {
@@ -216,12 +284,14 @@ function startWatchdog() {
216
284
  lCore.debug(`[MONITOR] [THREAD] [${process.pid}] Watchdog started`);
217
285
  }
218
286
  catch (e) {
219
- lCore.debug('[MONITOR] Failed to start watchdog', e);
287
+ lCore.log({}, `WATCHDOG_START_FAILED ${lText.stringifyError(e)}`, '-monitor');
288
+ lCore.display('[MONITOR] Failed to start watchdog', e);
220
289
  scheduleWatchdogRestart();
221
290
  }
222
291
  }
223
292
  /**
224
293
  * --- 看门狗异常退出后延迟重启,避免失去阻塞检测能力 ---
294
+ * @returns 无返回值
225
295
  */
226
296
  function scheduleWatchdogRestart() {
227
297
  if (!timer || watchdog || watchdogRestartTimer) {
@@ -237,6 +307,7 @@ function scheduleWatchdogRestart() {
237
307
  * --- 注册一个活跃请求,返回追踪 ID ---
238
308
  * @param url 请求 URL
239
309
  * @param method 请求方法
310
+ * @returns 追踪 ID
240
311
  */
241
312
  export function track(url, method) {
242
313
  const now = Date.now();
@@ -250,28 +321,64 @@ export function track(url, method) {
250
321
  'startCpu': process.cpuUsage(),
251
322
  'startMem': process.memoryUsage.rss(),
252
323
  });
324
+ watchdog?.postMessage({ 'type': 'request', id, 'request': activeRequests.get(id) });
253
325
  return id;
254
326
  }
255
327
  /**
256
328
  * --- 移除已完成的请求追踪 ---
257
329
  * @param id 追踪 ID
330
+ * @returns 无返回值
258
331
  */
259
332
  export function untrack(id) {
333
+ const req = activeRequests.get(id);
334
+ if (req) {
335
+ const now = Date.now();
336
+ const completed = snapshotRequest(req, now, process.cpuUsage(), process.memoryUsage.rss());
337
+ completed.completedAt = now;
338
+ recentRequests.push(completed);
339
+ trimRecentRequests(now);
340
+ watchdog?.postMessage({ 'type': 'complete', id, 'request': completed });
341
+ }
260
342
  activeRequests.delete(id);
261
343
  }
344
+ /**
345
+ * --- 获取请求存续期间的进程资源增量,不能据此认定请求独占资源 ---
346
+ * @param req 追踪中的请求
347
+ * @param now 当前时间戳 ms
348
+ * @param cpu 当前进程 CPU 累计用量
349
+ * @param rss 当前进程 RSS
350
+ * @returns 请求快照
351
+ */
352
+ function snapshotRequest(req, now, cpu, rss) {
353
+ return {
354
+ 'url': req.url, 'method': req.method, 'duration': now - req.start,
355
+ 'cpuUser': cpu.user - req.startCpu.user, 'cpuSystem': cpu.system - req.startCpu.system,
356
+ 'memDelta': rss - req.startMem,
357
+ };
358
+ }
359
+ /**
360
+ * --- 限制最近请求的保留时长及条数 ---
361
+ * @param now 当前时间戳 ms
362
+ * @returns 无返回值
363
+ */
364
+ function trimRecentRequests(now) {
365
+ while (recentRequests.length > MAX_ACTIVE_REQUEST_DETAILS ||
366
+ recentRequests.length && now - (recentRequests[0].completedAt ?? 0) > 60_000) {
367
+ recentRequests.shift();
368
+ }
369
+ }
262
370
  /**
263
371
  * --- 获取当前资源快照 ---
372
+ * @returns 资源与请求快照
264
373
  */
265
374
  export function getSnapshot() {
266
- const mem = process.memoryUsage();
267
- const heapStats = v8.getHeapStatistics();
268
375
  const cpuUsage = process.cpuUsage();
269
- const now = Date.now();
376
+ const cpuTime = clockMs();
270
377
  // --- 计算进程 CPU 使用率(单核基准) ---
271
378
  let cpuProcess = 0;
272
379
  if (lastSnapshotCpuUsage && lastSnapshotCpuTime) {
273
380
  /** --- 经过的时间(微秒) --- */
274
- const elapsed = (now - lastSnapshotCpuTime) * 1_000;
381
+ const elapsed = (cpuTime - lastSnapshotCpuTime) * 1_000;
275
382
  const userDiff = cpuUsage.user - lastSnapshotCpuUsage.user;
276
383
  const sysDiff = cpuUsage.system - lastSnapshotCpuUsage.system;
277
384
  if (elapsed > 0) {
@@ -279,15 +386,30 @@ export function getSnapshot() {
279
386
  }
280
387
  }
281
388
  lastSnapshotCpuUsage = cpuUsage;
282
- lastSnapshotCpuTime = now;
389
+ lastSnapshotCpuTime = cpuTime;
283
390
  // --- 计算系统总 CPU 使用率 ---
284
391
  const osCpus = os.cpus();
285
392
  const cpuOs = getOsCpuPercent(lastSnapshotOsCpus, osCpus);
286
393
  lastSnapshotOsCpus = osCpus;
394
+ return createSnapshot(Math.round(cpuProcess * 100) / 100, cpuOs);
395
+ }
396
+ /**
397
+ * --- 组装资源与请求快照,不推进对外查询的 CPU 采样基准 ---
398
+ * @param cpuProcess 当前采样窗口进程 CPU 百分比
399
+ * @param cpuOs 当前采样窗口系统 CPU 百分比
400
+ * @returns 资源快照
401
+ */
402
+ function createSnapshot(cpuProcess, cpuOs) {
403
+ const mem = process.memoryUsage();
404
+ const heapStats = v8.getHeapStatistics();
405
+ const cpuUsage = process.cpuUsage();
406
+ const now = Date.now();
287
407
  // --- 计算事件循环延迟(P99,纳秒转毫秒) ---
288
408
  let eloopLag = 0;
409
+ let eloopMax = 0;
289
410
  if (eloopHistogram) {
290
411
  eloopLag = Math.round(eloopHistogram.percentile(99) / 1_000_000);
412
+ eloopMax = Math.round(eloopHistogram.max / 1_000_000);
291
413
  }
292
414
  // --- 收集活跃请求 ---
293
415
  const requests = [];
@@ -295,16 +417,9 @@ export function getSnapshot() {
295
417
  if (requests.length >= MAX_ACTIVE_REQUEST_DETAILS) {
296
418
  break;
297
419
  }
298
- const reqCpu = process.cpuUsage(req.startCpu);
299
- requests.push({
300
- 'url': req.url,
301
- 'method': req.method,
302
- 'duration': now - req.start,
303
- 'cpuUser': reqCpu.user,
304
- 'cpuSystem': reqCpu.system,
305
- 'memDelta': mem.rss - req.startMem,
306
- });
420
+ requests.push(snapshotRequest(req, now, cpuUsage, mem.rss));
307
421
  }
422
+ trimRecentRequests(now);
308
423
  return {
309
424
  'pid': process.pid,
310
425
  'time': now,
@@ -327,146 +442,239 @@ export function getSnapshot() {
327
442
  'free': os.freemem(),
328
443
  },
329
444
  'eloopLag': eloopLag,
445
+ 'eloopMax': eloopMax,
330
446
  'activeRequests': requests,
331
447
  'activeCount': activeRequests.size,
448
+ 'recentRequests': [...recentRequests],
332
449
  };
333
450
  }
334
451
  /**
335
- * --- 周期性检查,在超阈值时记录日志 ---
452
+ * --- 周期检查:独立确认各指标,按一次持续异常记录开始、现场及恢复 ---
453
+ * @returns 无返回值
336
454
  */
337
455
  function check() {
338
456
  const cpuUsage = process.cpuUsage();
339
457
  const now = Date.now();
340
- // --- 更新心跳时间戳,让看门狗线程知道主线程还活着 ---
341
- if (heartbeatView) {
342
- Atomics.store(heartbeatView, 0, Math.floor(now / 1000));
343
- // --- 更新调试模式标志,让看门狗线程知道当前是否在调试 ---
344
- Atomics.store(heartbeatView, 1, isDebugMode() ? 1 : 0);
345
- }
346
- /** --- 计算本周期进程 CPU 使用率(单核基准) --- */
458
+ const cpuTime = clockMs();
459
+ const previousTime = lastCheckCpuTime;
460
+ const actualElapsed = cpuTime - previousTime;
347
461
  let cpuPercent = 0;
348
- /** --- 实际经过的时间(ms),用于检测事件循环阻塞 --- */
349
- let actualElapsed = INTERVAL;
350
- if (lastCheckCpuUsage && lastCheckCpuTime) {
351
- actualElapsed = now - lastCheckCpuTime;
352
- const elapsedUs = actualElapsed * 1_000;
353
- const userDiff = cpuUsage.user - lastCheckCpuUsage.user;
354
- const sysDiff = cpuUsage.system - lastCheckCpuUsage.system;
355
- if (elapsedUs > 0) {
356
- cpuPercent = ((userDiff + sysDiff) / elapsedUs) * 100;
357
- }
462
+ if (lastCheckCpuUsage && actualElapsed > 0) {
463
+ cpuPercent = (cpuUsage.user - lastCheckCpuUsage.user + cpuUsage.system - lastCheckCpuUsage.system)
464
+ / (actualElapsed * 1_000) * 100;
358
465
  }
359
466
  lastCheckCpuUsage = cpuUsage;
360
- lastCheckCpuTime = now;
361
- // --- 计算系统总 CPU 使用率 ---
467
+ lastCheckCpuTime = cpuTime;
362
468
  const osCpus = os.cpus();
363
469
  const cpuOs = getOsCpuPercent(lastCheckOsCpus, osCpus);
364
470
  lastCheckOsCpus = osCpus;
365
- const memMB = process.memoryUsage.rss() / 1024 / 1024;
366
- let eloopLag = 0;
367
- if (eloopHistogram) {
368
- eloopLag = Math.round(eloopHistogram.percentile(99) / 1_000_000);
369
- // --- 重置直方图,避免历史数据影响 ---
370
- eloopHistogram.reset();
371
- }
372
- // --- 检测事件循环阻塞:实际间隔远大于预期说明事件循环曾被阻塞 ---
373
- if (actualElapsed > INTERVAL * 3) {
374
- // --- 调试模式下断点暂停会导致计时差异,跳过阻塞检测 ---
375
- if (isDebugMode()) {
376
- spikeCounter = 0;
377
- return;
471
+ if (heartbeatView) {
472
+ Atomics.store(heartbeatView, 0, BigInt(cpuTime));
473
+ Atomics.store(heartbeatView, 1, isDebugMode() ? 1n : 0n);
474
+ }
475
+ const snapshot = createSnapshot(Math.round(cpuPercent * 100) / 100, cpuOs);
476
+ // --- 采样时同步配置,框架 reload 后看门狗也使用当前日志格式 ---
477
+ watchdog?.postMessage({ 'type': 'snapshot', snapshot, 'logFormat': lCore.globalConfig.logFormat ?? 'jsonl' });
478
+ eloopHistogram?.reset();
479
+ // --- 调试暂停和同步诊断不能冒充业务阻塞,也不能连接暂停前后的异常计数 ---
480
+ if (isDebugMode()) {
481
+ spikeSince = { 'cpu': null, 'mem': null, 'eloop': null };
482
+ return;
483
+ }
484
+ if (diagnosticOverhead) {
485
+ diagnosticOverhead = false;
486
+ // --- 已确认事件继续等待正常采样恢复,未确认的连续性重新计时 ---
487
+ if (!incident?.reasons.includes('PROC_CPU')) {
488
+ spikeSince.cpu = null;
489
+ }
490
+ if (!incident?.reasons.includes('MEM')) {
491
+ spikeSince.mem = null;
492
+ }
493
+ if (!incident?.reasons.includes('ELOOP_LAG')) {
494
+ spikeSince.eloop = null;
378
495
  }
379
- const blockMs = actualElapsed - INTERVAL;
380
- const alerts = [
381
- `ELOOP_BLOCKED ${Math.round(blockMs)}ms`
382
- ];
383
- // --- 事件循环阻塞是严重事件,跳过 spikeCounter 直接记录 ---
384
- logSpike(alerts, cpuPercent, cpuOs, eloopLag, true);
385
- spikeCounter = 0;
386
496
  return;
387
497
  }
388
- // --- 判断是否超阈值 ---
498
+ const sample = {
499
+ 'time': now, 'elapsed': actualElapsed, 'cpu': snapshot.cpuProcess, 'cpuOs': cpuOs,
500
+ 'rss': snapshot.mem.rss, 'heapUsed': snapshot.mem.heapUsed,
501
+ 'external': snapshot.mem.external, 'arrayBuffers': snapshot.mem.arrayBuffers,
502
+ 'eloop': snapshot.eloopLag, 'eloopMax': snapshot.eloopMax,
503
+ };
504
+ sampleHistory.push(sample);
505
+ while (sampleHistory.length > 60) {
506
+ sampleHistory.shift();
507
+ }
389
508
  const alerts = [];
390
- if (cpuPercent >= cpuThreshold) {
391
- alerts.push(`PROC_CPU ${Math.round(cpuPercent * 100) / 100}% >= ${cpuThreshold}%`);
392
- }
393
- if (memMB >= memThreshold) {
394
- alerts.push(`MEM ${Math.round(memMB)}MB >= ${memThreshold}MB`);
395
- }
396
- if (eloopLag >= eloopThreshold) {
397
- alerts.push(`ELOOP_LAG ${eloopLag}ms >= ${eloopThreshold}ms`);
398
- }
399
- if (alerts.length) {
400
- ++spikeCounter;
401
- if (spikeCounter >= SPIKE_COUNT) {
402
- // --- 超阈值持续达标,记录日志 ---
403
- logSpike(alerts, cpuPercent, cpuOs, eloopLag);
404
- // --- 重置计数,防止持续刷日志 ---
405
- spikeCounter = 0;
406
- }
509
+ const blocked = actualElapsed - INTERVAL >= watchdogThreshold;
510
+ if (blocked) {
511
+ alerts.push('ELOOP_BLOCKED');
512
+ // --- 延迟检查只能证明这一窗口发生过阻塞,不能证明资源持续超阈值 ---
513
+ spikeSince = { 'cpu': null, 'mem': null, 'eloop': null };
407
514
  }
408
515
  else {
409
- spikeCounter = 0;
516
+ if (snapshot.cpuProcess >= cpuThreshold) {
517
+ spikeSince.cpu ??= previousTime;
518
+ if (cpuTime - spikeSince.cpu >= spikeDuration) {
519
+ alerts.push('PROC_CPU');
520
+ }
521
+ }
522
+ else {
523
+ spikeSince.cpu = null;
524
+ }
525
+ if (snapshot.mem.rss / 1024 / 1024 >= memThreshold) {
526
+ // --- 内存是时点值,首次超阈值之前的一个采样周期不能计入持续时间 ---
527
+ spikeSince.mem ??= cpuTime;
528
+ if (cpuTime - spikeSince.mem >= spikeDuration) {
529
+ alerts.push('MEM');
530
+ }
531
+ }
532
+ else {
533
+ spikeSince.mem = null;
534
+ }
535
+ if (snapshot.eloopLag >= eloopThreshold) {
536
+ spikeSince.eloop ??= previousTime;
537
+ if (cpuTime - spikeSince.eloop >= spikeDuration) {
538
+ alerts.push('ELOOP_LAG');
539
+ }
540
+ }
541
+ else {
542
+ spikeSince.eloop = null;
543
+ }
410
544
  }
545
+ if (!alerts.length) {
546
+ if (incident) {
547
+ incident.endedAt = now;
548
+ incident.endReason = 'recovered';
549
+ incident.activeReasons = [];
550
+ incident.recoveredSnapshot = snapshot;
551
+ saveIncident(incident);
552
+ lCore.log({}, `RECOVERED PID:${process.pid} EVENT:${incident.dir}`, '-monitor');
553
+ incident = null;
554
+ }
555
+ return;
556
+ }
557
+ recordIncident(alerts, snapshot, sample, blocked, previousTime);
411
558
  }
412
559
  /**
413
- * --- 将骤升信息写入日志,并采集诊断数据 ---
414
- * @param alerts 告警描述
415
- * @param cpuPercent 当前 CPU 百分比
416
- * @param cpuOs 系统 CPU 百分比
417
- * @param eloopLag 当前事件循环延迟
418
- * @param blocked 是否为事件循环阻塞后触发(watchdog 已在阻塞期间远程采集诊断)
560
+ * --- 串行保存事件摘要,重型诊断失败不影响基础现场保存 ---
561
+ * @param event 要保存的事件
562
+ * @returns 无返回值
419
563
  */
420
- function logSpike(alerts, cpuPercent, cpuOs, eloopLag, blocked = false) {
421
- const mem = process.memoryUsage();
422
- const heapStats = v8.getHeapStatistics();
423
- // --- 收集活跃请求详情 ---
424
- const now = Date.now();
425
- const requestDetails = [];
426
- for (const [, req] of activeRequests) {
427
- if (requestDetails.length >= MAX_ACTIVE_REQUEST_DETAILS) {
428
- break;
564
+ function saveIncident(event) {
565
+ const content = lText.stringifyJson(event, 2);
566
+ eventWrites = eventWrites.then(async () => {
567
+ if (!await lFs.mkdir(event.dir, 0o700) || !await lFs.chmod(event.dir, 0o700) ||
568
+ !await lFs.putContent(`${event.dir}event.json`, content, { 'mode': 0o600 }) ||
569
+ !await lFs.chmod(`${event.dir}event.json`, 0o600)) {
570
+ lCore.display('[MONITOR] Failed to save event:', event.dir);
571
+ }
572
+ }).catch((error) => {
573
+ lCore.display('[MONITOR] Failed to save event:', error);
574
+ });
575
+ }
576
+ /**
577
+ * --- 记录一次已确认异常;同一事件定期更新,新事件不受上一事件冷却限制 ---
578
+ * @param alerts 已确认的指标
579
+ * @param snapshot 检测现场快照
580
+ * @param sample 当前采样
581
+ * @param blocked 是否刚从主线程阻塞恢复
582
+ * @param previousTime 本采样窗口起点的单调时钟毫秒数
583
+ * @returns 无返回值
584
+ */
585
+ function recordIncident(alerts, snapshot, sample, blocked, previousTime) {
586
+ const now = snapshot.time;
587
+ const first = incident === null;
588
+ if (!incident) {
589
+ const date = new Date(now);
590
+ const dir = `${kebab.LOG_CWD}monitor/${lTime.format(null, 'Y/m/d/His', date)}-pid-${process.pid}-${now}/`;
591
+ incident = {
592
+ dir, 'startedAt': now - Math.max(0, clockMs() - Math.min(spikeSince.cpu ?? Infinity, spikeSince.mem ?? Infinity, spikeSince.eloop ?? Infinity, blocked ? previousTime : Infinity)),
593
+ 'confirmedAt': now, 'endedAt': null, 'endReason': null, 'reasons': [], 'activeReasons': [], 'impact': 'resource-high',
594
+ 'thresholds': {
595
+ 'cpu': cpuThreshold, 'mem': memThreshold, 'eloop': eloopThreshold,
596
+ 'duration': spikeDuration, 'blocked': watchdogThreshold,
597
+ },
598
+ 'before': [...sampleHistory], 'samples': [],
599
+ 'peaks': { 'cpu': 0, 'rss': 0, 'heapUsed': 0, 'eloop': 0 },
600
+ snapshot, 'diagnostics': [],
601
+ };
602
+ lastDiagnosticTime = 0;
603
+ lastEventSaveTime = 0;
604
+ }
605
+ const event = incident;
606
+ // --- 内存事件尚未恢复时,CPU 恢复后再次升高仍是新的采集时机 ---
607
+ const newReason = alerts.some(alert => !event.activeReasons.includes(alert));
608
+ event.activeReasons = [...alerts];
609
+ for (const alert of alerts) {
610
+ if (!event.reasons.includes(alert)) {
611
+ event.reasons.push(alert);
612
+ }
613
+ }
614
+ if (alerts.includes('ELOOP_BLOCKED') || alerts.includes('ELOOP_LAG')) {
615
+ event.impact = 'event-loop-delayed';
616
+ }
617
+ event.samples.push(sample);
618
+ if (event.samples.length > 300) {
619
+ event.samples.shift();
620
+ }
621
+ event.peaks.cpu = Math.max(event.peaks.cpu, sample.cpu);
622
+ event.peaks.rss = Math.max(event.peaks.rss, sample.rss);
623
+ event.peaks.heapUsed = Math.max(event.peaks.heapUsed, sample.heapUsed);
624
+ event.peaks.eloop = Math.max(event.peaks.eloop, sample.eloopMax, sample.elapsed - INTERVAL);
625
+ if (first || newReason) {
626
+ const msg = `RESOURCE_HIGH [${alerts.join(', ')}] PID:${process.pid} ` +
627
+ `PROC_CPU:${snapshot.cpuProcess}% RSS:${lText.sizeFormat(snapshot.mem.rss, '')} ` +
628
+ `ELOOP_LAG:${snapshot.eloopLag}ms ACTIVE_REQ:${snapshot.activeCount} EVENT:${event.dir}`;
629
+ lCore.log({}, msg, '-monitor');
630
+ lCore.display('[MONITOR]', msg);
631
+ }
632
+ if (first || newReason || clockMs() - lastDiagnosticTime >= DIAGNOSTIC_COOLDOWN) {
633
+ lastDiagnosticTime = clockMs();
634
+ const result = { 'time': now, 'status': 'pending', 'files': [], 'errors': [], snapshot };
635
+ event.diagnostics.push(result);
636
+ if (event.diagnostics.length > 60) {
637
+ event.diagnostics.shift();
638
+ }
639
+ const view = heartbeatView;
640
+ // --- 只有看门狗确实保存过这一窗口的现场时,才省略恢复后的重复 Profile ---
641
+ const watchdogCaptured = blocked && view !== null && Atomics.load(view, 4) >= BigInt(previousTime);
642
+ if (watchdogCaptured) {
643
+ const heartbeat = Number(Atomics.load(view, 4));
644
+ if (lastWatchdogDiagnostic?.heartbeat === heartbeat) {
645
+ result.status = `watchdog-${lastWatchdogDiagnostic.status}`;
646
+ result.files = [`${lastWatchdogDiagnostic.dir}/blocked-event.json`, ...lastWatchdogDiagnostic.files];
647
+ result.errors.push(...lastWatchdogDiagnostic.errors);
648
+ }
649
+ else {
650
+ result.status = 'watchdog-captured';
651
+ watchdogIncidents.set(heartbeat, { event, result });
652
+ }
653
+ }
654
+ else if (diagnosing || view && Atomics.load(view, 2) !== 0n) {
655
+ result.status = 'skipped-busy';
656
+ lastDiagnosticTime = clockMs() - DIAGNOSTIC_COOLDOWN + 5_000;
657
+ }
658
+ else {
659
+ diagnosing = true;
660
+ const ts = `${lTime.format(null, 'YmdHis', new Date(now))}-${now}`;
661
+ // --- 阻塞恢复后的 Profile 只能记录后续执行,摘要保留这个采集时机限制 ---
662
+ if (blocked) {
663
+ result.errors.push('The blocking stack was not captured; this profile starts after recovery.');
664
+ }
665
+ void collectDiagnostics(event.dir, ts, alerts.includes('PROC_CPU') || blocked, alerts.includes('MEM') && heapSnapshotEnabled, result, view).catch((error) => {
666
+ result.status = 'failed';
667
+ result.errors.push(lText.stringifyError(error));
668
+ }).finally(() => {
669
+ diagnosing = false;
670
+ saveIncident(event);
671
+ });
429
672
  }
430
- const reqCpu = process.cpuUsage(req.startCpu);
431
- const duration = now - req.start;
432
- const cpuTotal = reqCpu.user + reqCpu.system;
433
- const memDelta = mem.rss - req.startMem;
434
- requestDetails.push(`[${req.method}] ${req.url} (${duration}ms, PROC_CPU_DELTA: ${cpuTotal}us, PROC_RSS_DELTA: ${memDelta >= 0 ? '+' : '-'}${lText.sizeFormat(Math.abs(memDelta), '')})`);
435
- }
436
- const omittedRequests = activeRequests.size - requestDetails.length;
437
- if (omittedRequests > 0) {
438
- requestDetails.push(`... ${omittedRequests} more`);
439
- }
440
- // --- 诊断采集策略 ---
441
- // --- blocked=true:阻塞期间 watchdog 已通过 connectToMainThread() 远程采集了精确堆栈和 Profile ---
442
- // --- blocked=false:持续性骤升,此处由主线程自行采集 Report/Profile/HeapSnapshot ---
443
- if (!blocked && !diagnosing && now - lastDiagnosticTime >= DIAGNOSTIC_COOLDOWN) {
444
- lastDiagnosticTime = now;
445
- diagnosing = true;
446
- const hasCpuSpike = cpuPercent >= cpuThreshold;
447
- const hasMemSpike = (mem.rss / 1024 / 1024) >= memThreshold;
448
- const diagnosticTime = new Date();
449
- const ts = lTime.format(null, 'YmdHis', diagnosticTime);
450
- const diagDir = `${kebab.LOG_CWD}monitor/${lTime.format(null, 'Y/m/d/His', diagnosticTime)}-pid-${process.pid}/`;
451
- collectDiagnostics(diagDir, ts, hasCpuSpike, hasMemSpike && heapSnapshotEnabled).catch((e) => {
452
- lCore.debug('[MONITOR] Diagnostic collection failed', e);
453
- }).finally(() => {
454
- diagnosing = false;
455
- });
456
673
  }
457
- const msg = `SPIKE [${alerts.join(', ')}] ` +
458
- `PID:${process.pid} ` +
459
- `PROC_CPU:${Math.round(cpuPercent * 100) / 100}% ` +
460
- `OS_CPU:${cpuOs}% ` +
461
- `RSS:${lText.sizeFormat(mem.rss, '')} ` +
462
- `HEAP:${lText.sizeFormat(mem.heapUsed, '')}/${lText.sizeFormat(mem.heapTotal, '')} ` +
463
- `HEAP_LIMIT:${lText.sizeFormat(heapStats.heap_size_limit, '')} ` +
464
- `OS_MEM:${lText.sizeFormat(os.totalmem() - os.freemem(), '')}/${lText.sizeFormat(os.totalmem(), '')} ` +
465
- `ELOOP_LAG:${eloopLag}ms ` +
466
- `ACTIVE_REQ:${activeRequests.size} ` +
467
- `REQUESTS:[${requestDetails.join(' | ')}]`;
468
- lCore.log({}, msg, '-monitor');
469
- lCore.display('[MONITOR]', msg);
674
+ if (first || newReason || now - lastEventSaveTime >= 5_000) {
675
+ lastEventSaveTime = now;
676
+ saveIncident(event);
677
+ }
470
678
  }
471
679
  /**
472
680
  * --- 通过 os.cpus() 两次采样的 Delta 计算系统总 CPU 使用率 ---
@@ -499,109 +707,178 @@ function getOsCpuPercent(previous, current) {
499
707
  return Math.round(Math.max(0, Math.min(100, percent)) * 100) / 100;
500
708
  }
501
709
  /**
502
- * --- 采集诊断报告及按需 Profile,避免并行采集互相污染数据 ---
503
- * @param dir 诊断文件输出目录
504
- * @param ts 时间戳字符串
505
- * @param hasCpuSpike 是否发生 CPU 骤升
710
+ * --- 独立保存 Report/Profile/HeapSnapshot,单项失败不阻止其他采集 ---
711
+ * @param dir 诊断目录
712
+ * @param ts 唯一采集时间标识
713
+ * @param hasCpuSpike 是否需要 CPU Profile
506
714
  * @param collectHeapSnapshot 是否采集堆快照
715
+ * @param result 写入事件摘要的采集结果
716
+ * @param view 本次监控实例的共享内存
717
+ * @returns 无返回值,失败原因保留在 result 中
507
718
  */
508
- async function collectDiagnostics(dir, ts, hasCpuSpike, collectHeapSnapshot) {
509
- if (!await lFs.mkdir(dir, 0o700)) {
510
- throw new Error(`Failed to create diagnostic directory: ${dir}`);
511
- }
512
- if (!await lFs.chmod(dir, 0o700)) {
513
- throw new Error(`Failed to secure diagnostic directory: ${dir}`);
514
- }
515
- // --- Diagnostic Report 默认会包含命令行、环境变量和网络端点,落盘前显式脱敏 ---
516
- const reportName = `report-${ts}.json`;
517
- const report = process.report.getReport();
518
- if (report.header) {
519
- report.header.commandLine = [];
520
- delete report.header.networkInterfaces;
521
- }
522
- delete report.environmentVariables;
523
- for (const handle of report.libuv ?? []) {
524
- delete handle.localEndpoint;
525
- delete handle.remoteEndpoint;
526
- }
527
- const reportPath = `${dir}${reportName}`;
528
- const written = await lFs.putContent(reportPath, lText.stringifyJson(report, 2), { 'encoding': 'utf8', 'mode': 0o600 });
529
- if (!written) {
530
- throw new Error(`Failed to write diagnostic report: ${reportPath}`);
531
- }
532
- if (!await lFs.chmod(reportPath, 0o600)) {
533
- throw new Error(`Failed to secure diagnostic report: ${reportPath}`);
534
- }
535
- lCore.display(`[MONITOR] Diagnostic report: ${reportPath}`);
536
- if (hasCpuSpike) {
537
- await collectCpuProfile(dir, ts);
538
- }
539
- // --- 堆快照会同步阻塞并额外占用大量内存,仅在调用方明确开启时采集 ---
540
- if (collectHeapSnapshot) {
541
- const heapFile = v8.writeHeapSnapshot(`${dir}heap-${ts}.heapsnapshot`);
542
- if (!await lFs.chmod(heapFile, 0o600)) {
543
- throw new Error(`Failed to secure heap snapshot: ${heapFile}`);
719
+ async function collectDiagnostics(dir, ts, hasCpuSpike, collectHeapSnapshot, result, view) {
720
+ try {
721
+ if (!await lFs.mkdir(dir, 0o700) || !await lFs.chmod(dir, 0o700)) {
722
+ result.status = 'failed';
723
+ result.errors.push('Failed to create or secure diagnostic directory.');
724
+ return;
725
+ }
726
+ if (view !== heartbeatView) {
727
+ result.status = 'cancelled';
728
+ return;
729
+ }
730
+ const reportPath = `${dir}report-${ts}.json`;
731
+ try {
732
+ // --- 同步诊断本身的阻塞不作为业务异常;Worker 在这段时间只保留采样 ---
733
+ diagnosticOverhead = true;
734
+ if (view) {
735
+ Atomics.store(view, 3, 1n);
736
+ }
737
+ const report = process.report.getReport();
738
+ if (report.header) {
739
+ report.header.commandLine = [];
740
+ delete report.header.networkInterfaces;
741
+ }
742
+ delete report.environmentVariables;
743
+ for (const handle of report.libuv ?? []) {
744
+ delete handle.localEndpoint;
745
+ delete handle.remoteEndpoint;
746
+ }
747
+ if (view) {
748
+ Atomics.store(view, 0, BigInt(clockMs()));
749
+ Atomics.store(view, 3, 0n);
750
+ }
751
+ if (await lFs.putContent(reportPath, lText.stringifyJson(report, 2), { 'mode': 0o600 }) &&
752
+ await lFs.chmod(reportPath, 0o600)) {
753
+ result.files.push(reportPath);
754
+ }
755
+ else {
756
+ result.errors.push('Failed to write or secure diagnostic report.');
757
+ }
544
758
  }
545
- lCore.display(`[MONITOR] Heap snapshot: ${heapFile}`);
759
+ catch (error) {
760
+ result.errors.push(`Report: ${lText.stringifyError(error)}`);
761
+ }
762
+ finally {
763
+ if (view) {
764
+ Atomics.store(view, 0, BigInt(clockMs()));
765
+ Atomics.store(view, 3, 0n);
766
+ }
767
+ }
768
+ if (hasCpuSpike) {
769
+ if (await collectCpuProfile(dir, ts, result)) {
770
+ result.files.push(`${dir}cpu-${ts}.cpuprofile`);
771
+ }
772
+ else {
773
+ result.errors.push('CPU profile collection failed.');
774
+ }
775
+ }
776
+ if (view !== heartbeatView) {
777
+ result.status = 'cancelled';
778
+ return;
779
+ }
780
+ if (collectHeapSnapshot) {
781
+ // --- 堆快照会额外使用约两倍堆内存,不在剩余内存明显不足时尝试 ---
782
+ const constrained = process.constrainedMemory();
783
+ const available = constrained > 0
784
+ ? Math.min(os.freemem(), Math.max(0, constrained - process.memoryUsage.rss()))
785
+ : os.freemem();
786
+ if (available < v8.getHeapStatistics().total_heap_size * 2) {
787
+ result.errors.push('Heap snapshot skipped: insufficient available memory.');
788
+ }
789
+ else {
790
+ try {
791
+ diagnosticOverhead = true;
792
+ if (view) {
793
+ Atomics.store(view, 3, 1n);
794
+ }
795
+ const heapFile = v8.writeHeapSnapshot(`${dir}heap-${ts}.heapsnapshot`);
796
+ if (view) {
797
+ Atomics.store(view, 0, BigInt(clockMs()));
798
+ Atomics.store(view, 3, 0n);
799
+ }
800
+ if (await lFs.chmod(heapFile, 0o600)) {
801
+ result.files.push(heapFile);
802
+ }
803
+ else {
804
+ result.errors.push('Failed to secure heap snapshot.');
805
+ }
806
+ }
807
+ catch (error) {
808
+ result.errors.push(`Heap snapshot: ${lText.stringifyError(error)}`);
809
+ }
810
+ finally {
811
+ if (view) {
812
+ Atomics.store(view, 0, BigInt(clockMs()));
813
+ Atomics.store(view, 3, 0n);
814
+ }
815
+ }
816
+ }
817
+ }
818
+ result.status = result.errors.length ? 'partial' : 'complete';
819
+ }
820
+ catch (error) {
821
+ result.status = 'failed';
822
+ result.errors.push(lText.stringifyError(error));
546
823
  }
547
824
  }
548
825
  /**
549
- * --- 通过 Inspector 协议采集 CPU Profile ---
550
- * @param dir 诊断文件输出目录
551
- * @param ts 时间戳字符串
826
+ * --- 由 Worker 定时采集主线程 Profile,主线程再次阻塞也能按时停止并落盘 ---
827
+ * @param dir 诊断目录
828
+ * @param ts 唯一采集时间标识
829
+ * @param result 接收 Worker 的文件和失败原因
830
+ * @returns 是否保存成功;无可用 Worker、超时或监控停止时返回 false
552
831
  */
553
- async function collectCpuProfile(dir, ts) {
554
- if (profiling) {
555
- return;
832
+ function collectCpuProfile(dir, ts, result) {
833
+ if (profiling || !watchdog) {
834
+ return Promise.resolve(false);
556
835
  }
836
+ const worker = watchdog;
557
837
  profiling = true;
558
- const session = new inspector.Session();
559
- try {
560
- session.connect();
561
- await new Promise((resolve, reject) => {
562
- session.post('Profiler.enable', (err) => {
563
- if (err) {
564
- reject(err);
565
- return;
566
- }
567
- resolve();
568
- });
569
- });
570
- await new Promise((resolve, reject) => {
571
- session.post('Profiler.start', (err) => {
572
- if (err) {
573
- reject(err);
574
- return;
838
+ const file = `cpu-${ts}.cpuprofile`;
839
+ return new Promise((resolve) => {
840
+ let finished = false;
841
+ const onMessage = (message) => {
842
+ if (message.type === 'profile' && message.file === file) {
843
+ result.errors.push(...message.errors);
844
+ if (message.recorded) {
845
+ result.files.push(`${dir}profile-${ts}.json`);
575
846
  }
576
- resolve();
577
- });
578
- });
579
- // --- 采集指定时长 ---
580
- await lCore.sleep(PROFILE_DURATION);
581
- const profile = await new Promise((resolve, reject) => {
582
- session.post('Profiler.stop', (err, result) => {
583
- if (err) {
584
- reject(err);
585
- return;
586
- }
587
- resolve(result.profile);
588
- });
589
- });
590
- const filePath = `${dir}cpu-${ts}.cpuprofile`;
591
- const written = await lFs.putContent(filePath, lText.stringifyJson(profile), {
592
- 'encoding': 'utf8',
593
- 'mode': 0o600,
594
- });
595
- if (!written) {
596
- throw new Error(`Failed to write CPU profile: ${filePath}`);
847
+ finish(message.saved);
848
+ }
849
+ };
850
+ const onExit = () => {
851
+ result.errors.push('Profile aborted because the watchdog exited.');
852
+ finish(false);
853
+ };
854
+ const timeout = setTimeout(() => {
855
+ result.errors.push('Profile Worker response timed out.');
856
+ finish(false);
857
+ }, PROFILE_DURATION + 15_000);
858
+ /**
859
+ * --- 正常响应、退出和超时共享清理,旧 Worker 不会继续持有监听器 ---
860
+ * @param saved 是否保存成功
861
+ * @returns 无返回值
862
+ */
863
+ function finish(saved) {
864
+ if (finished) {
865
+ return;
866
+ }
867
+ finished = true;
868
+ clearTimeout(timeout);
869
+ worker.off('message', onMessage);
870
+ worker.off('exit', onExit);
871
+ profiling = false;
872
+ resolve(saved);
597
873
  }
598
- if (!await lFs.chmod(filePath, 0o600)) {
599
- throw new Error(`Failed to secure CPU profile: ${filePath}`);
874
+ worker.on('message', onMessage);
875
+ worker.once('exit', onExit);
876
+ try {
877
+ worker.postMessage({ 'type': 'profile', dir, ts });
600
878
  }
601
- lCore.display(`[MONITOR] CPU profile: ${filePath}`);
602
- }
603
- finally {
604
- session.disconnect();
605
- profiling = false;
606
- }
879
+ catch (error) {
880
+ lCore.display('[MONITOR] Failed to request CPU profile:', error);
881
+ finish(false);
882
+ }
883
+ });
607
884
  }