#!/usr/bin/env node /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ /** * Memory-leak stress test for --alt-screen mode. * * Spawns dist-cli/index.js inside a pseudo-terminal, sends "ls -R /usr" * repeatedly, and logs RSS at each iteration so you can spot growth. * * Usage: * node script/mem-stress.mjs [cli_path] [iterations] [delay_seconds] * * Examples: * node script/mem-stress.mjs dist-cli/index.js 20 * node script/mem-stress.mjs ~/code/other/dist-cli/index.js 50 5 */ import { spawn } from "node-pty"; import { execSync } from "child_process"; import path from "path"; import { fileURLToPath } from "url"; import fs from "fs"; import os from "os"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); const CLI = path.join(ROOT, "dist-cli", "index.js"); const CLI_ARG = process.argv[2] || CLI; const CLI_PATH = path.resolve(CLI_ARG); const ITERATIONS = parseInt(process.argv[3] || "20", 10); const DELAY_S = parseInt(process.argv[4] || "4", 10); if (!fs.existsSync(CLI_PATH)) { console.error(`ERROR: ${CLI_PATH} not found. Run 'npm run build:cli' first.`); process.exit(1); } // Work in a temp dir so the agent doesn't modify the repo const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "mem-stress-")); process.chdir(workdir); function getRssKb(pid) { try { const out = execSync(`ps -o rss= -p ${pid}`, { encoding: "utf8" }); return parseInt(out.trim(), 10) || 0; } catch { return 0; } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } console.error(`\nMemory stress test: ${ITERATIONS} iterations, ${DELAY_S}s delay`); console.error(`CLI: ${CLI_PATH}`); console.error(`Workdir: ${workdir}\n`); // Header const header = "elapsed_s\trss_kb\trss_mb\titeration"; console.log(header); console.error(header); const pty = spawn("node", [CLI_PATH, "--alt-screen"], { name: "xterm-256color", cols: 120, rows: 40, cwd: workdir, env: { ...process.env, // Prevent the CLI from trying to authenticate TERM: "xterm-256color", }, }); const cliPid = pty.pid; const startTime = Date.now(); // Collect output to detect prompts let outputBuffer = ""; pty.onData((data) => { outputBuffer += data; // Keep buffer bounded if (outputBuffer.length > 50000) { outputBuffer = outputBuffer.slice(-25000); } }); function logSample(iteration) { const elapsed = Math.round((Date.now() - startTime) / 1000); const rss = getRssKb(cliPid); const rssMb = (rss / 1024).toFixed(1); const line = `${elapsed}\t${rss}\t${rssMb}\t${iteration}`; console.log(line); console.error(line); return rss; } async function run() { // Wait for the CLI to start console.error("Waiting for CLI to start..."); await sleep(5000); logSample("start"); const samples = []; for (let i = 1; i <= ITERATIONS; i++) { // Send the command pty.write("ls -R /usr\r"); // Wait for output to settle await sleep(DELAY_S * 1000); const rss = logSample(i); samples.push({ iteration: i, rss }); } // Final idle sample await sleep(3000); const finalRss = logSample("final"); samples.push({ iteration: "final", rss: finalRss }); // Summary const startRss = samples[0]?.rss || 0; const peakRss = Math.max(...samples.map((s) => s.rss)); const growth = finalRss - startRss; const growthPct = startRss > 0 ? ((growth / startRss) * 100).toFixed(1) : "N/A"; console.error("\n=== Summary ==="); console.error(`Iterations: ${ITERATIONS}`); console.error(`Start RSS: ${(startRss / 1024).toFixed(1)} MB`); console.error(`Peak RSS: ${(peakRss / 1024).toFixed(1)} MB`); console.error(`Final RSS: ${(finalRss / 1024).toFixed(1)} MB`); console.error(`Growth: ${(growth / 1024).toFixed(1)} MB (${growthPct}%)`); if (growth > startRss * 0.5) { console.error("\n⚠️ WARNING: RSS grew by more than 50% — possible memory leak!"); } else { console.error("\n✅ RSS growth looks reasonable."); } // Clean up pty.write("exit\r"); await sleep(1000); pty.kill(); fs.rmSync(workdir, { recursive: true, force: true }); } run().catch((err) => { console.error("Fatal error:", err); pty.kill(); process.exit(1); });