Files
redwoodjs__local-ci/scripts/rust-performance.mjs
Peter Pistorius 7a349fdb43 Add opt-in Rust native execution
## Problem

Agent CI still depended on the TypeScript runner as the main execution path. That made it harder to ship a small native binary and harder to prove the Rust path matched the TypeScript path. Review also found places where Rust and TypeScript could drift before the Rust path becomes the default.

## Solution

This PR adds the opt-in Rust native `agent-ci` binary and platform npm packages while keeping the TypeScript fallback. It splits the Rust code into `agent-ci-core`, `agent-ci-runtime`, and a thin CLI/orchestration crate. Pure planning, reusable workflow expansion, event contracts, and run-result contracts now live in core; runtime owns Docker, DTU, runner, macOS VM, workspace, execution-plan adapters, and the generic wave worker pool.

The fixture checks now use real TypeScript helpers instead of a shadow planner. Shared fixtures cover plans, events, run results, Docker socket resolution, and default job-limit CPU/memory vectors. The Rust runner honors `--jobs`, probes Docker memory for the default job limit, supports macOS VM concurrency limits, avoids nested runner container name collisions, has `--all` smoke coverage, and stages `bin/agent-ci` in every platform package.

## Validation

- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test --workspace --all-targets`
- `pnpm check`
- `pnpm fixtures:check`
- `pnpm golden:cli`
- `pnpm rust:smoke:parity`
- `pnpm agent-ci-dev run --all -q -p --json --github-token`

## Notes

Rust execution is still opt-in. The TypeScript implementation and fallback remain in place until the later default-switch and removal gates pass.
2026-05-24 15:22:20 +02:00

106 lines
3.4 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const rustBin = path.join(root, "target", "release", "agent-ci");
const tsCli = ["node", "packages/cli/dist/native-launcher.js"];
function runRequired(command, args) {
const result = spawnSync(command, args, { cwd: root, stdio: "inherit", env: process.env });
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function timed(command, args, env = {}) {
const start = performance.now();
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
env: { ...process.env, ...env },
});
const durationMs = performance.now() - start;
return { durationMs, status: result.status ?? 1, stdout: result.stdout, stderr: result.stderr };
}
function bench(label, command, args, options = {}) {
const iterations = options.iterations ?? 5;
const expectedStatuses = new Set(options.expectedStatuses ?? [0]);
const times = [];
for (let i = 0; i < iterations; i += 1) {
const result = timed(command, args, options.env);
if (!expectedStatuses.has(result.status)) {
throw new Error(
`${label}: unexpected exit ${result.status}\n${result.stdout}\n${result.stderr}`,
);
}
times.push(result.durationMs);
}
times.sort((a, b) => a - b);
const avg = times.reduce((sum, value) => sum + value, 0) / times.length;
return { label, min: times[0], avg, max: times.at(-1) };
}
runRequired("pnpm", ["--filter", "@redwoodjs/agent-ci", "build"]);
runRequired("cargo", ["build", "--release", "-p", "agent-ci"]);
const rows = [
bench("startup: TypeScript --help", tsCli[0], [...tsCli.slice(1), "--help"], {
env: { AGENT_CI_FORCE_TYPESCRIPT: "1" },
}),
bench("startup: Rust --help", rustBin, ["--help"]),
bench("workflow parse: TypeScript diff parser", "node", ["scripts/diff-parse.ts"], {
iterations: 3,
}),
bench(
"workflow parse: Rust smoke discovery",
rustBin,
["run", "--workflow", ".github/workflows/smoke-expressions.yml", "--quiet", "--no-matrix"],
{
iterations: 3,
expectedStatuses: [0, 1, 2],
},
),
bench(
"job orchestration: Rust matrix discovery",
rustBin,
["run", "--workflow", ".github/workflows/smoke-matrix.yml", "--quiet"],
{
iterations: 3,
expectedStatuses: [0, 1, 2],
},
),
];
function formatMs(value) {
return `${value.toFixed(1)} ms`;
}
const lines = [
"# Rust rewrite performance snapshot",
"",
`Generated by \`pnpm rust:perf\` on ${new Date().toISOString()}.`,
"",
"| Benchmark | min | avg | max |",
"| --- | ---: | ---: | ---: |",
...rows.map(
(row) =>
`| ${row.label} | ${formatMs(row.min)} | ${formatMs(row.avg)} | ${formatMs(row.max)} |`,
),
"",
"Notes:",
"",
"- Rust workflow execution parity is validated separately; workflow benchmarks time the release binary path and accept any known Agent CI exit status.",
"- The TypeScript workflow-parse comparison uses the existing differential parser script to avoid launching Docker jobs.",
"- Re-run before switching defaults so release notes include current numbers from the final release build.",
"",
];
const outPath = path.join(root, ".docs/rust-performance.md");
fs.writeFileSync(outPath, `${lines.join("\n")}`);
console.log(lines.join("\n"));