print-memory-consumption.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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 '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) => console.log(` ${flag}`));
  265. }
  266. // Summary
  267. console.log('\n📋 Summary:');
  268. if (info.heapLimit) {
  269. const heapUsagePercent = (heapUsedMB / heapLimitMB) * 100;
  270. console.log(
  271. `Heap Memory: ${heapUsedMB.toFixed(2)} MB / ${heapLimitMB.toFixed(2)} MB (${heapUsagePercent.toFixed(2)}%)`,
  272. );
  273. console.log(
  274. heapUsagePercent > 80
  275. ? '⚠️ Consider increasing heap limit with --max-old-space-size if needed'
  276. : '✅ Memory usage is within healthy limits',
  277. );
  278. }
  279. console.log(''.padEnd(50, '='));
  280. console.log(`Retrieved at: ${new Date(info.timestamp).toLocaleString()}`);
  281. }
  282. }
  283. // Command line interface
  284. function parseArgs(): {
  285. host: string;
  286. port: number;
  287. json: boolean;
  288. help: boolean;
  289. } {
  290. const args = process.argv.slice(2);
  291. let host = 'localhost';
  292. let port = 9229;
  293. let json = false;
  294. let help = false;
  295. for (const arg of args) {
  296. if (arg.startsWith('--host=')) {
  297. host = arg.split('=')[1];
  298. } else if (arg.startsWith('--port=')) {
  299. port = parseInt(arg.split('=')[1]);
  300. } else if (arg === '--json') {
  301. json = true;
  302. } else if (arg === '--help' || arg === '-h') {
  303. help = true;
  304. }
  305. }
  306. return {
  307. host,
  308. port,
  309. json,
  310. help,
  311. };
  312. }
  313. function showHelp(): void {
  314. console.log(`
  315. Node.js Memory Checker
  316. Retrieves heap memory information from a running Node.js server via Chrome DevTools Protocol.
  317. Usage:
  318. node --experimental-strip-types --experimental-transform-types \\
  319. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  320. print-memory-consumption.ts [OPTIONS]
  321. Options:
  322. --host=HOST Debug host (default: localhost)
  323. --port=PORT Debug port (default: 9229)
  324. --json Output in JSON format
  325. --help, -h Show this help message
  326. Prerequisites:
  327. - Target Node.js server must be started with --inspect flag
  328. - WebSocket package: npm install ws @types/ws
  329. Example:
  330. # Check memory of server running on default debug port
  331. node --experimental-strip-types --experimental-transform-types \\
  332. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  333. print-memory-consumption.ts
  334. # Check with custom port and JSON output
  335. node --experimental-strip-types --experimental-transform-types \\
  336. --experimental-detect-module --no-warnings=ExperimentalWarning \\
  337. print-memory-consumption.ts --port=9230 --json
  338. `);
  339. }
  340. // Main execution
  341. async function main(): Promise<void> {
  342. const { host, port, json, help } = parseArgs();
  343. if (help) {
  344. showHelp();
  345. process.exit(0);
  346. }
  347. const checker = new NodeMemoryConsumptionChecker(host, port, json);
  348. const memoryInfo = await checker.checkMemory();
  349. if (memoryInfo) {
  350. checker.displayResults(memoryInfo);
  351. process.exit(0);
  352. } else {
  353. process.exit(1);
  354. }
  355. }
  356. // Execute if called directly
  357. if (import.meta.url === `file://${process.argv[1]}`) {
  358. main().catch((error) => {
  359. console.error('Fatal error:', error);
  360. process.exit(1);
  361. });
  362. }