Created
February 15, 2026 14:25
-
-
Save devm33/172e80f662625e445ba4eec4b0adfc07 to your computer and use it in GitHub Desktop.
Memory stress test for Copilot CLI alt-screen mode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment