diff --git a/.github/workflows/wsl-orcad-e2e.yml b/.github/workflows/wsl-orcad-e2e.yml index fc96f7ea76f..3d7b8e8390e 100644 --- a/.github/workflows/wsl-orcad-e2e.yml +++ b/.github/workflows/wsl-orcad-e2e.yml @@ -151,8 +151,8 @@ jobs: name: WSL2 distro (${{ matrix.family }}) needs: [template, container-rootfs] runs-on: windows-2025 - # Healthy runs take 4-11 min; a runner frozen during WSL2 setup is only reaped here (retried by wsl-orcad-e2e-frozen-runner-retry.yml). - timeout-minutes: 30 + # Healthy runs take 4-11 min plus the phase 3 restart baseline; a runner frozen during WSL2 setup is only reaped here (retried by wsl-orcad-e2e-frozen-runner-retry.yml). + timeout-minutes: 40 strategy: fail-fast: false matrix: @@ -355,8 +355,8 @@ jobs: if ($global:LASTEXITCODE -ne 0) { throw "WSL move real pause failed: $global:LASTEXITCODE" } wsl.exe --terminate $env:PLAIN_DISTRO # R40: with no terminal left, orcad idle-exits and the holder lets the distro stop on its own. - - name: 'Phase 3: idle orcad releases the distro' - timeout-minutes: 15 + - name: 'Phase 3: idle orcad releases the distro; distro restart to first output baseline' + timeout-minutes: 30 env: ORCA_RUN_WSL_ORCAD_LANE: '1' ORCA_WSL_ORCAD_LANE_PHASE: idle-release diff --git a/src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts b/src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts index ab7812636cb..34f5cc24cda 100644 --- a/src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts +++ b/src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts @@ -22,6 +22,8 @@ import { execCommand } from '../ssh/remote-exec/remote-exec-command' import { readConfirmedRunningWslDistros } from '../wsl-background-distro-gate' import { readWindowsProcessTableFresh } from '../windows/windows-process-table' import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory' +import { buildWslExecArgs, buildWslLoginShellCommand } from '../../shared/wsl-login-shell-command' +import { resolveWslExecutablePath } from './wsl-executable-path' import { createWslDistroHolder, type WslDistroHolder } from './wsl-distro-holder' import { WSL_DISTRO_HOLDER_POLL_SECONDS } from './wsl-distro-holder-command' import { wslDistroHolderKey } from './wsl-distro-holder-record' @@ -269,6 +271,105 @@ async function idleReleasePhase(): Promise { ).catch(() => vmWorkingSetKiB()) } +async function wslExe(args: string[], timeoutMs = 120_000) { + const result = await runProcess({ + program: resolveWslExecutablePath(), + args, + cwd: resolveWslInteropSpawnCwd(), + env: { ...process.env, WSL_UTF8: '1' }, + timeoutMs + }) + return { code: result.code, output: `${result.stdout}${result.stderr}` } +} + +async function terminatePlain(): Promise { + expect((await wslExe(['--terminate', PLAIN])).code).toBe(0) + await waitFor('the distro to stop', async () => ((await plainRunning()) ? null : true), 60_000) +} + +/** printf splits the token, so the typed command's echo never matches what the shell prints. */ +function outputProbe(): { command: string; token: string } { + const nonce = Math.random().toString(36).slice(2, 10) + return { command: `printf 'COLD_%s\\n' ${nonce}`, token: `COLD_${nonce}` } +} + +/** (a) WSL host: holder boots the distro, orcad deploys and starts, a terminal spawns and prints. */ +async function restartThroughHost(repoPath: string): Promise { + await terminatePlain() + createManager() + const started = performance.now() + const connection = await manager.ensure(PLAIN) + const ensureMs = Math.round(performance.now() - started) + const created = await rpc<{ terminal: { handle: string } }>(connection, 'terminal.create', { + worktree: `path:${repoPath}` + }) + const createdMs = Math.round(performance.now() - started) + const probe = outputProbe() + await rpc(connection, 'terminal.send', { + terminal: created.terminal.handle, + text: probe.command, + enter: true + }) + await waitFor( + 'the host terminal to print the probe', + async () => { + const read = await rpc<{ terminal: { tail?: unknown[] } }>(connection, 'terminal.read', { + terminal: created.terminal.handle + }) + return (read.terminal.tail ?? []).map(String).join('\n').includes(probe.token) ? true : null + }, + 120_000, + 100 + ) + const firstOutputMs = Math.round(performance.now() - started) + await rpc(connection, 'terminal.close', { terminal: created.terminal.handle }).catch(() => {}) + await manager.dispose() + return { ensureMs, terminalCreatedMs: createdMs - ensureMs, firstOutputMs } +} + +/** + * (b) Old bridge lower bound: one non-PTY `wsl.exe --exec` into the login shell. The real bridge + * also starts ConPTY and paints, so this undercounts its first paint. + */ +async function restartThroughBridgeExec(): Promise { + await terminatePlain() + const probe = outputProbe() + const started = performance.now() + const result = await wslExe( + // The same login-shell pick Orca's own WSL commands use (bash/zsh run -ilc, so rc files load). + buildWslExecArgs(PLAIN, ['sh', '-c', buildWslLoginShellCommand(probe.command)]) + ) + const firstOutputMs = Math.round(performance.now() - started) + expect(result.output).toContain(probe.token) + return { firstOutputMs } +} + +/** + * Distro restart (VM warm) to first command output, median of 3 each, alternating so neither path + * always runs on a warmer VM. + */ +async function distroRestartBaseline(): Promise { + const repoPath = (await guest('cd "$HOME/wsl-orcad-lane-repo" && pwd')).trim() + // One unrecorded bridge run warms the WSL VM, which the idle phase let shut down. + await restartThroughBridgeExec() + const host: Receipt[] = [] + const bridge: Receipt[] = [] + for (let i = 0; i < 3; i++) { + host.push(await restartThroughHost(repoPath)) + bridge.push(await restartThroughBridgeExec()) + } + const median = (runs: Receipt[], key: string): number => + runs.map((run) => Number(run[key])).toSorted((a, b) => a - b)[1] ?? 0 + receipt.distroRestartToFirstOutput = { + host, + bridgeExecLowerBound: bridge, + medianHostFirstOutputMs: median(host, 'firstOutputMs'), + medianHostEnsureMs: median(host, 'ensureMs'), + medianBridgeExecLowerBoundMs: median(bridge, 'firstOutputMs') + } + await terminatePlain() +} + describe.runIf(RUN && process.platform === 'win32')('WSL distro idle release', () => { beforeAll(() => { mkdirSync(USER_DATA, { recursive: true }) @@ -290,4 +391,11 @@ describe.runIf(RUN && process.platform === 'win32')('WSL distro idle release', ( idleReleasePhase, RELEASE_BUDGET_MS + 2 * HELD_WITH_TERMINAL_MS + 6 * 60_000 ) + + // After the idle phase: six distro restarts plus a warm-up, each bounded. + it( + 'measures distro restart to first output for the WSL host and an old-bridge exec lower bound', + distroRestartBaseline, + 20 * 60_000 + ) })