node-loop-detective 1.2.0 → 1.4.0
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/README.md +9 -0
- package/bin/cli.js +35 -1
- package/package.json +1 -1
- package/src/detective.js +3 -3
package/README.md
CHANGED
|
@@ -74,6 +74,12 @@ loop-detective -p 12345 -d 30 -t 100
|
|
|
74
74
|
# Detect slow I/O with a 1-second threshold
|
|
75
75
|
loop-detective -p 12345 --io-threshold 1000
|
|
76
76
|
|
|
77
|
+
# Connect to a remote inspector (Docker, K8s, remote server)
|
|
78
|
+
loop-detective --host 192.168.1.100 --port 9229
|
|
79
|
+
|
|
80
|
+
# Save raw CPU profile for Chrome DevTools / speedscope
|
|
81
|
+
loop-detective -p 12345 --save-profile ./profile.cpuprofile
|
|
82
|
+
|
|
77
83
|
# Continuous monitoring mode
|
|
78
84
|
loop-detective -p 12345 --watch
|
|
79
85
|
|
|
@@ -86,11 +92,13 @@ loop-detective -p 12345 --json
|
|
|
86
92
|
| Flag | Description | Default |
|
|
87
93
|
|------|-------------|---------|
|
|
88
94
|
| `-p, --pid <pid>` | Target Node.js process ID | — |
|
|
95
|
+
| `-H, --host <host>` | Inspector host (remote connections) | 127.0.0.1 |
|
|
89
96
|
| `-P, --port <port>` | Inspector port (skip SIGUSR1) | — |
|
|
90
97
|
| `-d, --duration <sec>` | Profiling duration in seconds | 10 |
|
|
91
98
|
| `-t, --threshold <ms>` | Event loop lag threshold | 50 |
|
|
92
99
|
| `-i, --interval <ms>` | Lag sampling interval | 100 |
|
|
93
100
|
| `--io-threshold <ms>` | Slow I/O threshold | 500 |
|
|
101
|
+
| `--save-profile <path>` | Save raw CPU profile to file | — |
|
|
94
102
|
| `-j, --json` | Output as JSON | false |
|
|
95
103
|
| `-w, --watch` | Continuous monitoring | false |
|
|
96
104
|
|
|
@@ -124,6 +132,7 @@ const { Detective } = require('node-loop-detective');
|
|
|
124
132
|
|
|
125
133
|
const detective = new Detective({
|
|
126
134
|
pid: 12345,
|
|
135
|
+
inspectorHost: '127.0.0.1', // or remote host
|
|
127
136
|
duration: 10000,
|
|
128
137
|
threshold: 50,
|
|
129
138
|
interval: 100,
|
package/bin/cli.js
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
const { Detective } = require('../src/detective');
|
|
6
6
|
const { Reporter } = require('../src/reporter');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
7
9
|
|
|
8
10
|
// Simple arg parser compatible with Node.js 16+
|
|
9
11
|
function parseCliArgs(argv) {
|
|
10
12
|
const args = argv.slice(2);
|
|
11
13
|
const values = {
|
|
12
14
|
pid: null,
|
|
15
|
+
host: null,
|
|
13
16
|
port: null,
|
|
14
17
|
duration: '10',
|
|
15
18
|
threshold: '50',
|
|
16
19
|
interval: '100',
|
|
17
20
|
'io-threshold': '500',
|
|
21
|
+
'save-profile': null,
|
|
18
22
|
json: false,
|
|
19
23
|
watch: false,
|
|
20
24
|
help: false,
|
|
@@ -24,11 +28,13 @@ function parseCliArgs(argv) {
|
|
|
24
28
|
|
|
25
29
|
const flagMap = {
|
|
26
30
|
'-p': 'pid', '--pid': 'pid',
|
|
31
|
+
'-H': 'host', '--host': 'host',
|
|
27
32
|
'-P': 'port', '--port': 'port',
|
|
28
33
|
'-d': 'duration', '--duration': 'duration',
|
|
29
34
|
'-t': 'threshold', '--threshold': 'threshold',
|
|
30
35
|
'-i': 'interval', '--interval': 'interval',
|
|
31
36
|
'--io-threshold': 'io-threshold',
|
|
37
|
+
'--save-profile': 'save-profile',
|
|
32
38
|
};
|
|
33
39
|
const boolMap = {
|
|
34
40
|
'-j': 'json', '--json': 'json',
|
|
@@ -85,14 +91,17 @@ function printUsage() {
|
|
|
85
91
|
loop-detective <pid>
|
|
86
92
|
loop-detective --pid <pid>
|
|
87
93
|
loop-detective --port <inspector-port>
|
|
94
|
+
loop-detective --host <remote-host> --port <inspector-port>
|
|
88
95
|
|
|
89
96
|
OPTIONS
|
|
90
97
|
-p, --pid <pid> Target Node.js process ID
|
|
98
|
+
-H, --host <host> Inspector host (default: 127.0.0.1)
|
|
91
99
|
-P, --port <port> Connect to an already-open inspector port
|
|
92
100
|
-d, --duration <sec> Profiling duration in seconds (default: 10)
|
|
93
101
|
-t, --threshold <ms> Event loop lag threshold in ms (default: 50)
|
|
94
102
|
-i, --interval <ms> Sampling interval in ms (default: 100)
|
|
95
103
|
--io-threshold <ms> Slow I/O threshold in ms (default: 500)
|
|
104
|
+
--save-profile <path> Save raw CPU profile to .cpuprofile file
|
|
96
105
|
-j, --json Output results as JSON
|
|
97
106
|
-w, --watch Continuous monitoring mode
|
|
98
107
|
-h, --help Show this help
|
|
@@ -102,6 +111,7 @@ function printUsage() {
|
|
|
102
111
|
loop-detective 12345
|
|
103
112
|
loop-detective --pid 12345 --duration 30 --threshold 100
|
|
104
113
|
loop-detective --port 9229 --watch
|
|
114
|
+
loop-detective --host 192.168.1.100 --port 9229
|
|
105
115
|
loop-detective -p 12345 -d 5 -j
|
|
106
116
|
|
|
107
117
|
HOW IT WORKS
|
|
@@ -116,11 +126,13 @@ function printUsage() {
|
|
|
116
126
|
async function main() {
|
|
117
127
|
const config = {
|
|
118
128
|
pid: pid ? parseInt(pid, 10) : null,
|
|
129
|
+
inspectorHost: values.host || '127.0.0.1',
|
|
119
130
|
inspectorPort,
|
|
120
131
|
duration: parseInt(values.duration, 10) * 1000,
|
|
121
132
|
threshold: parseInt(values.threshold, 10),
|
|
122
133
|
interval: parseInt(values.interval, 10),
|
|
123
134
|
ioThreshold: parseInt(values['io-threshold'], 10),
|
|
135
|
+
saveProfile: values['save-profile'],
|
|
124
136
|
watch: values.watch,
|
|
125
137
|
json: values.json,
|
|
126
138
|
};
|
|
@@ -128,10 +140,32 @@ async function main() {
|
|
|
128
140
|
const reporter = new Reporter(config);
|
|
129
141
|
const detective = new Detective(config);
|
|
130
142
|
|
|
143
|
+
// Security warning for remote connections
|
|
144
|
+
if (config.inspectorHost !== '127.0.0.1' && config.inspectorHost !== 'localhost') {
|
|
145
|
+
reporter.onInfo(`⚠ Warning: Connecting to remote host ${config.inspectorHost}. The CDP protocol has no authentication — ensure the network is trusted.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
detective.on('connected', () => reporter.onConnected());
|
|
132
149
|
detective.on('lag', (data) => reporter.onLag(data));
|
|
133
150
|
detective.on('slowIO', (data) => reporter.onSlowIO(data));
|
|
134
|
-
detective.on('profile', (
|
|
151
|
+
detective.on('profile', (analysis, rawProfile) => {
|
|
152
|
+
reporter.onProfile(analysis);
|
|
153
|
+
|
|
154
|
+
// Save raw CPU profile if requested
|
|
155
|
+
if (config.saveProfile && rawProfile) {
|
|
156
|
+
try {
|
|
157
|
+
const filePath = path.resolve(config.saveProfile);
|
|
158
|
+
fs.writeFileSync(filePath, JSON.stringify(rawProfile));
|
|
159
|
+
if (!config.json) {
|
|
160
|
+
console.log(`\n \x1b[32m✔\x1b[0m CPU profile saved to ${filePath}`);
|
|
161
|
+
console.log(` Open in Chrome DevTools: Performance tab → Load profile`);
|
|
162
|
+
console.log(` Or visit https://www.speedscope.app\n`);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(`\n \x1b[31m✖ Failed to save profile: ${err.message}\x1b[0m\n`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
135
169
|
detective.on('error', (err) => reporter.onError(err));
|
|
136
170
|
detective.on('disconnected', () => reporter.onDisconnected());
|
|
137
171
|
|
package/package.json
CHANGED
package/src/detective.js
CHANGED
|
@@ -406,7 +406,7 @@ class Detective extends EventEmitter {
|
|
|
406
406
|
this._activateInspector();
|
|
407
407
|
const port = await this._findInspectorPort();
|
|
408
408
|
|
|
409
|
-
this.inspector = new Inspector({ port });
|
|
409
|
+
this.inspector = new Inspector({ host: this.config.inspectorHost, port });
|
|
410
410
|
await this.inspector.connect();
|
|
411
411
|
this.emit('connected');
|
|
412
412
|
|
|
@@ -424,7 +424,7 @@ class Detective extends EventEmitter {
|
|
|
424
424
|
|
|
425
425
|
const profile = await this._captureProfile(this.config.duration);
|
|
426
426
|
const analysis = this.analyzer.analyzeProfile(profile);
|
|
427
|
-
this.emit('profile', analysis);
|
|
427
|
+
this.emit('profile', analysis, profile);
|
|
428
428
|
} finally {
|
|
429
429
|
await this.stop();
|
|
430
430
|
}
|
|
@@ -444,7 +444,7 @@ class Detective extends EventEmitter {
|
|
|
444
444
|
try {
|
|
445
445
|
const profile = await this._captureProfile(this.config.duration);
|
|
446
446
|
const analysis = this.analyzer.analyzeProfile(profile);
|
|
447
|
-
this.emit('profile', analysis);
|
|
447
|
+
this.emit('profile', analysis, profile);
|
|
448
448
|
} catch (err) {
|
|
449
449
|
this.emit('error', err);
|
|
450
450
|
}
|