print-memory-consumption.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. #!/usr/bin/env node
  2. /**
  3. * Node.js Memory Consumption checker
  4. *
  5. * Retrieves heap memory information from a running Node.js server
  6. * started with --inspect flag via Chrome DevTools Protocol
  7. *
  8. * Usage:
  9. * node --experimental-strip-types --experimental-transform-types \
  10. * --experimental-detect-module --no-warnings=ExperimentalWarning \
  11. * print-memory-consumption.ts [--port=9229] [--host=localhost] [--json]
  12. */
  13. import { get } from 'node:http';
  14. import WebSocket from 'ws';
  15. interface MemoryInfo {
  16. heapUsed: number;
  17. heapTotal: number;
  18. rss: number;
  19. external: number;
  20. arrayBuffers: number;
  21. heapLimit?: number;
  22. heapLimitSource: 'explicit' | 'estimated';
  23. architecture: string;
  24. platform: string;
  25. nodeVersion: string;
  26. pid: number;
  27. uptime: number;
  28. memoryFlags: string[];
  29. timestamp: number;
  30. }
  31. interface DebugTarget {
  32. webSocketDebuggerUrl: string;
  33. title: string;
  34. id: string;
  35. }
  36. class NodeMemoryConsumptionChecker {
  37. private host: string;
  38. private port: number;
  39. private outputJson: boolean;
  40. constructor(host = 'localhost', port = 9229, outputJson = false) {
  41. this.host = host;
  42. this.port = port;
  43. this.outputJson = outputJson;
  44. }
  45. // Helper method to convert bytes to MB
  46. private toMB(bytes: number): number {
  47. return bytes / 1024 / 1024;
  48. }
  49. // Helper method to get pressure status and icon
  50. private getPressureInfo(percentage: number): {
  51. status: string;
  52. icon: string;
  53. } {
  54. if (percentage > 90) return { status: 'HIGH PRESSURE', icon: '🔴' };
  55. if (percentage > 70) return { status: 'MODERATE PRESSURE', icon: '🟡' };
  56. return { status: 'LOW PRESSURE', icon: '🟢' };
  57. }
  58. // Helper method to create standard error
  59. private createError(message: string): Error {
  60. return new Error(message);
  61. }
  62. // Helper method to handle promise-based HTTP request
  63. private httpGet(url: string): Promise<string> {
  64. return new Promise((resolve, reject) => {
  65. get(url, (res) => {
  66. let data = '';
  67. res.on('data', (chunk) => {
  68. data += chunk;
  69. });
  70. res.on('end', () => resolve(data));
  71. }).on('error', (err) =>
  72. reject(this.createError(`Cannot connect to ${url}: ${err.message}`)),
  73. );
  74. });
  75. }
  76. // Generate JavaScript expression for memory collection
  77. private getMemoryCollectionScript(): string {
  78. return `JSON.stringify((() => {
  79. const mem = process.memoryUsage();
  80. const result = { ...mem, architecture: process.arch, platform: process.platform,
  81. nodeVersion: process.version, pid: process.pid, uptime: process.uptime(),
  82. timestamp: Date.now(), execArgv: process.execArgv };
  83. const memFlags = process.execArgv.filter(arg =>
  84. arg.includes('max-old-space-size') || arg.includes('max-heap-size'));
  85. result.memoryFlags = memFlags;
  86. const maxOldSpaceArg = memFlags.find(flag => flag.includes('max-old-space-size'));
  87. if (maxOldSpaceArg) {
  88. const match = maxOldSpaceArg.match(/max-old-space-size=(\\\\d+)/);
  89. if (match) result.explicitHeapLimit = parseInt(match[1]) * 1024 * 1024;
  90. }
  91. if (!result.explicitHeapLimit) {
  92. const is64bit = result.architecture === 'x64' || result.architecture === 'arm64';
  93. const nodeVersion = parseInt(result.nodeVersion.split('.')[0].slice(1));
  94. result.estimatedHeapLimit = is64bit
  95. ? (nodeVersion >= 14 ? 4 * 1024 * 1024 * 1024 : 1.7 * 1024 * 1024 * 1024)
  96. : 512 * 1024 * 1024;
  97. }
  98. return result;
  99. })())`;
  100. }
  101. async checkMemory(): Promise<MemoryInfo | null> {
  102. try {
  103. // Get debug targets
  104. const targets = await this.getDebugTargets();
  105. if (targets.length === 0) {
  106. throw new Error(
  107. 'No debug targets found. Is the Node.js server running with --inspect?',
  108. );
  109. }
  110. // Get memory information via WebSocket
  111. const memoryInfo = await this.getMemoryInfoViaWebSocket(targets[0]);
  112. return memoryInfo;
  113. } catch (error: unknown) {
  114. const errorMessage =
  115. error instanceof Error ? error.message : String(error);
  116. if (!this.outputJson) {
  117. console.error('❌ Error:', errorMessage);
  118. }
  119. return null;
  120. }
  121. }
  122. private async getDebugTargets(): Promise<DebugTarget[]> {
  123. const url = `http://${this.host}:${this.port}/json/list`;
  124. try {
  125. const data = await this.httpGet(url);
  126. return JSON.parse(data);
  127. } catch (e) {
  128. throw this.createError(`Failed to parse debug targets: ${e}`);
  129. }
  130. }
  131. private async getMemoryInfoViaWebSocket(
  132. target: DebugTarget,
  133. ): Promise<MemoryInfo> {
  134. return new Promise((resolve, reject) => {
  135. const ws = new WebSocket(target.webSocketDebuggerUrl);
  136. const timeout = setTimeout(() => {
  137. ws.close();
  138. reject(new Error('WebSocket connection timeout'));
  139. }, 10000);
  140. ws.on('open', () => {
  141. // Send Chrome DevTools Protocol message
  142. const message = JSON.stringify({
  143. id: 1,
  144. method: 'Runtime.evaluate',
  145. params: { expression: this.getMemoryCollectionScript() },
  146. });
  147. ws.send(message);
  148. });
  149. ws.on('message', (data: Buffer | string) => {
  150. clearTimeout(timeout);
  151. try {
  152. const response = JSON.parse(data.toString());
  153. if (response.result?.result?.value) {
  154. const rawData = JSON.parse(response.result.result.value);
  155. const memoryInfo: MemoryInfo = {
  156. heapUsed: rawData.heapUsed,
  157. heapTotal: rawData.heapTotal,
  158. rss: rawData.rss,
  159. external: rawData.external,
  160. arrayBuffers: rawData.arrayBuffers,
  161. heapLimit:
  162. rawData.explicitHeapLimit || rawData.estimatedHeapLimit,
  163. heapLimitSource: rawData.explicitHeapLimit
  164. ? 'explicit'
  165. : 'estimated',
  166. architecture: rawData.architecture,
  167. platform: rawData.platform,
  168. nodeVersion: rawData.nodeVersion,
  169. pid: rawData.pid,
  170. uptime: rawData.uptime,
  171. memoryFlags: rawData.memoryFlags || [],
  172. timestamp: rawData.timestamp,
  173. };
  174. resolve(memoryInfo);
  175. } else {
  176. reject(
  177. new Error(
  178. 'Invalid response format from Chrome DevTools Protocol',
  179. ),
  180. );
  181. }
  182. } catch (error) {
  183. reject(new Error(`Failed to parse WebSocket response: ${error}`));
  184. } finally {
  185. ws.close();
  186. }
  187. });
  188. ws.on('error', (error: Error) => {
  189. clearTimeout(timeout);
  190. reject(new Error(`WebSocket error: ${error.message}`));
  191. });
  192. });
  193. }
  194. displayResults(info: MemoryInfo): void {
  195. if (this.outputJson) {
  196. console.log(JSON.stringify(info, null, 2));
  197. return;
  198. }
  199. const [
  200. heapUsedMB,
  201. heapTotalMB,
  202. heapLimitMB,
  203. rssMB,
  204. externalMB,
  205. arrayBuffersMB,
  206. ] = [
  207. this.toMB(info.heapUsed),
  208. this.toMB(info.heapTotal),
  209. this.toMB(info.heapLimit || 0),
  210. this.toMB(info.rss),
  211. this.toMB(info.external),
  212. this.toMB(info.arrayBuffers),
  213. ];
  214. console.log('\n📊 Node.js Memory Information');
  215. console.log(''.padEnd(50, '='));
  216. // Current Memory Usage
  217. console.log('\n🔸 Current Memory Usage:');
  218. console.log(` Heap Used: ${heapUsedMB.toFixed(2)} MB`);
  219. console.log(` Heap Total: ${heapTotalMB.toFixed(2)} MB`);
  220. console.log(` RSS: ${rssMB.toFixed(2)} MB`);
  221. console.log(` External: ${externalMB.toFixed(2)} MB`);
  222. console.log(` Array Buffers: ${arrayBuffersMB.toFixed(2)} MB`);
  223. // Heap Limits
  224. console.log('\n🔸 Heap Limits:');
  225. if (info.heapLimit) {
  226. const limitType =
  227. info.heapLimitSource === 'explicit'
  228. ? 'Explicit Limit'
  229. : 'Default Limit';
  230. const limitSource =
  231. info.heapLimitSource === 'explicit'
  232. ? '(from --max-old-space-size)'
  233. : '(system default)';
  234. console.log(
  235. ` ${limitType}: ${heapLimitMB.toFixed(2)} MB ${limitSource}`,
  236. );
  237. console.log(
  238. ` Global Usage: ${((heapUsedMB / heapLimitMB) * 100).toFixed(2)}% of maximum`,
  239. );
  240. }
  241. // Heap Pressure Analysis
  242. const heapPressure = (info.heapUsed / info.heapTotal) * 100;
  243. const { status: pressureStatus, icon: pressureIcon } =
  244. this.getPressureInfo(heapPressure);
  245. console.log('\n� Memory Pressure Analysis:');
  246. console.log(
  247. ` Current Pool: ${pressureIcon} ${pressureStatus} (${heapPressure.toFixed(1)}% of allocated heap)`,
  248. );
  249. if (heapPressure > 90) {
  250. console.log(
  251. ' 📝 Note: High pressure is normal - Node.js will allocate more heap as needed',
  252. );
  253. }
  254. // System Information
  255. console.log('\n🔸 System Information:');
  256. console.log(` Architecture: ${info.architecture}`);
  257. console.log(` Platform: ${info.platform}`);
  258. console.log(` Node.js: ${info.nodeVersion}`);
  259. console.log(` Process ID: ${info.pid}`);
  260. console.log(` Uptime: ${(info.uptime / 60).toFixed(1)} minutes`);
  261. // Memory Flags
  262. if (info.memoryFlags.length > 0) {
  263. console.log('\n🔸 Memory Flags:');
  264. info.memoryFlags.forEach((flag) => {
  265. console.log(` ${flag}`);
  266. });
  267. }
  268. // Summary
  269. console.log('\n📋 Summary:');
  270. if (info.heapLimit) {
  271. const heapUsagePercent = (heapUsedMB / heapLimitMB) * 100;
  272. console.log(
  273. `Heap Memory: ${heapUsedMB.toFixed(2)} MB / ${heapLimitMB.toFixed(2)} MB (${heapUsagePercent.toFixed(2)}%)`,
  274. );
  275. console.log(
  276. heapUsagePercent > 80
  277. ? '⚠️ Consider increasing heap limit with --max-old-space-size if needed'
  278. : '✅ Memory usage is within healthy limits',
  279. );
  280. }
  281. console.log(''.padEnd(50, '='));
  282. console.log(`Retrieved at: ${new Date(info.timestamp).toLocaleString()}`);
  283. }
  284. }
  285. // Command line interface
  286. function parseArgs(): {
  287. host: string;
  288. port: number;
  289. json: boolean;
  290. help: boolean;
  291. } {
  292. const args = process.argv.slice(2);
  293. let host = 'localhost';
  294. let port = 9229;
  295. let json = false;
  296. let help = false;
  297. for (const arg of args) {
  298. if (arg.startsWith('--host=')) {
  299. host = arg.split('=')[1];
  300. } else if (arg.startsWith('--port=')) {
  301. port = parseInt(arg.split('=')[1]);
  302. } else if (arg === '--json') {
  303. json = true;
  304. } else if (arg === '--help' || arg === '-h') {
  305. help = true;
  306. }
  307. }
  308. return {
  309. host,
  310. port,
  311. json,
  312. help,
  313. };
  314. }
  315. function showHelp(): void {
  316. console.log(`
  317. Node.js Memory Checker
  318. Retrieves heap memory information from a running Node.js server via Chrome DevTools Protocol.
  319. Usage:
  320. node --experimental-strip-types --experimental-transform-types \\
  321. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  322. print-memory-consumption.ts [OPTIONS]
  323. Options:
  324. --host=HOST Debug host (default: localhost)
  325. --port=PORT Debug port (default: 9229)
  326. --json Output in JSON format
  327. --help, -h Show this help message
  328. Prerequisites:
  329. - Target Node.js server must be started with --inspect flag
  330. - WebSocket package: npm install ws @types/ws
  331. Example:
  332. # Check memory of server running on default debug port
  333. node --experimental-strip-types --experimental-transform-types \\
  334. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  335. print-memory-consumption.ts
  336. # Check with custom port and JSON output
  337. node --experimental-strip-types --experimental-transform-types \\
  338. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  339. print-memory-consumption.ts --port=9230 --json
  340. `);
  341. }
  342. // Main execution
  343. async function main(): Promise<void> {
  344. const { host, port, json, help } = parseArgs();
  345. if (help) {
  346. showHelp();
  347. process.exit(0);
  348. }
  349. const checker = new NodeMemoryConsumptionChecker(host, port, json);
  350. const memoryInfo = await checker.checkMemory();
  351. if (memoryInfo) {
  352. checker.displayResults(memoryInfo);
  353. process.exit(0);
  354. } else {
  355. process.exit(1);
  356. }
  357. }
  358. // Execute if called directly
  359. if (import.meta.url === `file://${process.argv[1]}`) {
  360. main().catch((error) => {
  361. console.error('Fatal error:', error);
  362. process.exit(1);
  363. });
  364. }