node-loop-detective 1.0.1 → 1.0.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/package.json +1 -1
- package/src/detective.js +23 -2
- package/src/reporter.js +28 -0
package/package.json
CHANGED
package/src/detective.js
CHANGED
|
@@ -56,7 +56,7 @@ class Detective extends EventEmitter {
|
|
|
56
56
|
* We inject a tiny lag-measuring snippet into the target process
|
|
57
57
|
*/
|
|
58
58
|
async _startLagDetection() {
|
|
59
|
-
// Inject a lag detector
|
|
59
|
+
// Inject a lag detector that also captures stack traces
|
|
60
60
|
const script = `
|
|
61
61
|
(function() {
|
|
62
62
|
if (globalThis.__loopDetective) {
|
|
@@ -66,12 +66,33 @@ class Detective extends EventEmitter {
|
|
|
66
66
|
let lastTime = Date.now();
|
|
67
67
|
const threshold = ${this.config.threshold};
|
|
68
68
|
|
|
69
|
+
// Capture stack trace at the point of lag detection
|
|
70
|
+
function captureStack() {
|
|
71
|
+
const orig = Error.stackTraceLimit;
|
|
72
|
+
Error.stackTraceLimit = 20;
|
|
73
|
+
const err = new Error();
|
|
74
|
+
Error.stackTraceLimit = orig;
|
|
75
|
+
// Parse the stack into structured frames
|
|
76
|
+
const frames = (err.stack || '').split('\\n').slice(2).map(line => {
|
|
77
|
+
const m = line.match(/at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?/);
|
|
78
|
+
if (m) return { fn: m[1] || '(anonymous)', file: m[2], line: +m[3], col: +m[4] };
|
|
79
|
+
const m2 = line.match(/at\\s+(.+)/);
|
|
80
|
+
if (m2) return { fn: m2[1], file: '', line: 0, col: 0 };
|
|
81
|
+
return null;
|
|
82
|
+
}).filter(Boolean).filter(f =>
|
|
83
|
+
!f.file.includes('loopDetective') &&
|
|
84
|
+
!f.fn.includes('Timeout.') &&
|
|
85
|
+
!f.file.includes('node:internal')
|
|
86
|
+
);
|
|
87
|
+
return frames;
|
|
88
|
+
}
|
|
89
|
+
|
|
69
90
|
const timer = setInterval(() => {
|
|
70
91
|
const now = Date.now();
|
|
71
92
|
const delta = now - lastTime;
|
|
72
93
|
const lag = delta - ${this.config.interval};
|
|
73
94
|
if (lag > threshold) {
|
|
74
|
-
lags.push({ lag, timestamp: now });
|
|
95
|
+
lags.push({ lag, timestamp: now, stack: captureStack() });
|
|
75
96
|
if (lags.length > 100) lags.shift();
|
|
76
97
|
}
|
|
77
98
|
lastTime = now;
|
package/src/reporter.js
CHANGED
|
@@ -33,6 +33,11 @@ class Reporter {
|
|
|
33
33
|
if (this.config.json) return;
|
|
34
34
|
const severity = data.lag > 500 ? COLORS.red : data.lag > 200 ? COLORS.yellow : COLORS.cyan;
|
|
35
35
|
this._print(`${severity}⚠ Event loop lag: ${data.lag}ms${COLORS.reset} ${COLORS.dim}at ${new Date(data.timestamp).toISOString()}${COLORS.reset}`);
|
|
36
|
+
if (data.stack && data.stack.length > 0) {
|
|
37
|
+
for (const frame of data.stack.slice(0, 5)) {
|
|
38
|
+
this._print(` ${COLORS.dim} → ${frame.fn} ${frame.file}:${frame.line}:${frame.col}${COLORS.reset}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
onProfile(analysis) {
|
|
@@ -143,6 +148,29 @@ class Reporter {
|
|
|
143
148
|
this._print(` Events: ${this.lagEvents.length}`);
|
|
144
149
|
this._print(` Max: ${maxLag}ms`);
|
|
145
150
|
this._print(` Avg: ${avgLag}ms`);
|
|
151
|
+
|
|
152
|
+
// Aggregate lag by code location
|
|
153
|
+
const locationMap = new Map();
|
|
154
|
+
for (const evt of this.lagEvents) {
|
|
155
|
+
if (evt.stack && evt.stack.length > 0) {
|
|
156
|
+
const top = evt.stack[0];
|
|
157
|
+
const key = `${top.fn} ${top.file}:${top.line}`;
|
|
158
|
+
const entry = locationMap.get(key) || { count: 0, totalLag: 0, maxLag: 0, frame: top };
|
|
159
|
+
entry.count++;
|
|
160
|
+
entry.totalLag += evt.lag;
|
|
161
|
+
entry.maxLag = Math.max(entry.maxLag, evt.lag);
|
|
162
|
+
locationMap.set(key, entry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (locationMap.size > 0) {
|
|
167
|
+
this._print(`\n ${COLORS.bold}Lag by Code Location:${COLORS.reset}`);
|
|
168
|
+
const sorted = [...locationMap.values()].sort((a, b) => b.totalLag - a.totalLag);
|
|
169
|
+
for (const loc of sorted.slice(0, 5)) {
|
|
170
|
+
this._print(` ${COLORS.yellow}${loc.frame.fn}${COLORS.reset} ${COLORS.dim}${loc.frame.file}:${loc.frame.line}${COLORS.reset}`);
|
|
171
|
+
this._print(` ${loc.count} events, total ${loc.totalLag}ms, max ${loc.maxLag}ms`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
this._print(`\n${'─'.repeat(60)}\n`);
|