@maiyunnet/kebab 9.21.0 → 9.21.2
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/doc/kebab-rag.md +436 -10
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +59 -0
- package/lib/core.js +83 -2
- package/package.json +1 -1
- package/sys/master.js +54 -0
- package/sys/monitor/files.d.ts +34 -0
- package/sys/monitor/files.js +260 -0
- package/sys/monitor/watchdog.js +284 -215
- package/sys/monitor.d.ts +28 -3
- package/sys/monitor.js +527 -250
- package/www/example/ctr/test.d.ts +21 -0
- package/www/example/ctr/test.js +250 -2
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-
|
|
5
|
-
* ---
|
|
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 =
|
|
29
|
-
/** ---
|
|
30
|
-
|
|
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
|
-
/** ---
|
|
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
|
|
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
|
-
/** ---
|
|
83
|
+
/** --- 心跳共享内存,协议见 monitor/watchdog.ts --- */
|
|
74
84
|
let heartbeatBuffer = null;
|
|
75
85
|
/** --- 心跳共享内存视图 --- */
|
|
76
86
|
let heartbeatView = null;
|
|
77
|
-
/** ---
|
|
78
|
-
|
|
87
|
+
/** --- 主线程持续未更新心跳的确认时间,单位 ms --- */
|
|
88
|
+
let watchdogThreshold = 5_000;
|
|
79
89
|
/** --- 看门狗检测间隔,单位 ms --- */
|
|
80
|
-
const WATCHDOG_INTERVAL =
|
|
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
|
-
|
|
126
|
+
return false;
|
|
102
127
|
}
|
|
103
128
|
if (!Number.isFinite(nextMemThreshold) || nextMemThreshold < 0) {
|
|
104
|
-
|
|
129
|
+
return false;
|
|
105
130
|
}
|
|
106
131
|
if (!Number.isFinite(nextEloopThreshold) || nextEloopThreshold <= 0) {
|
|
107
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
// ---
|
|
134
|
-
heartbeatBuffer = new SharedArrayBuffer(
|
|
135
|
-
heartbeatView = new
|
|
136
|
-
Atomics.store(heartbeatView, 0,
|
|
137
|
-
Atomics.store(heartbeatView, 1, isDebugMode() ?
|
|
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
|
-
|
|
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':
|
|
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.
|
|
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.
|
|
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
|
|
376
|
+
const cpuTime = clockMs();
|
|
270
377
|
// --- 计算进程 CPU 使用率(单核基准) ---
|
|
271
378
|
let cpuProcess = 0;
|
|
272
379
|
if (lastSnapshotCpuUsage && lastSnapshotCpuTime) {
|
|
273
380
|
/** --- 经过的时间(微秒) --- */
|
|
274
|
-
const elapsed = (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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 =
|
|
361
|
-
// --- 计算系统总 CPU 使用率 ---
|
|
467
|
+
lastCheckCpuTime = cpuTime;
|
|
362
468
|
const osCpus = os.cpus();
|
|
363
469
|
const cpuOs = getOsCpuPercent(lastCheckOsCpus, osCpus);
|
|
364
470
|
lastCheckOsCpus = osCpus;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
|
415
|
-
* @
|
|
416
|
-
* @param cpuOs 系统 CPU 百分比
|
|
417
|
-
* @param eloopLag 当前事件循环延迟
|
|
418
|
-
* @param blocked 是否为事件循环阻塞后触发(watchdog 已在阻塞期间远程采集诊断)
|
|
560
|
+
* --- 串行保存事件摘要,重型诊断失败不影响基础现场保存 ---
|
|
561
|
+
* @param event 要保存的事件
|
|
562
|
+
* @returns 无返回值
|
|
419
563
|
*/
|
|
420
|
-
function
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
* ---
|
|
503
|
-
* @param dir
|
|
504
|
-
* @param ts
|
|
505
|
-
* @param hasCpuSpike
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
-
* ---
|
|
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
|
-
|
|
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
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
|
|
874
|
+
worker.on('message', onMessage);
|
|
875
|
+
worker.once('exit', onExit);
|
|
876
|
+
try {
|
|
877
|
+
worker.postMessage({ 'type': 'profile', dir, ts });
|
|
600
878
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
lCore.display('[MONITOR] Failed to request CPU profile:', error);
|
|
881
|
+
finish(false);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
607
884
|
}
|