node-loop-detective 1.0.0 → 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/bin/cli.js CHANGED
@@ -2,32 +2,58 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const { parseArgs } = require('node:util');
6
5
  const { Detective } = require('../src/detective');
7
6
  const { Reporter } = require('../src/reporter');
8
7
 
9
- const options = {
10
- pid: { type: 'string', short: 'p' },
11
- port: { type: 'string', short: 'P', default: '' },
12
- duration: { type: 'string', short: 'd', default: '10' },
13
- threshold: { type: 'string', short: 't', default: '50' },
14
- interval: { type: 'string', short: 'i', default: '100' },
15
- json: { type: 'boolean', short: 'j', default: false },
16
- watch: { type: 'boolean', short: 'w', default: false },
17
- help: { type: 'boolean', short: 'h', default: false },
18
- version: { type: 'boolean', short: 'v', default: false },
19
- };
20
-
21
- let parsed;
22
- try {
23
- parsed = parseArgs({ options, allowPositionals: true });
24
- } catch (err) {
25
- console.error(`Error: ${err.message}\n`);
26
- printUsage();
27
- process.exit(1);
8
+ // Simple arg parser compatible with Node.js 16+
9
+ function parseCliArgs(argv) {
10
+ const args = argv.slice(2);
11
+ const values = {
12
+ pid: null,
13
+ port: null,
14
+ duration: '10',
15
+ threshold: '50',
16
+ interval: '100',
17
+ json: false,
18
+ watch: false,
19
+ help: false,
20
+ version: false,
21
+ };
22
+ const positionals = [];
23
+
24
+ const flagMap = {
25
+ '-p': 'pid', '--pid': 'pid',
26
+ '-P': 'port', '--port': 'port',
27
+ '-d': 'duration', '--duration': 'duration',
28
+ '-t': 'threshold', '--threshold': 'threshold',
29
+ '-i': 'interval', '--interval': 'interval',
30
+ };
31
+ const boolMap = {
32
+ '-j': 'json', '--json': 'json',
33
+ '-w': 'watch', '--watch': 'watch',
34
+ '-h': 'help', '--help': 'help',
35
+ '-v': 'version', '--version': 'version',
36
+ };
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
40
+ if (flagMap[arg]) {
41
+ values[flagMap[arg]] = args[++i] || '';
42
+ } else if (boolMap[arg]) {
43
+ values[boolMap[arg]] = true;
44
+ } else if (!arg.startsWith('-')) {
45
+ positionals.push(arg);
46
+ } else {
47
+ console.error(`Unknown option: ${arg}\n`);
48
+ printUsage();
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ return { values, positionals };
28
54
  }
29
55
 
30
- const { values, positionals } = parsed;
56
+ const { values, positionals } = parseCliArgs(process.argv);
31
57
 
32
58
  if (values.version) {
33
59
  const pkg = require('../package.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-loop-detective",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Detect event loop blocking & lag in running Node.js apps without code changes or restarts",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 into the target process
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`);