print-memory-consumption.ts 12 KB

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