print-memory-consumption.ts 12 KB

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