From 619859e1be491b12a66f8c8cb3e236dfaf8a3a04 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sat, 25 Apr 2026 22:10:53 +0800 Subject: [PATCH 01/19] ci(pm-bench-phases): phase-isolated bench + standalone manifest-bench + PR auto-comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a comprehensive benchmark infrastructure to support data-driven review of subsequent perf PRs. Infrastructure layers: 1. Phase-isolated bench (`pm-bench-phases.yml` + `e2e/pm-bench-phases.sh`) - Splits a full install into `p0_full_cold`, `p1_resolve`, `p3_cold_install`, `p4_warm_link` so each phase's wall + RSS + ctx-switches + IO + network metrics is captured separately - Runs both `utoo` and `bun` under hyperfine for direct comparison - Per-iteration prepare via generated bash scripts (lockfile + cache + node_modules wiped or selectively kept per phase) - Triggers on `bench-phases` PR label so it stays opt-in and runs before the default-branch merge gate - Captures `/usr/bin/time -v` per phase for memory+ctx visibility 2. Standalone manifest-bench (`crates/manifest-bench`) - New non-published crate: a single binary that fires parallel HTTP GETs for every package name in a `package-lock.json`, with no parsing / no resolver state / no caching. Same `reqwest + rustls + aws-lc-rs + tokio` stack as ruborist. - Exposes the same diag fields (wall, busy, sum, avg_conc, p50, p95, p99, max) so its output is directly comparable to ruborist's `Preload HTTP diag` log line - CLI flags sweep concurrency / H1 vs H2 / single-version endpoint / User-Agent — useful for empirically establishing the HTTP stack ceiling under the same Cloudflare conditions as the real bench - Wired into the workflow as a sweep step after the phase bench 3. PCAP capture (`e2e/pm-bench-pcap.sh`) - Single-iteration tcpdump of the resolve phase per PM, scoped to the registry SNI, uploaded as artifact for offline analysis 4. PR auto-comment - Workflow extracts the `## p_` summary tables from the bench output and upserts them into a single sticky PR comment keyed by an HTML marker (re-runs replace, not append) - Uses `actions/github-script@v7`; only fires on `bench-phases` -labeled PRs so workflow_dispatch + push runs do not spam - Job declares `permissions: pull-requests: write` to allow comment writes on a normally read-only fork-PR token Why this lands first: subsequent perf PRs (preload worker-pool, manifest cache cleanup, install path tweaks) need this in place so the PR description can cite real before/after numbers and the PR comment can show the per-commit delta. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-bench-phases.yml | 288 ++++++++++++++++++ Cargo.lock | 53 ++++ Cargo.toml | 1 + crates/manifest-bench/Cargo.toml | 37 +++ crates/manifest-bench/src/main.rs | 371 +++++++++++++++++++++++ e2e/pm-bench-pcap.sh | 83 ++++++ e2e/pm-bench-phases.sh | 412 ++++++++++++++++++++++++++ e2e/pm-bench.sh | 30 +- 8 files changed, 1269 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/pm-bench-phases.yml create mode 100644 crates/manifest-bench/Cargo.toml create mode 100644 crates/manifest-bench/src/main.rs create mode 100755 e2e/pm-bench-pcap.sh create mode 100755 e2e/pm-bench-phases.sh diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml new file mode 100644 index 0000000000..4bcabca57e --- /dev/null +++ b/.github/workflows/pm-bench-phases.yml @@ -0,0 +1,288 @@ +name: utoopm-benchmark-phases + +on: + workflow_dispatch: + inputs: + project: + description: 'Project to benchmark (under github.com/ant-design/)' + required: false + default: 'ant-design' + type: string + registry: + description: 'Registry URL' + required: false + default: 'https://registry.npmjs.org' + type: string + runs: + description: 'Runs per phase' + required: false + default: '3' + type: string + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-linux: + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.label.name == 'bench-phases') + runs-on: ubuntu-latest + container: + image: ubuntu:20.04 + steps: + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git + - uses: actions/checkout@v4 + - name: Configure git safe directory + run: git config --global --add safe.directory /__w/utoo/utoo + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-bench-linux + - name: Build current branch utoo + manifest-bench + run: | + cargo build --release --bin utoo -p utoo-pm + cargo build --release -p manifest-bench + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-linux-bench-phases + path: target/release/utoo + retention-days: 1 + - name: Upload manifest-bench binary + uses: actions/upload-artifact@v4 + with: + name: manifest-bench-linux + path: target/release/manifest-bench + retention-days: 1 + + bench: + needs: [build-linux] + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.label.name == 'bench-phases') + runs-on: ubuntu-latest + # Comment-writing on PRs needs `pull-requests: write`. Other tokens + # default to read-only on this workflow's events. Granular instead + # of `write-all` to keep the workflow's blast radius minimal. + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-bench-phases + path: dist + - name: Download manifest-bench binary + uses: actions/download-artifact@v4 + with: + name: manifest-bench-linux + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo dist/manifest-bench + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install hyperfine, GNU time, tcpdump and dnsutils + run: sudo apt-get update && sudo apt-get install -y hyperfine time tcpdump dnsutils + - name: Verify tools + run: | + hyperfine --version + utoo --version + bun --version + - name: Run phase-isolated benchmark + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + BENCH_RUNS: ${{ github.event.inputs.runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + chmod +x e2e/pm-bench-phases.sh + # Capture full output to a file so the PR comment step can + # extract the per-phase summary tables. `tee` keeps live + # progress visible in the workflow log. + mkdir -p /tmp/pm-bench-output + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases.log + + - name: Standalone manifest-bench (HTTP-only fetch sweep) + env: + REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + run: | + set -eu + # Generate a project lockfile to source the manifest names from. + # Use bun (which we already have) to do a quick lockfile-only + # install so manifest-bench has a real workload to fire against. + PROJECT_DIR="/tmp/pm-bench/$PROJECT" + if [ ! -d "$PROJECT_DIR" ]; then + mkdir -p /tmp/pm-bench + git clone --depth 1 "https://github.com/ant-design/$PROJECT" "$PROJECT_DIR" + fi + cd "$PROJECT_DIR" + if [ ! -f package-lock.json ]; then + echo "==> generating lockfile via utoo" + utoo deps --registry "$REGISTRY" || true + fi + ls -la package-lock.json || { echo "no lockfile, skip"; exit 0; } + + echo + echo "============================================================" + echo "manifest-bench: HTTP-only fetch (no parse, no resolver)" + echo " Goal: isolate reqwest/rustls/tokio behaviour from" + echo " ruborist's resolver pipeline. Same numbers (wall, busy," + echo " avg_conc, p50/p95/max) as ruborist's Preload HTTP diag." + echo "============================================================" + for CAP in 32 64 96 128 192 256; do + echo + echo "--- concurrency=$CAP, h1, full manifest, default UA ---" + manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency "$CAP" --reps 2 --http1-only || true + done + + echo + echo "--- concurrency=128, h2 negotiate, full manifest, default UA ---" + manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 || true + + echo + echo "--- concurrency=128, h1, single-version endpoint ---" + manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 --http1-only --single-version || true + + echo + echo "--- concurrency=128, h1, UA=Bun/1.2.21 ---" + manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 --http1-only --user-agent "Bun/1.2.21" || true + + - name: Capture pcap (resolve phase, single run per PM) + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + run: | + chmod +x e2e/pm-bench-pcap.sh + bash e2e/pm-bench-pcap.sh + + - name: Upload pcap artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-pcap + path: /tmp/pm-bench-pcap + retention-days: 3 + + # Post bench results as a PR comment. Triggers only on + # `bench-phases`-labeled PRs (the existing label trigger above), so + # workflow_dispatch + push runs do not spam comments. The comment + # body is the per-phase summary tables (plain text, kept compact). + # The action upserts a single sticky comment per PR keyed by an + # HTML marker so re-runs replace rather than append. + - name: Post bench results to PR comment + if: > + always() && + github.event_name == 'pull_request' && + github.event.label.name == 'bench-phases' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = '/tmp/pm-bench-output/bench-phases.log'; + if (!fs.existsSync(path)) { + core.warning(`No bench output at ${path}`); + return; + } + const raw = fs.readFileSync(path, 'utf8'); + + // Strip ANSI color escape codes + const stripAnsi = s => s.replace(/\x1b\[[0-9;]*m/g, ''); + const cleaned = stripAnsi(raw); + + // Pull out the per-phase summary tables. The pm-bench-phases.sh + // script prints sections delimited by `## p_` headers + // followed by tab-separated wall / RSS / IO / context-switch / + // network / cache / lockfile rows. Capture from each `## ` to + // the next `## ` (or end). + const sections = []; + const re = /^## p\d+_[a-z0-9_]+.*$/gm; + const headers = []; + let m; + while ((m = re.exec(cleaned)) !== null) { + headers.push({ idx: m.index, name: m[0].trim() }); + } + for (let i = 0; i < headers.length; i++) { + const start = headers[i].idx; + const end = i + 1 < headers.length ? headers[i + 1].idx : cleaned.length; + const body = cleaned.slice(start, end).trim(); + sections.push(body); + } + + const marker = ''; + const sha = context.sha.slice(0, 7); + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + let bodyParts = [ + marker, + `## pm-bench-phases · \`${sha}\``, + '', + `[Workflow run](${runUrl}) — ant-design / npmjs.org / ubuntu-latest`, + '', + ]; + + if (sections.length === 0) { + bodyParts.push('_No phase summary tables found in bench output. See workflow log._'); + } else { + for (const sec of sections) { + bodyParts.push('```'); + bodyParts.push(sec); + bodyParts.push('```'); + bodyParts.push(''); + } + } + + const body = bodyParts.join('\n'); + + // Upsert: find an existing comment with our marker, edit it; + // otherwise create a new one. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + core.info(`Updated existing bench comment ${existing.id}`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + core.info('Created new bench comment'); + } diff --git a/Cargo.lock b/Cargo.lock index 46d2141e2e..9207794977 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -573,6 +588,27 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "browserslist-data" version = "0.1.4" @@ -1110,6 +1146,7 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" dependencies = [ + "brotli", "compression-core", "flate2", "memchr", @@ -4901,6 +4938,21 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" +[[package]] +name = "manifest-bench" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "futures", + "reqwest 0.12.24", + "rustls", + "rustls-native-certs", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "markdown" version = "1.0.0" @@ -7244,6 +7296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", diff --git a/Cargo.toml b/Cargo.toml index 3caf94264e..769cac33c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/manifest-bench", "crates/pack-api", "crates/pack-cli", "crates/pack-core", diff --git a/crates/manifest-bench/Cargo.toml b/crates/manifest-bench/Cargo.toml new file mode 100644 index 0000000000..5b01e57c0a --- /dev/null +++ b/crates/manifest-bench/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "manifest-bench" +version = "0.0.0" +edition = "2024" +license = "MIT" +publish = false +description = "Standalone HTTP-only manifest fetch benchmark, isolating network behaviour from ruborist's resolver pipeline." + +[[bin]] +name = "manifest-bench" +path = "src/main.rs" + +# tombi: format.rules.table-keys-order.disabled = true +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +futures = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs", "time"] } + +# Identical TLS / DNS choices to ruborist so we measure the *protocol* +# characteristics of the same stack, not a different implementation. +reqwest = { version = "0.12", default-features = false, features = [ + "brotli", + "gzip", + "http2", + "rustls-tls-native-roots-no-provider", + "socks" +] } +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "logging", + "std", + "tls12" +] } +rustls-native-certs = "0.8" diff --git a/crates/manifest-bench/src/main.rs b/crates/manifest-bench/src/main.rs new file mode 100644 index 0000000000..fa70f3fe42 --- /dev/null +++ b/crates/manifest-bench/src/main.rs @@ -0,0 +1,371 @@ +//! Standalone HTTP-only manifest fetch benchmark. +//! +//! Isolates the network behaviour of `reqwest + rustls + tokio` from +//! ruborist's resolver pipeline (BFS, dedup, parse, lockfile, project +//! cache). Reads a list of package names, builds manifest URLs, fires +//! parallel `GET` requests, records `(start, end)` per request, and +//! reports the same diag shape as ruborist's `Preload HTTP diag` line. +//! +//! Two input modes: +//! - `--names-file ` — newline-separated package names +//! - `--lockfile ` — a npm-style package-lock.json; we extract +//! the `packages.*` (v3) or `dependencies.*` (v2) keys +//! +//! Two registry modes: +//! - `/` — full manifest endpoint (default, npmjs) +//! - `//latest` — single-version endpoint +//! (gated behind `--single-version`) +//! +//! Each request reads the body to completion (we only measure I/O, no +//! parse). Output: same fields as preload's HTTP diag for direct +//! comparison. + +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Instant; + +use anyhow::{Context, Result, anyhow}; +use clap::Parser; +use futures::stream::{FuturesUnordered, StreamExt}; + +#[derive(Parser, Debug)] +#[command( + name = "manifest-bench", + about = "HTTP-only manifest fetch bench (no parse, no resolver)" +)] +struct Args { + /// Registry base URL. + #[arg(long, default_value = "https://registry.npmjs.org")] + registry: String, + + /// File of newline-separated package names. Mutually exclusive with `--lockfile`. + #[arg(long, conflicts_with = "lockfile")] + names_file: Option, + + /// `package-lock.json` file. Reads top-level `packages.*.name` keys. + #[arg(long)] + lockfile: Option, + + /// Maximum concurrent in-flight requests. + #[arg(long, default_value_t = 128)] + concurrency: usize, + + /// Number of times to repeat the whole sweep (each iteration is a + /// fresh `reqwest::Client`, so connection pool / TLS handshake + /// costs are paid each time, matching `hyperfine` cold-start). + #[arg(long, default_value_t = 1)] + reps: usize, + + /// Use the single-version endpoint `//latest` instead of the + /// full-manifest endpoint `/`. Smaller bodies, more requests + /// served per byte. + #[arg(long)] + single_version: bool, + + /// Override `Accept` header. Default mimics ruborist's preload + /// (`application/vnd.npm.install-v1+json` — abbreviated metadata). + #[arg(long)] + accept: Option, + + /// Override `User-Agent`. Default uses reqwest's default. Try + /// `Bun/1.x.x` to test whether Cloudflare differentiates by UA. + #[arg(long)] + user_agent: Option, + + /// Force HTTP/1.1 (no H2 negotiation). Default lets ALPN decide. + #[arg(long)] + http1_only: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + let names = load_names(&args)?; + if names.is_empty() { + return Err(anyhow!("no package names found in input")); + } + + println!( + "manifest-bench: registry={} concurrency={} reps={} names={} h1_only={} single_version={} accept={} ua={}", + args.registry, + args.concurrency, + args.reps, + names.len(), + args.http1_only, + args.single_version, + args.accept.as_deref().unwrap_or(""), + args.user_agent.as_deref().unwrap_or(""), + ); + + for rep in 1..=args.reps { + run_once(&args, &names, rep).await?; + } + + Ok(()) +} + +fn load_names(args: &Args) -> Result> { + if let Some(path) = &args.names_file { + let raw = std::fs::read_to_string(path).with_context(|| format!("read {path:?}"))?; + return Ok(raw + .lines() + .map(str::trim) + .filter(|s| !s.is_empty() && !s.starts_with('#')) + .map(str::to_string) + .collect()); + } + + if let Some(path) = &args.lockfile { + let raw = std::fs::read_to_string(path).with_context(|| format!("read {path:?}"))?; + return extract_lockfile_names(&raw); + } + + Err(anyhow!("provide --names-file or --lockfile")) +} + +/// Pull unique package names from an npm v3 lockfile (`packages.*`) +/// or an older v2 lockfile (`dependencies.*`). +fn extract_lockfile_names(raw: &str) -> Result> { + use std::collections::BTreeSet; + + let v: serde_json::Value = serde_json::from_str(raw).context("parse lockfile JSON")?; + let mut names: BTreeSet = BTreeSet::new(); + + if let Some(packages) = v.get("packages").and_then(|p| p.as_object()) { + for key in packages.keys() { + if key.is_empty() { + continue; + } + // npm v3 packages key like "node_modules/foo" or + // "node_modules/@scope/bar/node_modules/baz" — take the + // last path segment (or @scope/name pair). + let last = last_module_name(key); + if !last.is_empty() { + names.insert(last); + } + } + } else if let Some(deps) = v.get("dependencies").and_then(|d| d.as_object()) { + for key in deps.keys() { + names.insert(key.clone()); + } + } + + Ok(names.into_iter().collect()) +} + +fn last_module_name(key: &str) -> String { + let parts: Vec<&str> = key.split("node_modules/").collect(); + let tail = parts.last().copied().unwrap_or(""); + tail.to_string() +} + +#[derive(Debug)] +struct ReqResult { + start: Instant, + end: Instant, + bytes: usize, + status: u16, +} + +async fn run_once(args: &Args, names: &[String], rep: usize) -> Result<()> { + // Build a fresh client per rep — matches hyperfine's cold-start + // assumption that each iteration pays the TLS handshake cost. + let client = build_client(args)?; + let registry = Arc::new(args.registry.trim_end_matches('/').to_string()); + let accept = Arc::new( + args.accept + .clone() + .unwrap_or_else(|| "application/vnd.npm.install-v1+json".to_string()), + ); + + let single_version = args.single_version; + let concurrency = args.concurrency; + + let phase_start = Instant::now(); + let mut futs = FuturesUnordered::new(); + let mut idx = 0usize; + let mut results: Vec = Vec::with_capacity(names.len()); + + while idx < names.len() && futs.len() < concurrency { + spawn_one( + &client, + ®istry, + &names[idx], + &accept, + single_version, + &mut futs, + ); + idx += 1; + } + + while let Some(res) = futs.next().await { + results.push(res); + if idx < names.len() { + spawn_one( + &client, + ®istry, + &names[idx], + &accept, + single_version, + &mut futs, + ); + idx += 1; + } + } + let phase_wall_ms = phase_start.elapsed().as_millis(); + + report(rep, &results, phase_wall_ms); + Ok(()) +} + +type Fut = std::pin::Pin + Send>>; + +fn spawn_one( + client: &reqwest::Client, + registry: &Arc, + name: &str, + accept: &Arc, + single_version: bool, + futs: &mut FuturesUnordered, +) { + let url = if single_version { + format!("{registry}/{name}/latest") + } else { + format!("{registry}/{name}") + }; + let client = client.clone(); + let accept = Arc::clone(accept); + futs.push(Box::pin(async move { + let start = Instant::now(); + let req = client.get(&url).header("accept", accept.as_str()).send(); + let (bytes, status) = match req.await { + Ok(resp) => { + let status = resp.status().as_u16(); + let body = resp.bytes().await.map(|b| b.len()).unwrap_or(0); + (body, status) + } + Err(_) => (0, 0), + }; + let end = Instant::now(); + ReqResult { + start, + end, + bytes, + status, + } + })); +} + +fn build_client(args: &Args) -> Result { + // Install aws-lc-rs as the default crypto provider (idempotent — + // first call wins). Matches ruborist's `service::http` setup. + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + let mut roots = rustls::RootCertStore::empty(); + let native = rustls_native_certs::load_native_certs(); + for cert in native.certs { + let _ = roots.add(cert); + } + + let tls_config = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new( + rustls::crypto::aws_lc_rs::default_provider(), + )) + .with_safe_default_protocol_versions() + .map_err(|e| anyhow!("rustls protocol versions: {e}"))? + .with_root_certificates(roots) + .with_no_client_auth(); + + let mut builder = reqwest::Client::builder() + .use_preconfigured_tls(tls_config) + .no_proxy() + .pool_max_idle_per_host(256); + if args.http1_only { + builder = builder.http1_only(); + } + if let Some(ua) = &args.user_agent { + builder = builder.user_agent(ua); + } + builder.build().context("build reqwest client") +} + +fn report(rep: usize, results: &[ReqResult], wall_ms: u128) { + if results.is_empty() { + eprintln!("[rep {rep}] no results"); + return; + } + + let mut spans: Vec<(Instant, Instant)> = results.iter().map(|r| (r.start, r.end)).collect(); + spans.sort_by_key(|(s, _)| *s); + + let first_start = spans.first().unwrap().0; + let last_end = spans.iter().map(|(_, e)| *e).max().unwrap(); + let win_wall = last_end.duration_since(first_start).as_millis(); + + let mut per_us: Vec = spans + .iter() + .map(|(s, e)| e.duration_since(*s).as_micros()) + .collect(); + per_us.sort_unstable(); + let n = per_us.len(); + let pct = |p: usize| per_us[(n * p).div_ceil(100).saturating_sub(1)]; + let sum: u128 = per_us.iter().sum(); + let p50 = per_us[n / 2]; + + let mut busy_us: u128 = 0; + let (mut cur_s, mut cur_e) = spans[0]; + for &(s, e) in &spans[1..] { + if s <= cur_e { + if e > cur_e { + cur_e = e; + } + } else { + busy_us += cur_e.duration_since(cur_s).as_micros(); + cur_s = s; + cur_e = e; + } + } + busy_us += cur_e.duration_since(cur_s).as_micros(); + + let bytes_total: usize = results.iter().map(|r| r.bytes).sum(); + let ok = results.iter().filter(|r| r.status == 200).count(); + let err = results.iter().filter(|r| r.status == 0).count(); + let four_xx = results + .iter() + .filter(|r| (400..500).contains(&r.status)) + .count(); + let five_xx = results + .iter() + .filter(|r| (500..600).contains(&r.status)) + .count(); + + let avg_conc = if busy_us > 0 { + sum as f64 / busy_us as f64 + } else { + 0.0 + }; + + println!( + "[rep {rep}] n={} phase_wall={}ms win_wall={}ms busy={}ms ({:.0}%) sum={}ms avg_conc={:.1} p50={}ms p95={}ms p99={}ms max={}ms bytes={} 200={} 4xx={} 5xx={} err={}", + n, + wall_ms, + win_wall, + busy_us / 1000, + if win_wall > 0 { + 100.0 * (busy_us as f64 / 1000.0) / win_wall as f64 + } else { + 0.0 + }, + sum / 1000, + avg_conc, + p50 / 1000, + pct(95) / 1000, + pct(99) / 1000, + per_us.last().unwrap() / 1000, + bytes_total, + ok, + four_xx, + five_xx, + err, + ); +} diff --git a/e2e/pm-bench-pcap.sh b/e2e/pm-bench-pcap.sh new file mode 100755 index 0000000000..c8e869d78e --- /dev/null +++ b/e2e/pm-bench-pcap.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# One-off network trace capture: runs a single cold p1_resolve per PM with +# tcpdump active, so we can inspect connection topology (IPs, concurrency, +# timing) on CI after local evidence was inconclusive. +# +# Outputs: +# $PCAP_DIR/dns.txt — A records for the registry host +# $PCAP_DIR/{bun,utoo}.pcap — tcpdump capture of the resolve phase +# $PCAP_DIR/{bun,utoo}.log — stdout/stderr of the run +set -eo pipefail + +PROJECT=${PROJECT:-ant-design} +REGISTRY=${REGISTRY:-https://registry.npmjs.org} +BENCH_DIR=${BENCH_DIR:-/tmp/pm-bench} +PCAP_DIR=${PCAP_DIR:-/tmp/pm-bench-pcap} +UTOO_CACHE=/tmp/utoo-pcap-cache +BUN_CACHE=/tmp/bun-pcap-cache +export BUN_INSTALL_CACHE_DIR="$BUN_CACHE" + +mkdir -p "$PCAP_DIR" + +# Project must already be cloned by pm-bench-phases.sh (this script is meant +# to run after it, reusing the clone). +PROJECT_DIR="$BENCH_DIR/$PROJECT" +if [ ! -d "$PROJECT_DIR" ]; then + echo "missing $PROJECT_DIR — run pm-bench-phases.sh first" >&2 + exit 1 +fi + +# Extract hostname (strip scheme + any path) for DNS lookup. +HOST=$(echo "$REGISTRY" | sed -E 's#^https?://([^/]+).*#\1#') +echo "=== DNS records for $HOST ===" +{ + echo "# getent hosts" + getent hosts "$HOST" || true + echo + echo "# dig +short A" + dig +short A "$HOST" || true + echo + echo "# dig +short AAAA" + dig +short AAAA "$HOST" || true +} | tee "$PCAP_DIR/dns.txt" + +IFACE=$(ip route | awk '/default/ {print $5; exit}') +echo "capturing on interface: $IFACE" + +capture_one() { + local name=$1 + shift + local pcap="$PCAP_DIR/$name.pcap" + local log="$PCAP_DIR/$name.log" + + cd "$PROJECT_DIR" + # Cold: nothing reused. + rm -f package-lock.json bun.lock + rm -rf "$UTOO_CACHE" "$BUN_CACHE" node_modules + + echo "=== capturing $name ===" + # tcpdump as root; ubuntu-latest runners allow sudo without password. + sudo tcpdump -s 0 -i "$IFACE" -w "$pcap" 'tcp port 443' >/dev/null 2>&1 & + local tcpdump_pid=$! + # Let tcpdump bind before the workload starts. + sleep 1 + + /usr/bin/time -v "$@" >"$log" 2>&1 || echo " run exited with $?" + + sudo kill "$tcpdump_pid" 2>/dev/null || true + wait "$tcpdump_pid" 2>/dev/null || true + + # Make pcap readable by the later upload-artifact step (tcpdump writes + # as root). + sudo chmod 644 "$pcap" + echo " → $pcap ($(wc -c <"$pcap") bytes), log: $log" +} + +capture_one utoo \ + utoo deps --registry="$REGISTRY" --cache-dir="$UTOO_CACHE" + +capture_one bun \ + bun install --lockfile-only --registry="$REGISTRY" + +echo "done. files:" +ls -lh "$PCAP_DIR" diff --git a/e2e/pm-bench-phases.sh b/e2e/pm-bench-phases.sh new file mode 100755 index 0000000000..3f39b7c58a --- /dev/null +++ b/e2e/pm-bench-phases.sh @@ -0,0 +1,412 @@ +#!/bin/bash +# Phase-isolated cold-install bench: resolve vs cold-install vs warm-link. +# utoo vs bun only (they're the PMs with a true --lockfile-only mode). +set -eo pipefail + +# --- config --- +PROJECT=${PROJECT:-ant-design} +REGISTRY=${REGISTRY:-https://registry.npmjs.org} +RUNS=${BENCH_RUNS:-3} +IFS=',' read -ra PACKAGE_MANAGERS <<< "${PM_LIST:-utoo,bun}" + +BENCH_DIR=${BENCH_DIR:-/tmp/pm-bench} +RESULTS_DIR=${RESULTS_DIR:-/tmp/pm-bench-results} +mkdir -p "$BENCH_DIR" "$RESULTS_DIR" + +# Explicit bench-scoped cache dirs so rm -rf is unambiguous and we don't +# depend on what each PM considers the default (which varies by HOME, OS +# image, and previously-set user config). +UTOO_CACHE="${UTOO_CACHE:-/tmp/utoo-bench-cache}" +BUN_CACHE="${BUN_CACHE:-/tmp/bun-bench-cache}" +export BUN_INSTALL_CACHE_DIR="$BUN_CACHE" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +NC='\033[0m' + +banner() { echo -e "${YELLOW}=== $* ===${NC}"; } + +# --- metrics wrapper --- +# Keeps the hot path fast: only /usr/bin/time + counter snapshots inside the +# measured window. Disk-footprint measurement (du on node_modules / cache) +# happens once per phase AFTER hyperfine exits — see capture_footprint in +# run_phase — so 1-2s of `du` traversal doesn't bleed into wall-clock. +METRICS_WRAPPER="$RESULTS_DIR/metrics_wrapper.sh" +cat > "$METRICS_WRAPPER" <<'METRICS_EOF' +#!/bin/bash +METRICS_FILE="$1"; shift +TIME_TMP=$(mktemp) + +# --- snapshot network BEFORE the command (cheap: one read of /proc/net/dev) --- +snap_net_rx=0; snap_net_tx=0 +if [ -r /proc/net/dev ]; then + read snap_net_rx snap_net_tx < <(awk '/:/ && $1 !~ /^lo:/ {rx += $2; tx += $10} END {print rx+0, tx+0}' /proc/net/dev) +fi + +if [[ "$(uname)" == "Darwin" ]]; then + /usr/bin/time -l "$@" 2>"$TIME_TMP" + EXIT_CODE=$? + RSS=$(awk '/maximum resident set size/ {print $1}' "$TIME_TMP") + PG_MAJOR=$(awk '/page faults/ {print $1}' "$TIME_TMP") + PG_MINOR=$(awk '/page reclaims/ {print $1}' "$TIME_TMP") + VOL_CTX=$(awk '/ voluntary context switches/ {print $1}' "$TIME_TMP") + INVOL_CTX=$(awk '/involuntary context switches/ {print $1}' "$TIME_TMP") + USER_S=$(awk '/real/ && /user/ && /sys/ {print $3}' "$TIME_TMP") + SYS_S=$(awk '/real/ && /user/ && /sys/ {print $5}' "$TIME_TMP") +else + /usr/bin/time -v "$@" 2>"$TIME_TMP" + EXIT_CODE=$? + RSS_KB=$(awk '/Maximum resident set size/ {print $NF}' "$TIME_TMP") + RSS=$(( ${RSS_KB:-0} * 1024 )) + PG_MAJOR=$(awk '/Major \(requiring I\/O\) page faults/ {print $NF}' "$TIME_TMP") + PG_MINOR=$(awk '/Minor \(reclaiming a frame\) page faults/ {print $NF}' "$TIME_TMP") + VOL_CTX=$(awk '/Voluntary context switches/ {print $NF}' "$TIME_TMP") + INVOL_CTX=$(awk '/Involuntary context switches/ {print $NF}' "$TIME_TMP") + USER_S=$(awk -F': ' '/User time \(seconds\)/ {print $2}' "$TIME_TMP") + SYS_S=$(awk -F': ' '/System time \(seconds\)/ {print $2}' "$TIME_TMP") +fi + +# --- snapshot network AFTER, compute delta --- +net_rx=0; net_tx=0 +if [ -r /proc/net/dev ]; then + read cur_net_rx cur_net_tx < <(awk '/:/ && $1 !~ /^lo:/ {rx += $2; tx += $10} END {print rx+0, tx+0}' /proc/net/dev) + net_rx=$(( cur_net_rx - snap_net_rx )) + net_tx=$(( cur_net_tx - snap_net_tx )) +fi + +printf '{"rss":%d,"user_s":%s,"sys_s":%s,"page_major":%d,"page_minor":%d,"vol_ctx":%d,"invol_ctx":%d,"net_rx":%d,"net_tx":%d}\n' \ + "${RSS:-0}" "${USER_S:-0}" "${SYS_S:-0}" "${PG_MAJOR:-0}" "${PG_MINOR:-0}" \ + "${VOL_CTX:-0}" "${INVOL_CTX:-0}" \ + "${net_rx:-0}" "${net_tx:-0}" >> "$METRICS_FILE" +rm -f "$TIME_TMP" +exit $EXIT_CODE +METRICS_EOF +chmod +x "$METRICS_WRAPPER" + +# Post-phase footprint helper: records final on-disk size of the paths each +# phase should have touched. Called ONCE after hyperfine finishes, so the +# (slow) du traversal is not part of any timed window. +du_bytes() { + for p in "$@"; do + if [ -e "$p" ]; then + if [[ "$(uname)" == "Darwin" ]]; then + du -sk "$p" 2>/dev/null | awk '{print $1 * 1024}' + else + du -sb "$p" 2>/dev/null | awk '{print $1}' + fi + else + echo 0 + fi + done | awk '{s += $1} END {print s+0}' +} + +capture_footprint() { + local phase=$1 pm=$2 out=$3 + local cache + case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + printf '{"cache":%d,"node_modules":%d,"lockfile":%d}\n' \ + "$(du_bytes "$cache")" \ + "$(du_bytes "$PROJECT_DIR/node_modules")" \ + "$(du_bytes "$PROJECT_DIR/package-lock.json" "$PROJECT_DIR/bun.lock")" \ + > "$out" +} + +# --- project --- +banner "Preparing $PROJECT" +cd "$BENCH_DIR" +if [ ! -d "$PROJECT" ]; then + git clone --depth=1 "https://github.com/ant-design/${PROJECT}.git" "$PROJECT" +fi +PROJECT_DIR="$BENCH_DIR/$PROJECT" + +# --- prepare script builder --- +# Emit a self-contained reset script that hyperfine's --prepare can re-exec. +# The script always prints what it deleted so the CI log proves prepare ran. +write_prepare() { + local path=$1 phase=$2 pm=$3 + local cache + case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + + cat > "$path" </dev/null || true + +EOF + + # Use explicit bench-scoped cache paths and rm -rf them directly — previously + # `utoo clean` / `bun pm cache rm` didn't actually empty the cache on the CI + # runner, leading to cache-hit runs masquerading as cold installs. + case "$phase" in + p0_*) + # Phase 0: full cold install — nothing reused. Lockfile + all caches wiped. + cat >> "$path" <> "$path" <> "$path" <> "$path" <> "$path" <> "$path" < "$RESULTS_DIR/seed_${phase}_${pm}.log" 2>&1 + fi + ;; + p3_*:bun|p4_*:bun) + if [ ! -f bun.lock ]; then + echo -e " ${CYAN}seed: running \`bun install --lockfile-only\` to generate bun.lock${NC}" + rm -f package-lock.json + bun install --lockfile-only --registry="$REGISTRY" > "$RESULTS_DIR/seed_${phase}_${pm}.log" 2>&1 + fi + ;; + esac + # Phase 4 also needs a pre-warmed cache. + if [[ "$phase" == p4_* ]]; then + local cache + case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + if [ ! -d "$cache" ] || [ -z "$(ls -A "$cache" 2>/dev/null)" ]; then + echo -e " ${CYAN}seed: warming $pm cache via full install${NC}" + rm -rf node_modules + case "$pm" in + utoo) utoo install --ignore-scripts --registry="$REGISTRY" --cache-dir="$UTOO_CACHE" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; + bun) bun install --ignore-scripts --registry="$REGISTRY" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; + esac + rm -rf node_modules + fi + fi +} + +install_cmd() { + case "$1" in + utoo) echo "utoo install --ignore-scripts --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; + bun) echo "bun install --ignore-scripts --registry=$REGISTRY" ;; + esac +} + +resolve_cmd() { + case "$1" in + utoo) echo "utoo deps --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; + bun) echo "bun install --lockfile-only --registry=$REGISTRY" ;; + esac +} + +run_phase() { + local phase=$1 pm=$2 cmd=$3 + local json="$RESULTS_DIR/${PROJECT}_${phase}_${pm}.json" + local metrics="$RESULTS_DIR/${PROJECT}_${phase}_${pm}_metrics.jsonl" + local prep_script="$RESULTS_DIR/prep_${phase}_${pm}.sh" + local cmd_script="$RESULTS_DIR/cmd_${phase}_${pm}.sh" + + seed_for_phase "$phase" "$pm" + write_prepare "$prep_script" "$phase" "$pm" + + # Reset metrics file; wrap the bench command so /usr/bin/time can measure it. + # The wrapper reads PROJECT_DIR / UTOO_CACHE / BUN_CACHE from env to scope du. + : > "$metrics" + printf 'set -eo pipefail\ncd %s\n%s\n' "$PROJECT_DIR" "$cmd" > "$cmd_script" + + echo -e " ${CYAN}$pm${NC} · $phase" + export PROJECT_DIR UTOO_CACHE BUN_CACHE + if ! hyperfine \ + --runs "$RUNS" \ + --prepare "bash $prep_script" \ + --export-json "$json" \ + --show-output \ + -n "${pm}-${phase}" \ + "bash $METRICS_WRAPPER $metrics bash $cmd_script"; then + echo -e " ${RED}$pm $phase failed${NC}" + fi + + # After the timed runs: capture the state left on disk by the last run. + # Done outside hyperfine so du's traversal doesn't pollute wall-clock. + capture_footprint "$phase" "$pm" "$RESULTS_DIR/${PROJECT}_${phase}_${pm}_footprint.json" +} + +# === PHASE 0: full cold install (clean slate + full install) === +# Matches the end-to-end user scenario: no lockfile, no cache, no node_modules. +# Directly comparable to `bun install` / `utoo install` on a freshly cloned repo. +banner "Phase 0 · full cold install (lockfile + cache + node_modules all wiped)" +for pm in "${PACKAGE_MANAGERS[@]}"; do + run_phase "p0_full_cold" "$pm" "$(install_cmd "$pm")" +done + +# === PHASE 1: resolve only (clean slate) === +banner "Phase 1 · resolve (lockfile only, cold cache)" +for pm in "${PACKAGE_MANAGERS[@]}"; do + run_phase "p1_resolve" "$pm" "$(resolve_cmd "$pm")" +done + +# === PHASE 3: cold install (lockfile exists, cache empty) === +banner "Phase 3 · cold install (lockfile present, empty cache, empty node_modules)" +for pm in "${PACKAGE_MANAGERS[@]}"; do + run_phase "p3_cold_install" "$pm" "$(install_cmd "$pm")" +done + +# === PHASE 4: warm link (cache populated, lockfile exists) === +banner "Phase 4 · warm link (lockfile present, populated cache, empty node_modules)" +for pm in "${PACKAGE_MANAGERS[@]}"; do + run_phase "p4_warm_link" "$pm" "$(install_cmd "$pm")" +done + +# === SUMMARY === +banner "Summary" +RESULTS_DIR="$RESULTS_DIR" node -e " + const fs = require('fs'), path = require('path'); + const dir = process.env.RESULTS_DIR; + const order = ['p0_full_cold', 'p1_resolve', 'p3_cold_install', 'p4_warm_link']; + const timing = {}; // phase -> pm -> {mean,stddev,min,max} + const metrics = {}; // phase -> pm -> averaged resource fields + + const parseKey = (file, suffix) => { + const base = file.replace(suffix, ''); + for (const p of order) { + const idx = base.indexOf('_' + p + '_'); + if (idx === -1) continue; + const pm = base.slice(idx + p.length + 2); + return { phase: p, pm }; + } + return null; + }; + + for (const f of fs.readdirSync(dir).filter(x => x.endsWith('.json') && !x.endsWith('_metrics.jsonl') && !x.endsWith('_footprint.json'))) { + const key = parseKey(f, '.json'); + if (!key) continue; + let data; try { data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { continue; } + const r = data.results[0]; + if (!r) continue; + (timing[key.phase] ??= {})[key.pm] = { mean: r.mean, stddev: r.stddev, min: r.min, max: r.max }; + } + + const metricKeys = ['rss','user_s','sys_s','page_major','page_minor','vol_ctx','invol_ctx','net_rx','net_tx']; + for (const f of fs.readdirSync(dir).filter(x => x.endsWith('_metrics.jsonl'))) { + const key = parseKey(f, '_metrics.jsonl'); + if (!key) continue; + const rows = []; + for (const l of fs.readFileSync(path.join(dir, f), 'utf8').trim().split('\n').filter(Boolean)) { + try { rows.push(JSON.parse(l)); } catch (_) {} + } + if (rows.length === 0) continue; + const avg = {}; + for (const k of metricKeys) { + avg[k] = rows.reduce((s,e) => s + Number(e[k] || 0), 0) / rows.length; + } + (metrics[key.phase] ??= {})[key.pm] = avg; + } + + // Footprint is a post-phase single sample (not averaged): final on-disk size + // of the paths the phase should have touched. + for (const f of fs.readdirSync(dir).filter(x => x.endsWith('_footprint.json'))) { + const key = parseKey(f, '_footprint.json'); + if (!key) continue; + let data; try { data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { continue; } + const slot = (metrics[key.phase] ??= {})[key.pm] ??= {}; + slot.foot_cache = data.cache || 0; + slot.foot_nm = data.node_modules || 0; + slot.foot_lock = data.lockfile || 0; + } + + const pad = (s, n) => String(s).padEnd(n); + const padR = (s, n) => String(s).padStart(n); + const fmtB = b => b >= 1<<30 ? (b/(1<<30)).toFixed(2)+'G' : b >= 1<<20 ? (b/(1<<20)).toFixed(0)+'M' : b >= 1<<10 ? (b/(1<<10)).toFixed(0)+'K' : Math.round(b)+'B'; + const fmtN = n => n >= 1e6 ? (n/1e6).toFixed(2)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(Math.round(n)); + + for (const phase of order) { + const tp = timing[phase] || {}, mp = metrics[phase] || {}; + const pms = [...new Set([...Object.keys(tp), ...Object.keys(mp)])]; + if (pms.length === 0) continue; + + console.log('\n## ' + phase); + + // Table A: wall + CPU + memory + console.log(pad('PM', 6) + ' ' + padR('wall', 8) + ' ' + padR('±σ', 7) + ' ' + padR('user', 7) + ' ' + padR('sys', 7) + ' ' + padR('RSS', 6) + ' ' + padR('pgMinor', 8)); + for (const pm of pms) { + const t = tp[pm] || {}, m = mp[pm] || {}; + console.log( + pad(pm, 6) + ' ' + + padR((t.mean ?? 0).toFixed(2)+'s', 8) + ' ' + + padR((t.stddev ?? 0).toFixed(2)+'s', 7) + ' ' + + padR((m.user_s || 0).toFixed(2)+'s', 7) + ' ' + + padR((m.sys_s || 0).toFixed(2)+'s', 7) + ' ' + + padR(m.rss ? fmtB(m.rss) : '-', 6) + ' ' + + padR(m.page_minor ? fmtN(m.page_minor) : '-', 8) + ); + } + + // Table B: context switches + network + final on-disk footprint + console.log(pad('PM', 6) + ' ' + padR('vCtx', 8) + ' ' + padR('iCtx', 8) + ' ' + padR('net RX', 8) + ' ' + padR('net TX', 8) + ' ' + padR('cache', 8) + ' ' + padR('node_mod', 9) + ' ' + padR('lock', 7)); + for (const pm of pms) { + const m = mp[pm] || {}; + console.log( + pad(pm, 6) + ' ' + + padR(m.vol_ctx ? fmtN(m.vol_ctx) : '-', 8) + ' ' + + padR(m.invol_ctx ? fmtN(m.invol_ctx) : '-', 8) + ' ' + + padR(m.net_rx ? fmtB(m.net_rx) : '-', 8) + ' ' + + padR(m.net_tx ? fmtB(m.net_tx) : '-', 8) + ' ' + + padR(m.foot_cache ? fmtB(m.foot_cache) : '-', 8) + ' ' + + padR(m.foot_nm ? fmtB(m.foot_nm) : '-', 9) + ' ' + + padR(m.foot_lock ? fmtB(m.foot_lock) : '-', 7) + ); + } + } +" + +echo -e "${GREEN}Done. Raw results in $RESULTS_DIR${NC}" diff --git a/e2e/pm-bench.sh b/e2e/pm-bench.sh index c4c43bdf14..12645c8b9e 100755 --- a/e2e/pm-bench.sh +++ b/e2e/pm-bench.sh @@ -293,13 +293,15 @@ run_cold_benchmarks() { > "$metrics_file" echo -e " ${CYAN}$pm${NC}..." - hyperfine \ + if ! hyperfine \ --runs "$BENCH_COLD_RUNS" \ --prepare "bash $PREPARE_SCRIPT $project_dir $pm --cold" \ --export-json "$json_file" \ + --show-output \ -n "$pm" \ - "bash $METRICS_WRAPPER $metrics_file $cmd_script" \ - 2>&1 | tail -1 || echo -e " ${RED}$pm cold install failed${NC}" + "bash $METRICS_WRAPPER $metrics_file $cmd_script"; then + echo -e " ${RED}$pm cold install failed${NC}" + fi done } @@ -372,7 +374,15 @@ print_results() { // --- Parse hyperfine timing --- const timeRows = []; for (const file of jsonFiles) { - const data = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')); + let data; + try { + const raw = fs.readFileSync(path.join(dir, file), 'utf8'); + if (!raw.trim()) { console.log(' skip empty: ' + file); continue; } + data = JSON.parse(raw); + } catch (err) { + console.log(' skip unparseable: ' + file + ' (' + err.message + ')'); + continue; + } const base = file.replace('.json', ''); const parts = base.split('_'); const typeIdx = parts.findIndex(p => p === 'cold' || p === 'warm'); @@ -406,7 +416,11 @@ print_results() { .trim().split('\n').filter(Boolean); if (lines.length === 0) continue; - const entries = lines.map(l => JSON.parse(l)); + const entries = []; + for (const l of lines) { + try { entries.push(JSON.parse(l)); } catch (_) { /* skip malformed line */ } + } + if (entries.length === 0) continue; const avg = {}; for (const key of ['rss', 'page_faults', 'vol_ctx', 'invol_ctx', 'io_in', 'io_out']) { avg[key] = Math.round(entries.reduce((s, e) => s + (e[key] || 0), 0) / entries.length); @@ -513,7 +527,11 @@ main() { echo -e "${YELLOW}----------------------------------------${NC}" run_cold_benchmarks "$project" "$registry" "$reg_short" - run_warm_benchmarks "$project" "$registry" "$reg_short" + if [ "$BENCH_WARM_RUNS" -gt 0 ]; then + run_warm_benchmarks "$project" "$registry" "$reg_short" + else + echo -e " ${YELLOW}Warm installs: skipped (BENCH_WARM_RUNS=0)${NC}" + fi echo "" done From e190e41d7e38be24a3f9b357c9fddc10ead7eb67 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sat, 25 Apr 2026 22:12:29 +0800 Subject: [PATCH 02/19] chore(manifest-bench): tombi format + fix typo Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/manifest-bench/Cargo.toml | 21 ++++++++------------- e2e/pm-bench.sh | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/manifest-bench/Cargo.toml b/crates/manifest-bench/Cargo.toml index 5b01e57c0a..84a9aabf73 100644 --- a/crates/manifest-bench/Cargo.toml +++ b/crates/manifest-bench/Cargo.toml @@ -2,9 +2,9 @@ name = "manifest-bench" version = "0.0.0" edition = "2024" +description = "Standalone HTTP-only manifest fetch benchmark, isolating network behaviour from ruborist's resolver pipeline." license = "MIT" publish = false -description = "Standalone HTTP-only manifest fetch benchmark, isolating network behaviour from ruborist's resolver pipeline." [[bin]] name = "manifest-bench" @@ -12,12 +12,12 @@ path = "src/main.rs" # tombi: format.rules.table-keys-order.disabled = true [dependencies] -anyhow = { workspace = true } -clap = { workspace = true } -futures = "0.3" -serde = { version = "1", features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "fs", "time"] } +anyhow = { workspace = true } +clap = { workspace = true } +futures = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "time"] } # Identical TLS / DNS choices to ruborist so we measure the *protocol* # characteristics of the same stack, not a different implementation. @@ -28,10 +28,5 @@ reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls-native-roots-no-provider", "socks" ] } -rustls = { version = "0.23", default-features = false, features = [ - "aws-lc-rs", - "logging", - "std", - "tls12" -] } +rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "logging", "std", "tls12"] } rustls-native-certs = "0.8" diff --git a/e2e/pm-bench.sh b/e2e/pm-bench.sh index 12645c8b9e..2d3a58de44 100755 --- a/e2e/pm-bench.sh +++ b/e2e/pm-bench.sh @@ -380,7 +380,7 @@ print_results() { if (!raw.trim()) { console.log(' skip empty: ' + file); continue; } data = JSON.parse(raw); } catch (err) { - console.log(' skip unparseable: ' + file + ' (' + err.message + ')'); + console.log(' skip unparsable: ' + file + ' (' + err.message + ')'); continue; } const base = file.replace('.json', ''); From b136dc0c21e22c8f8fc5dcb8eaa78fe288d3844a Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sat, 25 Apr 2026 22:19:45 +0800 Subject: [PATCH 03/19] chore(manifest-bench): tombi 0.7.32 format CI runs tombi 0.7.32 which aligns column widths across the file (local 0.8.0 normalises per-table and disagreed). Apply the formatting CI expects. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/manifest-bench/Cargo.toml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/manifest-bench/Cargo.toml b/crates/manifest-bench/Cargo.toml index 84a9aabf73..0b4e9657de 100644 --- a/crates/manifest-bench/Cargo.toml +++ b/crates/manifest-bench/Cargo.toml @@ -12,13 +12,12 @@ path = "src/main.rs" # tombi: format.rules.table-keys-order.disabled = true [dependencies] -anyhow = { workspace = true } -clap = { workspace = true } -futures = "0.3" -serde = { version = "1", features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "time"] } - +anyhow = { workspace = true } +clap = { workspace = true } +futures = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "time"] } # Identical TLS / DNS choices to ruborist so we measure the *protocol* # characteristics of the same stack, not a different implementation. reqwest = { version = "0.12", default-features = false, features = [ From 6c64269358674f67fbf1a992f969ce789fb2bc49 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sat, 25 Apr 2026 23:28:35 +0800 Subject: [PATCH 04/19] ci(pm-bench-phases): auto-trigger on (pm)-scoped PRs + sticky comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror `pack-perf.yml` pattern: - Trigger on `pull_request` to main/next AND `push` to main/next when PR title / commit message contains `(pm` (matches `perf(pm)`, `refactor(pm)`, `ci(pm-bench-phases)`, etc — same scope convention as pack uses for `(pack`) - Replace the ad-hoc `actions/github-script` upsert with battle-tested `marocchino/sticky-pull-request-comment@v2` (same action pack-perf uses) - Build the comment body in a separate shell step that strips ANSI escapes and wraps each `## p_` summary section as a fenced code block - Auto-detect PR number by branch name → SHA fallback (handles push events where the PR isn't on the event payload) - Drop the prior `pull_request: types: [labeled]` trigger — labels are no longer needed; scope-in-title is the convention This makes the bench results auto-post on every PR push for any pm- scoped change. workflow_dispatch still works for manual runs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-bench-phases.yml | 179 +++++++++++++------------- 1 file changed, 86 insertions(+), 93 deletions(-) diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml index 4bcabca57e..9265f7619e 100644 --- a/.github/workflows/pm-bench-phases.yml +++ b/.github/workflows/pm-bench-phases.yml @@ -19,17 +19,23 @@ on: default: '3' type: string pull_request: - types: [labeled] + branches: [main, next] + push: + branches: [main, next] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build-linux: + # Mirrors `pack-perf.yml` auto-trigger pattern: only run on + # PRs / pushes whose title or commit message references the pm + # scope. Manual workflow_dispatch always runs. if: > github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'bench-phases') + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) runs-on: ubuntu-latest container: image: ubuntu:20.04 @@ -72,7 +78,8 @@ jobs: needs: [build-linux] if: > github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'bench-phases') + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) runs-on: ubuntu-latest # Comment-writing on PRs needs `pull-requests: write`. Other tokens # default to read-only on this workflow's events. Granular instead @@ -191,98 +198,84 @@ jobs: path: /tmp/pm-bench-pcap retention-days: 3 - # Post bench results as a PR comment. Triggers only on - # `bench-phases`-labeled PRs (the existing label trigger above), so - # workflow_dispatch + push runs do not spam comments. The comment - # body is the per-phase summary tables (plain text, kept compact). - # The action upserts a single sticky comment per PR keyed by an - # HTML marker so re-runs replace rather than append. - - name: Post bench results to PR comment - if: > - always() && - github.event_name == 'pull_request' && - github.event.label.name == 'bench-phases' - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const path = '/tmp/pm-bench-output/bench-phases.log'; - if (!fs.existsSync(path)) { - core.warning(`No bench output at ${path}`); - return; - } - const raw = fs.readFileSync(path, 'utf8'); - - // Strip ANSI color escape codes - const stripAnsi = s => s.replace(/\x1b\[[0-9;]*m/g, ''); - const cleaned = stripAnsi(raw); - - // Pull out the per-phase summary tables. The pm-bench-phases.sh - // script prints sections delimited by `## p_` headers - // followed by tab-separated wall / RSS / IO / context-switch / - // network / cache / lockfile rows. Capture from each `## ` to - // the next `## ` (or end). - const sections = []; - const re = /^## p\d+_[a-z0-9_]+.*$/gm; - const headers = []; - let m; - while ((m = re.exec(cleaned)) !== null) { - headers.push({ idx: m.index, name: m[0].trim() }); - } - for (let i = 0; i < headers.length; i++) { - const start = headers[i].idx; - const end = i + 1 < headers.length ? headers[i + 1].idx : cleaned.length; - const body = cleaned.slice(start, end).trim(); - sections.push(body); - } + # Build the comment body from the bench output. Strips ANSI escape + # codes and pulls out the `## p_` summary tables; bun vs + # utoo wall / RSS / IO / context-switch / network / cache / + # lockfile rows are wrapped as fenced blocks for readability. + - name: Build PR comment body + if: always() + run: | + set -eu + mkdir -p /tmp/pm-bench-output + OUT=/tmp/pm-bench-output/pr_comment.md + LOG=/tmp/pm-bench-output/bench-phases.log + if [ ! -f "$LOG" ]; then + echo "_No bench output produced — see [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})._" > "$OUT" + exit 0 + fi + # Strip ANSI escapes + CLEANED=$(mktemp) + sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$LOG" > "$CLEANED" - const marker = ''; - const sha = context.sha.slice(0, 7); - const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + SHA="${GITHUB_SHA:0:7}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - let bodyParts = [ - marker, - `## pm-bench-phases · \`${sha}\``, - '', - `[Workflow run](${runUrl}) — ant-design / npmjs.org / ubuntu-latest`, - '', - ]; + { + echo "## 📊 pm-bench-phases · \`$SHA\`" + echo "" + echo "[Workflow run]($RUN_URL) — ant-design / npmjs.org / ubuntu-latest" + echo "" + } > "$OUT" - if (sections.length === 0) { - bodyParts.push('_No phase summary tables found in bench output. See workflow log._'); - } else { - for (const sec of sections) { - bodyParts.push('```'); - bodyParts.push(sec); - bodyParts.push('```'); - bodyParts.push(''); - } + # Extract each `## p_` section (up to but not including + # the next `## ` or EOF) and wrap as code fence. + awk ' + /^## p[0-9]+_[a-z0-9_]+/ { + if (in_section) print "```\n"; + in_section = 1; + print "```"; + print $0; + next; + } + /^## / && in_section { + print "```\n"; + in_section = 0; } + in_section { print } + END { if (in_section) print "```" } + ' "$CLEANED" >> "$OUT" - const body = bodyParts.join('\n'); + # Find the PR number that owns this run. On `pull_request` events + # the number is on the event payload directly; on `push` events + # we look it up by branch name (then by commit SHA as a fallback). + # Mirrors pack-perf.yml so behaviour stays consistent across the + # two perf workflows. + - name: Find Current PR + id: findPr + if: always() + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} - // Upsert: find an existing comment with our marker, edit it; - // otherwise create a new one. - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => c.body && c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - core.info(`Updated existing bench comment ${existing.id}`); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - core.info('Created new bench comment'); - } + - name: Post PR Comment + if: always() && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-report + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a247a9b78b094019f23ecd4bb752d485631a9ee0 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 00:03:25 +0800 Subject: [PATCH 05/19] ci(pm-bench-phases): cross-platform matrix + npmmirror run + revert legacy bench script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes per review feedback: 1. **Revert `e2e/pm-bench.sh` changes** — those (`--show-output`, JSON parse fault tolerance) were part of the legacy non-phase bench. Bench-phases lives in its own script (`pm-bench-phases.sh`); the legacy script doesn't need our touch and was bloating the diff. 2. **Add npmmirror run after npmjs** with full cache wipe between. Both `pm-bench-phases.sh` and `manifest-bench` now run twice per bench job. The wipe step removes: /tmp/pm-bench /tmp/pm-bench-results /tmp/utoo-bench-cache /tmp/bun-bench-cache ~/.bun/install/cache ~/.cache/nm so the second registry starts truly cold (lockfile, project clone, manifest cache, tarball cache — all wiped). The PR comment renders both registries' phase tables under labeled sections. 3. **Cross-platform matrix** — `build` and `bench` jobs both run on `[ubuntu-latest, macos-latest]`. Each platform posts its own sticky comment (header `pm-bench-phases-{linux,mac}`) so they don't race. `gnu-time` and `hyperfine` installed via brew on mac. PCAP step is linux-only (raw sockets + tcpdump availability). Bonus: extracted manifest-bench sweep loop to `.github/scripts/run-manifest-bench.sh` so the workflow yaml stays readable and the sweep can be reused by ad-hoc scripts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/run-manifest-bench.sh | 51 +++++ .github/workflows/pm-bench-phases.yml | 292 ++++++++++++++++---------- e2e/pm-bench.sh | 30 +-- 3 files changed, 235 insertions(+), 138 deletions(-) create mode 100755 .github/scripts/run-manifest-bench.sh diff --git a/.github/scripts/run-manifest-bench.sh b/.github/scripts/run-manifest-bench.sh new file mode 100755 index 0000000000..02a6f454d3 --- /dev/null +++ b/.github/scripts/run-manifest-bench.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Run manifest-bench (standalone HTTP-only fetch tool) against a +# given REGISTRY for a given PROJECT. Used by `pm-bench-phases.yml` +# to capture HTTP stack ceiling numbers per registry / per platform. +# +# Required env: REGISTRY, PROJECT +# Generates the lockfile via `utoo deps` if it doesn't exist yet. +set -eu + +PROJECT_DIR="/tmp/pm-bench/$PROJECT" +if [ ! -d "$PROJECT_DIR" ]; then + mkdir -p /tmp/pm-bench + git clone --depth 1 "https://github.com/ant-design/$PROJECT" "$PROJECT_DIR" +fi +cd "$PROJECT_DIR" +if [ ! -f package-lock.json ]; then + echo "==> generating lockfile via utoo (registry=$REGISTRY)" + utoo deps --registry "$REGISTRY" || true +fi +ls -la package-lock.json || { echo "no lockfile, skip"; exit 0; } + +echo +echo "============================================================" +echo "manifest-bench: HTTP-only fetch (no parse, no resolver)" +echo " registry=$REGISTRY" +echo " Goal: isolate reqwest/rustls/tokio behaviour from" +echo " ruborist's resolver pipeline. Same numbers (wall, busy," +echo " avg_conc, p50/p95/max) as ruborist's Preload HTTP diag." +echo "============================================================" + +for CAP in 32 64 96 128 192 256; do + echo + echo "--- concurrency=$CAP, h1, full manifest, default UA ---" + manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency "$CAP" --reps 2 --http1-only || true +done + +echo +echo "--- concurrency=128, h2 negotiate, full manifest, default UA ---" +manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 || true + +echo +echo "--- concurrency=128, h1, single-version endpoint ---" +manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 --http1-only --single-version || true + +echo +echo "--- concurrency=128, h1, UA=Bun/1.2.21 ---" +manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ + --concurrency 128 --reps 2 --http1-only --user-agent "Bun/1.2.21" || true diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml index 9265f7619e..10dc96d5fb 100644 --- a/.github/workflows/pm-bench-phases.yml +++ b/.github/workflows/pm-bench-phases.yml @@ -8,11 +8,6 @@ on: required: false default: 'ant-design' type: string - registry: - description: 'Registry URL' - required: false - default: 'https://registry.npmjs.org' - type: string runs: description: 'Runs per phase' required: false @@ -28,7 +23,7 @@ concurrency: cancel-in-progress: true jobs: - build-linux: + build: # Mirrors `pack-perf.yml` auto-trigger pattern: only run on # PRs / pushes whose title or commit message references the pm # scope. Manual workflow_dispatch always runs. @@ -36,17 +31,17 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) - runs-on: ubuntu-latest - container: - image: ubuntu:20.04 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + - os: macos-latest + platform: mac + runs-on: ${{ matrix.os }} steps: - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git - uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory /__w/utoo/utoo - name: Init git submodules run: git submodule update --init --recursive --depth 1 - name: Install Rust @@ -56,7 +51,7 @@ jobs: - name: Cache cargo uses: Swatinem/rust-cache@v2 with: - shared-key: pm-bench-linux + shared-key: pm-bench-${{ matrix.platform }} - name: Build current branch utoo + manifest-bench run: | cargo build --release --bin utoo -p utoo-pm @@ -64,23 +59,33 @@ jobs: - name: Upload utoo binary uses: actions/upload-artifact@v4 with: - name: utoo-linux-bench-phases + name: utoo-${{ matrix.platform }}-bench-phases path: target/release/utoo retention-days: 1 - name: Upload manifest-bench binary uses: actions/upload-artifact@v4 with: - name: manifest-bench-linux + name: manifest-bench-${{ matrix.platform }} path: target/release/manifest-bench retention-days: 1 bench: - needs: [build-linux] + needs: [build] if: > github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) - runs-on: ubuntu-latest + strategy: + # Don't cancel mac if linux fails (or vice versa) — each platform's + # comment is independent and useful on its own. + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + - os: macos-latest + platform: mac + runs-on: ${{ matrix.os }} # Comment-writing on PRs needs `pull-requests: write`. Other tokens # default to read-only on this workflow's events. Granular instead # of `write-all` to keep the workflow's blast radius minimal. @@ -96,12 +101,12 @@ jobs: - name: Download utoo binary uses: actions/download-artifact@v4 with: - name: utoo-linux-bench-phases + name: utoo-${{ matrix.platform }}-bench-phases path: dist - name: Download manifest-bench binary uses: actions/download-artifact@v4 with: - name: manifest-bench-linux + name: manifest-bench-${{ matrix.platform }} path: dist - name: Install utoo run: | @@ -111,145 +116,201 @@ jobs: uses: oven-sh/setup-bun@v2 with: bun-version: latest - - name: Install hyperfine, GNU time, tcpdump and dnsutils + # hyperfine + GNU time available via OS package manager. tcpdump + # only needed for pcap step (linux only — sudo + raw sockets not + # generally available on macos GH runners). + - name: Install bench tools (linux) + if: matrix.platform == 'linux' run: sudo apt-get update && sudo apt-get install -y hyperfine time tcpdump dnsutils + - name: Install bench tools (mac) + if: matrix.platform == 'mac' + run: | + brew install hyperfine + # macOS has /usr/bin/time built-in; pm-bench-phases.sh's metrics + # wrapper uses `time -v` (GNU). Install gnu-time and alias. + brew install gnu-time + # symlink so `time` in scripts hits gnu-time + mkdir -p "$HOME/bin" + ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" + echo "$HOME/bin" >> $GITHUB_PATH - name: Verify tools run: | hyperfine --version utoo --version bun --version - - name: Run phase-isolated benchmark + + # ============================================================ + # Helper: wipe everything between registry runs so cache + state + # from the previous registry doesn't leak into the next one. + # The PROJECT clone is removed too — the bench script re-clones. + # ============================================================ + - name: Define cache wipe function + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + # Wipe ALL bench state — caches, project clone, results, lockfiles. + # Used between registry runs so the second one starts truly cold. + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + + # ------------------------------------------------------------ + # Run 1: npmjs.org (the primary upstream) + # ------------------------------------------------------------ + - name: Wipe bench state before npmjs + run: bash /tmp/wipe-bench-state.sh + + - name: Run phase-isolated benchmark · npmjs env: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + REGISTRY: 'https://registry.npmjs.org' BENCH_RUNS: ${{ github.event.inputs.runs || '3' }} PM_LIST: 'utoo,bun' run: | chmod +x e2e/pm-bench-phases.sh - # Capture full output to a file so the PR comment step can - # extract the per-phase summary tables. `tee` keeps live - # progress visible in the workflow log. - mkdir -p /tmp/pm-bench-output - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases.log + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - name: Standalone manifest-bench (HTTP-only fetch sweep) + - name: Standalone manifest-bench · npmjs env: - REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + REGISTRY: 'https://registry.npmjs.org' PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - run: | - set -eu - # Generate a project lockfile to source the manifest names from. - # Use bun (which we already have) to do a quick lockfile-only - # install so manifest-bench has a real workload to fire against. - PROJECT_DIR="/tmp/pm-bench/$PROJECT" - if [ ! -d "$PROJECT_DIR" ]; then - mkdir -p /tmp/pm-bench - git clone --depth 1 "https://github.com/ant-design/$PROJECT" "$PROJECT_DIR" - fi - cd "$PROJECT_DIR" - if [ ! -f package-lock.json ]; then - echo "==> generating lockfile via utoo" - utoo deps --registry "$REGISTRY" || true - fi - ls -la package-lock.json || { echo "no lockfile, skip"; exit 0; } - - echo - echo "============================================================" - echo "manifest-bench: HTTP-only fetch (no parse, no resolver)" - echo " Goal: isolate reqwest/rustls/tokio behaviour from" - echo " ruborist's resolver pipeline. Same numbers (wall, busy," - echo " avg_conc, p50/p95/max) as ruborist's Preload HTTP diag." - echo "============================================================" - for CAP in 32 64 96 128 192 256; do - echo - echo "--- concurrency=$CAP, h1, full manifest, default UA ---" - manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency "$CAP" --reps 2 --http1-only || true - done + run: bash .github/scripts/run-manifest-bench.sh 2>&1 | tee /tmp/pm-bench-output/manifest-bench-npmjs.log - echo - echo "--- concurrency=128, h2 negotiate, full manifest, default UA ---" - manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 || true + # ------------------------------------------------------------ + # Run 2: npmmirror.com (Asia-Pacific, semver-supporting registry) + # ------------------------------------------------------------ + # Save the npmjs logs out of /tmp/pm-bench-output before wipe + # (the wipe also clears the output dir for cleanliness). + - name: Stash npmjs logs and wipe state before npmmirror + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - echo - echo "--- concurrency=128, h1, single-version endpoint ---" - manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 --http1-only --single-version || true + - name: Run phase-isolated benchmark · npmmirror + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: ${{ github.event.inputs.runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - echo - echo "--- concurrency=128, h1, UA=Bun/1.2.21 ---" - manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 --http1-only --user-agent "Bun/1.2.21" || true + - name: Standalone manifest-bench · npmmirror + env: + REGISTRY: 'https://registry.npmmirror.com' + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + run: bash .github/scripts/run-manifest-bench.sh 2>&1 | tee /tmp/pm-bench-output/manifest-bench-npmmirror.log + # PCAP only on linux (mac runners lack tcpdump + raw socket perms). - name: Capture pcap (resolve phase, single run per PM) + if: matrix.platform == 'linux' env: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: ${{ github.event.inputs.registry || 'https://registry.npmjs.org' }} + REGISTRY: 'https://registry.npmjs.org' run: | chmod +x e2e/pm-bench-pcap.sh bash e2e/pm-bench-pcap.sh - name: Upload pcap artifact - if: always() + if: always() && matrix.platform == 'linux' uses: actions/upload-artifact@v4 with: name: pm-bench-pcap path: /tmp/pm-bench-pcap retention-days: 3 - # Build the comment body from the bench output. Strips ANSI escape - # codes and pulls out the `## p_` summary tables; bun vs - # utoo wall / RSS / IO / context-switch / network / cache / - # lockfile rows are wrapped as fenced blocks for readability. + - name: Upload bench logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-${{ matrix.platform }} + path: /tmp/pm-bench-output + retention-days: 7 + + # Build the comment body from BOTH registry runs and BOTH phases' + # output. Strips ANSI escape codes; pulls out per-phase summary + # tables for each registry under a labeled section. - name: Build PR comment body if: always() run: | set -eu - mkdir -p /tmp/pm-bench-output OUT=/tmp/pm-bench-output/pr_comment.md - LOG=/tmp/pm-bench-output/bench-phases.log - if [ ! -f "$LOG" ]; then - echo "_No bench output produced — see [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})._" > "$OUT" - exit 0 - fi - # Strip ANSI escapes - CLEANED=$(mktemp) - sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$LOG" > "$CLEANED" SHA="${GITHUB_SHA:0:7}" RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + PLATFORM='${{ matrix.platform }}' + OS='${{ matrix.os }}' { - echo "## 📊 pm-bench-phases · \`$SHA\`" + echo "## 📊 pm-bench-phases · \`$SHA\` · ${PLATFORM} (\`${OS}\`)" echo "" - echo "[Workflow run]($RUN_URL) — ant-design / npmjs.org / ubuntu-latest" + echo "[Workflow run]($RUN_URL) — ant-design" echo "" } > "$OUT" - # Extract each `## p_` section (up to but not including - # the next `## ` or EOF) and wrap as code fence. - awk ' - /^## p[0-9]+_[a-z0-9_]+/ { - if (in_section) print "```\n"; - in_section = 1; - print "```"; - print $0; - next; - } - /^## / && in_section { - print "```\n"; - in_section = 0; - } - in_section { print } - END { if (in_section) print "```" } - ' "$CLEANED" >> "$OUT" + # Helper to extract `## p_` sections from a log file + # and emit them under a labeled heading. + render_log() { + local label="$1" + local file="$2" + if [ ! -f "$file" ]; then + echo "_${label}: no output captured._" + echo "" + return + fi + echo "### ${label}" + echo "" + local cleaned + cleaned=$(mktemp) + sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$file" > "$cleaned" + awk ' + /^## p[0-9]+_[a-z0-9_]+/ { + if (in_section) print "```\n"; + in_section = 1; + print "```"; + print $0; + next; + } + /^## / && in_section { + print "```\n"; + in_section = 0; + } + in_section { print } + END { if (in_section) print "```" } + ' "$cleaned" + echo "" + rm -f "$cleaned" + } + + render_log "npmjs.org" /tmp/pm-bench-output/bench-phases-npmjs.log >> "$OUT" + render_log "npmmirror.com" /tmp/pm-bench-output/bench-phases-npmmirror.log >> "$OUT" + + # Manifest-bench: include only the section markers + rep + # summary lines so the comment doesn't balloon. + { + echo "### manifest-bench (HTTP-only fetch sweep)" + echo "" + for label in npmjs npmmirror; do + mb_log="/tmp/pm-bench-output/manifest-bench-${label}.log" + if [ -f "$mb_log" ]; then + echo "**${label}**" + echo "" + echo '```' + sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$mb_log" \ + | grep -E '^---|rep [12]\] n=' || echo '(no manifest-bench output)' + echo '```' + echo "" + fi + done + } >> "$OUT" - # Find the PR number that owns this run. On `pull_request` events - # the number is on the event payload directly; on `push` events - # we look it up by branch name (then by commit SHA as a fallback). - # Mirrors pack-perf.yml so behaviour stays consistent across the - # two perf workflows. - name: Find Current PR id: findPr if: always() @@ -270,11 +331,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} + # Each platform posts its own sticky comment (different `header`) + # so linux + mac results live as two separate comments and don't + # race / overwrite each other. - name: Post PR Comment if: always() && steps.findPr.outputs.number uses: marocchino/sticky-pull-request-comment@v2 with: - header: pm-bench-phases-report + header: pm-bench-phases-${{ matrix.platform }} path: /tmp/pm-bench-output/pr_comment.md number: ${{ steps.findPr.outputs.number }} recreate: true diff --git a/e2e/pm-bench.sh b/e2e/pm-bench.sh index 2d3a58de44..c4c43bdf14 100755 --- a/e2e/pm-bench.sh +++ b/e2e/pm-bench.sh @@ -293,15 +293,13 @@ run_cold_benchmarks() { > "$metrics_file" echo -e " ${CYAN}$pm${NC}..." - if ! hyperfine \ + hyperfine \ --runs "$BENCH_COLD_RUNS" \ --prepare "bash $PREPARE_SCRIPT $project_dir $pm --cold" \ --export-json "$json_file" \ - --show-output \ -n "$pm" \ - "bash $METRICS_WRAPPER $metrics_file $cmd_script"; then - echo -e " ${RED}$pm cold install failed${NC}" - fi + "bash $METRICS_WRAPPER $metrics_file $cmd_script" \ + 2>&1 | tail -1 || echo -e " ${RED}$pm cold install failed${NC}" done } @@ -374,15 +372,7 @@ print_results() { // --- Parse hyperfine timing --- const timeRows = []; for (const file of jsonFiles) { - let data; - try { - const raw = fs.readFileSync(path.join(dir, file), 'utf8'); - if (!raw.trim()) { console.log(' skip empty: ' + file); continue; } - data = JSON.parse(raw); - } catch (err) { - console.log(' skip unparsable: ' + file + ' (' + err.message + ')'); - continue; - } + const data = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')); const base = file.replace('.json', ''); const parts = base.split('_'); const typeIdx = parts.findIndex(p => p === 'cold' || p === 'warm'); @@ -416,11 +406,7 @@ print_results() { .trim().split('\n').filter(Boolean); if (lines.length === 0) continue; - const entries = []; - for (const l of lines) { - try { entries.push(JSON.parse(l)); } catch (_) { /* skip malformed line */ } - } - if (entries.length === 0) continue; + const entries = lines.map(l => JSON.parse(l)); const avg = {}; for (const key of ['rss', 'page_faults', 'vol_ctx', 'invol_ctx', 'io_in', 'io_out']) { avg[key] = Math.round(entries.reduce((s, e) => s + (e[key] || 0), 0) / entries.length); @@ -527,11 +513,7 @@ main() { echo -e "${YELLOW}----------------------------------------${NC}" run_cold_benchmarks "$project" "$registry" "$reg_short" - if [ "$BENCH_WARM_RUNS" -gt 0 ]; then - run_warm_benchmarks "$project" "$registry" "$reg_short" - else - echo -e " ${YELLOW}Warm installs: skipped (BENCH_WARM_RUNS=0)${NC}" - fi + run_warm_benchmarks "$project" "$registry" "$reg_short" echo "" done From 9ec183611fb6493bf37586ac00098067cd2b4d3f Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 00:12:06 +0800 Subject: [PATCH 06/19] ci(pm-bench-phases): reuse pm-e2e's linux utoo build artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pm-e2e and pm-bench-phases both build `utoo` from the same source on every (pm)-scoped PR — wasteful (~3 min per build). Bench now downloads pm-e2e's `utoo-linux-x64` artifact from the matching commit's pm-e2e run. Linux flow: bench job → `gh run list` finds pm-e2e run for this commit's SHA → download-artifact pulls `utoo-linux-x64` from there → falls back to local cargo build only if pm-e2e didn't run for this commit (e.g. workflow_dispatch path) manifest-bench: still built locally (pm-e2e doesn't build it). Mac flow unchanged: pm-e2e builds mac in its e2e matrix but doesn't upload an artifact, so bench still builds utoo + manifest-bench locally on macos-latest. Permissions: - `actions: read` added to bench job for `gh run list` + cross-run artifact download. Net CI savings on (pm)-scoped PRs: linux utoo cargo build (~3 min) runs once across both workflows instead of twice. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-bench-phases.yml | 104 +++++++++++++++++++++----- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml index 10dc96d5fb..6c9b48b7a0 100644 --- a/.github/workflows/pm-bench-phases.yml +++ b/.github/workflows/pm-bench-phases.yml @@ -23,10 +23,14 @@ concurrency: cancel-in-progress: true jobs: - build: - # Mirrors `pack-perf.yml` auto-trigger pattern: only run on - # PRs / pushes whose title or commit message references the pm - # scope. Manual workflow_dispatch always runs. + # Build manifest-bench only — `utoo` itself is built by `pm-e2e.yml`'s + # `build-linux-x64` job (linux artifact `utoo-linux-x64`) and the + # `bench` job below downloads it from there. We avoid the duplicate + # cargo build of utoo across the two workflows. + # + # `utoo-pm` builds for mac inside the bench job itself (pm-e2e + # builds mac in its e2e matrix but doesn't upload an artifact for it). + build-manifest-bench: if: > github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || @@ -52,25 +56,34 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: pm-bench-${{ matrix.platform }} - - name: Build current branch utoo + manifest-bench - run: | - cargo build --release --bin utoo -p utoo-pm - cargo build --release -p manifest-bench - - name: Upload utoo binary - uses: actions/upload-artifact@v4 - with: - name: utoo-${{ matrix.platform }}-bench-phases - path: target/release/utoo - retention-days: 1 + - name: Build manifest-bench + run: cargo build --release -p manifest-bench + # On mac we also need utoo here — pm-e2e builds it for mac in + # its e2e job but doesn't upload. On linux we download utoo from + # pm-e2e instead. + - name: Build utoo (mac only) + if: matrix.platform == 'mac' + run: cargo build --release --bin utoo -p utoo-pm - name: Upload manifest-bench binary uses: actions/upload-artifact@v4 with: name: manifest-bench-${{ matrix.platform }} path: target/release/manifest-bench retention-days: 1 + - name: Upload utoo binary (mac only) + if: matrix.platform == 'mac' + uses: actions/upload-artifact@v4 + with: + name: utoo-mac-bench-phases + path: target/release/utoo + retention-days: 1 bench: - needs: [build] + # Wait for both manifest-bench (this workflow) and pm-e2e's linux + # build (separate workflow) to finish. Linux uses pm-e2e's + # `utoo-linux-x64` artifact downloaded from the same commit's + # pm-e2e run. + needs: [build-manifest-bench] if: > github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || @@ -86,28 +99,79 @@ jobs: - os: macos-latest platform: mac runs-on: ${{ matrix.os }} - # Comment-writing on PRs needs `pull-requests: write`. Other tokens - # default to read-only on this workflow's events. Granular instead - # of `write-all` to keep the workflow's blast radius minimal. + # `pull-requests: write` for sticky comment posting. + # `actions: read` for `gh run list` to find pm-e2e's run id and + # for `download-artifact` cross-run via run-id. permissions: contents: read pull-requests: write + actions: read steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary + + # Linux: download utoo from pm-e2e's `build-linux-x64` job in + # the matching workflow run for this commit/PR. Avoids building + # utoo twice (once in pm-e2e, once here). + - name: Resolve pm-e2e run id (linux only) + if: matrix.platform == 'linux' + id: pme2e + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + # Find the most recent pm-e2e run for this commit. pm-e2e + # triggers on the same `(pm` scope so it should exist for + # any commit that reaches us. + RUN_ID=$(gh run list \ + --workflow utoopm-e2e \ + --commit "${{ github.event.pull_request.head.sha || github.sha }}" \ + --limit 1 \ + --json databaseId,status,conclusion \ + --jq '.[0].databaseId' || true) + if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then + echo "::warning::No pm-e2e run found for this commit. Falling back to local build." + echo "fallback=true" >> $GITHUB_OUTPUT + else + echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT + echo "Found pm-e2e run: $RUN_ID" + fi + + - name: Download utoo from pm-e2e (linux) + if: matrix.platform == 'linux' && steps.pme2e.outputs.fallback != 'true' uses: actions/download-artifact@v4 with: - name: utoo-${{ matrix.platform }}-bench-phases + name: utoo-linux-x64 path: dist + run-id: ${{ steps.pme2e.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Fallback: pm-e2e didn't run for this commit (e.g. workflow_dispatch + # without a PR / commit message scope mismatch) — build utoo locally. + - name: Build utoo locally (linux fallback) + if: matrix.platform == 'linux' && steps.pme2e.outputs.fallback == 'true' + run: | + mkdir -p dist + cargo build --release --bin utoo -p utoo-pm + cp target/release/utoo dist/utoo + + # Mac: utoo built by build-manifest-bench job in this workflow. + - name: Download utoo binary (mac) + if: matrix.platform == 'mac' + uses: actions/download-artifact@v4 + with: + name: utoo-mac-bench-phases + path: dist + - name: Download manifest-bench binary uses: actions/download-artifact@v4 with: name: manifest-bench-${{ matrix.platform }} path: dist + - name: Install utoo run: | chmod +x dist/utoo dist/manifest-bench From b383019e956f80a90f84d70b5e39a48e04ae7b16 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 00:16:31 +0800 Subject: [PATCH 07/19] ci(pm-bench-phases): verify pm-e2e artifact exists before reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bench job's `gh run list` would find a pm-e2e run for the commit even when its build job had been skipped (pm-e2e gates on `(pm)` with closing paren — `ci(pm-bench-phases): ...` doesn't match), causing `download-artifact` to fail. Check that the run actually uploaded `utoo-linux-x64` before deciding to reuse it, otherwise fall back to the local build path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-bench-phases.yml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml index 6c9b48b7a0..75217ed454 100644 --- a/.github/workflows/pm-bench-phases.yml +++ b/.github/workflows/pm-bench-phases.yml @@ -123,21 +123,34 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} run: | - # Find the most recent pm-e2e run for this commit. pm-e2e - # triggers on the same `(pm` scope so it should exist for - # any commit that reaches us. + # Find the most recent pm-e2e run for this commit, and verify + # it actually uploaded the `utoo-linux-x64` artifact. The run + # may exist but its build job may have been skipped (pm-e2e's + # gate is `(pm)` with closing paren, while this workflow's is + # the looser `(pm` — so titles like `ci(pm-bench-phases): ...` + # trigger us but skip pm-e2e), in which case the run completes + # with no artifact and the download would fail. + SHA="${{ github.event.pull_request.head.sha || github.sha }}" RUN_ID=$(gh run list \ --workflow utoopm-e2e \ - --commit "${{ github.event.pull_request.head.sha || github.sha }}" \ + --commit "$SHA" \ --limit 1 \ - --json databaseId,status,conclusion \ + --json databaseId \ --jq '.[0].databaseId' || true) if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then - echo "::warning::No pm-e2e run found for this commit. Falling back to local build." + echo "::warning::No pm-e2e run found for $SHA. Falling back to local build." + echo "fallback=true" >> $GITHUB_OUTPUT + exit 0 + fi + HAS_ARTIFACT=$(gh api "repos/${GH_REPO}/actions/runs/${RUN_ID}/artifacts" \ + --jq '.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false) | .id' \ + | head -n 1) + if [ -z "$HAS_ARTIFACT" ]; then + echo "::warning::pm-e2e run $RUN_ID has no utoo-linux-x64 artifact (build job likely skipped). Falling back to local build." echo "fallback=true" >> $GITHUB_OUTPUT else echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT - echo "Found pm-e2e run: $RUN_ID" + echo "Found pm-e2e run $RUN_ID with utoo-linux-x64 artifact" fi - name: Download utoo from pm-e2e (linux) From 7a2e00a9cbbd495b2fb47fa8d728549c4b5b59c6 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 00:45:30 +0800 Subject: [PATCH 08/19] ci(pm-build): consolidate pm-e2e + pm-bench + pm-bench-phases into one workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single workflow that builds utoo once per target and fans out to every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap), so each target's `cargo build --release` runs at most once per commit instead of being duplicated across the three previous workflows. Build jobs (one per target, gated on the union of consumer gates): - build-linux x86_64-unknown-linux-gnu - build-mac-arm64 aarch64-apple-darwin - build-mac-x64 x86_64-apple-darwin - build-windows x86_64-pc-windows-msvc Downstream jobs (pure download-and-run, no cargo build): - e2e-{linux,mac-arm64,mac-x64,windows} auto on `(pm)` scope - bench-phases-{linux,mac} auto on `(pm` scope - pm-bench-{linux,mac} manual workflow_dispatch - pm-bench-pcap-linux manual workflow_dispatch Job names like `utoopm-e2e-success` and the per-target e2e check names are preserved so existing branch-protection rules continue to work. Also delete `crates/manifest-bench` (debug-only HTTP fetch tool) and its `.github/scripts/run-manifest-bench.sh` helper — no longer used. The pcap step previously ran inline on every bench-phases run; it now lives in its own workflow_dispatch target since pcap is only useful during manual perf analysis. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/render-bench-phases-comment.sh | 59 ++ .github/scripts/run-manifest-bench.sh | 51 - .github/workflows/pm-bench-phases.yml | 422 -------- .github/workflows/pm-bench.yml | 338 ------- .github/workflows/pm-build.yml | 921 ++++++++++++++++++ .github/workflows/pm-e2e.yml | 321 ------ Cargo.lock | 53 - Cargo.toml | 1 - crates/manifest-bench/Cargo.toml | 31 - crates/manifest-bench/src/main.rs | 371 ------- 10 files changed, 980 insertions(+), 1588 deletions(-) create mode 100755 .github/scripts/render-bench-phases-comment.sh delete mode 100755 .github/scripts/run-manifest-bench.sh delete mode 100644 .github/workflows/pm-bench-phases.yml delete mode 100644 .github/workflows/pm-bench.yml create mode 100644 .github/workflows/pm-build.yml delete mode 100644 .github/workflows/pm-e2e.yml delete mode 100644 crates/manifest-bench/Cargo.toml delete mode 100644 crates/manifest-bench/src/main.rs diff --git a/.github/scripts/render-bench-phases-comment.sh b/.github/scripts/render-bench-phases-comment.sh new file mode 100755 index 0000000000..312fbd0d03 --- /dev/null +++ b/.github/scripts/render-bench-phases-comment.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Render the pm-bench-phases sticky PR comment from the per-registry log +# files in /tmp/pm-bench-output/. Called by pm-build.yml's bench-phases-* +# jobs. Output: /tmp/pm-bench-output/pr_comment.md +# +# Args: +# $1 PLATFORM e.g. linux, mac +# $2 OS e.g. ubuntu-latest, macos-latest +set -eu + +PLATFORM="$1" +OS="$2" +OUT=/tmp/pm-bench-output/pr_comment.md +SHA="${GITHUB_SHA:0:7}" +RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + +mkdir -p /tmp/pm-bench-output +{ + echo "## 📊 pm-bench-phases · \`$SHA\` · ${PLATFORM} (\`${OS}\`)" + echo "" + echo "[Workflow run]($RUN_URL) — ant-design" + echo "" +} > "$OUT" + +# Extract `## p_` sections from a log under a labeled heading. +render_log() { + local label="$1" + local file="$2" + if [ ! -f "$file" ]; then + echo "_${label}: no output captured._" + echo "" + return + fi + echo "### ${label}" + echo "" + local cleaned + cleaned=$(mktemp) + sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$file" > "$cleaned" + awk ' + /^## p[0-9]+_[a-z0-9_]+/ { + if (in_section) print "```\n"; + in_section = 1; + print "```"; + print $0; + next; + } + /^## / && in_section { + print "```\n"; + in_section = 0; + } + in_section { print } + END { if (in_section) print "```" } + ' "$cleaned" + echo "" + rm -f "$cleaned" +} + +render_log "npmjs.org" /tmp/pm-bench-output/bench-phases-npmjs.log >> "$OUT" +render_log "npmmirror.com" /tmp/pm-bench-output/bench-phases-npmmirror.log >> "$OUT" diff --git a/.github/scripts/run-manifest-bench.sh b/.github/scripts/run-manifest-bench.sh deleted file mode 100755 index 02a6f454d3..0000000000 --- a/.github/scripts/run-manifest-bench.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# Run manifest-bench (standalone HTTP-only fetch tool) against a -# given REGISTRY for a given PROJECT. Used by `pm-bench-phases.yml` -# to capture HTTP stack ceiling numbers per registry / per platform. -# -# Required env: REGISTRY, PROJECT -# Generates the lockfile via `utoo deps` if it doesn't exist yet. -set -eu - -PROJECT_DIR="/tmp/pm-bench/$PROJECT" -if [ ! -d "$PROJECT_DIR" ]; then - mkdir -p /tmp/pm-bench - git clone --depth 1 "https://github.com/ant-design/$PROJECT" "$PROJECT_DIR" -fi -cd "$PROJECT_DIR" -if [ ! -f package-lock.json ]; then - echo "==> generating lockfile via utoo (registry=$REGISTRY)" - utoo deps --registry "$REGISTRY" || true -fi -ls -la package-lock.json || { echo "no lockfile, skip"; exit 0; } - -echo -echo "============================================================" -echo "manifest-bench: HTTP-only fetch (no parse, no resolver)" -echo " registry=$REGISTRY" -echo " Goal: isolate reqwest/rustls/tokio behaviour from" -echo " ruborist's resolver pipeline. Same numbers (wall, busy," -echo " avg_conc, p50/p95/max) as ruborist's Preload HTTP diag." -echo "============================================================" - -for CAP in 32 64 96 128 192 256; do - echo - echo "--- concurrency=$CAP, h1, full manifest, default UA ---" - manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency "$CAP" --reps 2 --http1-only || true -done - -echo -echo "--- concurrency=128, h2 negotiate, full manifest, default UA ---" -manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 || true - -echo -echo "--- concurrency=128, h1, single-version endpoint ---" -manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 --http1-only --single-version || true - -echo -echo "--- concurrency=128, h1, UA=Bun/1.2.21 ---" -manifest-bench --lockfile package-lock.json --registry "$REGISTRY" \ - --concurrency 128 --reps 2 --http1-only --user-agent "Bun/1.2.21" || true diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml deleted file mode 100644 index 75217ed454..0000000000 --- a/.github/workflows/pm-bench-phases.yml +++ /dev/null @@ -1,422 +0,0 @@ -name: utoopm-benchmark-phases - -on: - workflow_dispatch: - inputs: - project: - description: 'Project to benchmark (under github.com/ant-design/)' - required: false - default: 'ant-design' - type: string - runs: - description: 'Runs per phase' - required: false - default: '3' - type: string - pull_request: - branches: [main, next] - push: - branches: [main, next] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - # Build manifest-bench only — `utoo` itself is built by `pm-e2e.yml`'s - # `build-linux-x64` job (linux artifact `utoo-linux-x64`) and the - # `bench` job below downloads it from there. We avoid the duplicate - # cargo build of utoo across the two workflows. - # - # `utoo-pm` builds for mac inside the bench job itself (pm-e2e - # builds mac in its e2e matrix but doesn't upload an artifact for it). - build-manifest-bench: - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: linux - - os: macos-latest - platform: mac - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-${{ matrix.platform }} - - name: Build manifest-bench - run: cargo build --release -p manifest-bench - # On mac we also need utoo here — pm-e2e builds it for mac in - # its e2e job but doesn't upload. On linux we download utoo from - # pm-e2e instead. - - name: Build utoo (mac only) - if: matrix.platform == 'mac' - run: cargo build --release --bin utoo -p utoo-pm - - name: Upload manifest-bench binary - uses: actions/upload-artifact@v4 - with: - name: manifest-bench-${{ matrix.platform }} - path: target/release/manifest-bench - retention-days: 1 - - name: Upload utoo binary (mac only) - if: matrix.platform == 'mac' - uses: actions/upload-artifact@v4 - with: - name: utoo-mac-bench-phases - path: target/release/utoo - retention-days: 1 - - bench: - # Wait for both manifest-bench (this workflow) and pm-e2e's linux - # build (separate workflow) to finish. Linux uses pm-e2e's - # `utoo-linux-x64` artifact downloaded from the same commit's - # pm-e2e run. - needs: [build-manifest-bench] - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) - strategy: - # Don't cancel mac if linux fails (or vice versa) — each platform's - # comment is independent and useful on its own. - fail-fast: false - matrix: - include: - - os: ubuntu-latest - platform: linux - - os: macos-latest - platform: mac - runs-on: ${{ matrix.os }} - # `pull-requests: write` for sticky comment posting. - # `actions: read` for `gh run list` to find pm-e2e's run id and - # for `download-artifact` cross-run via run-id. - permissions: - contents: read - pull-requests: write - actions: read - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - # Linux: download utoo from pm-e2e's `build-linux-x64` job in - # the matching workflow run for this commit/PR. Avoids building - # utoo twice (once in pm-e2e, once here). - - name: Resolve pm-e2e run id (linux only) - if: matrix.platform == 'linux' - id: pme2e - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - # Find the most recent pm-e2e run for this commit, and verify - # it actually uploaded the `utoo-linux-x64` artifact. The run - # may exist but its build job may have been skipped (pm-e2e's - # gate is `(pm)` with closing paren, while this workflow's is - # the looser `(pm` — so titles like `ci(pm-bench-phases): ...` - # trigger us but skip pm-e2e), in which case the run completes - # with no artifact and the download would fail. - SHA="${{ github.event.pull_request.head.sha || github.sha }}" - RUN_ID=$(gh run list \ - --workflow utoopm-e2e \ - --commit "$SHA" \ - --limit 1 \ - --json databaseId \ - --jq '.[0].databaseId' || true) - if [ -z "$RUN_ID" ] || [ "$RUN_ID" == "null" ]; then - echo "::warning::No pm-e2e run found for $SHA. Falling back to local build." - echo "fallback=true" >> $GITHUB_OUTPUT - exit 0 - fi - HAS_ARTIFACT=$(gh api "repos/${GH_REPO}/actions/runs/${RUN_ID}/artifacts" \ - --jq '.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false) | .id' \ - | head -n 1) - if [ -z "$HAS_ARTIFACT" ]; then - echo "::warning::pm-e2e run $RUN_ID has no utoo-linux-x64 artifact (build job likely skipped). Falling back to local build." - echo "fallback=true" >> $GITHUB_OUTPUT - else - echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT - echo "Found pm-e2e run $RUN_ID with utoo-linux-x64 artifact" - fi - - - name: Download utoo from pm-e2e (linux) - if: matrix.platform == 'linux' && steps.pme2e.outputs.fallback != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - run-id: ${{ steps.pme2e.outputs.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - # Fallback: pm-e2e didn't run for this commit (e.g. workflow_dispatch - # without a PR / commit message scope mismatch) — build utoo locally. - - name: Build utoo locally (linux fallback) - if: matrix.platform == 'linux' && steps.pme2e.outputs.fallback == 'true' - run: | - mkdir -p dist - cargo build --release --bin utoo -p utoo-pm - cp target/release/utoo dist/utoo - - # Mac: utoo built by build-manifest-bench job in this workflow. - - name: Download utoo binary (mac) - if: matrix.platform == 'mac' - uses: actions/download-artifact@v4 - with: - name: utoo-mac-bench-phases - path: dist - - - name: Download manifest-bench binary - uses: actions/download-artifact@v4 - with: - name: manifest-bench-${{ matrix.platform }} - path: dist - - - name: Install utoo - run: | - chmod +x dist/utoo dist/manifest-bench - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - # hyperfine + GNU time available via OS package manager. tcpdump - # only needed for pcap step (linux only — sudo + raw sockets not - # generally available on macos GH runners). - - name: Install bench tools (linux) - if: matrix.platform == 'linux' - run: sudo apt-get update && sudo apt-get install -y hyperfine time tcpdump dnsutils - - name: Install bench tools (mac) - if: matrix.platform == 'mac' - run: | - brew install hyperfine - # macOS has /usr/bin/time built-in; pm-bench-phases.sh's metrics - # wrapper uses `time -v` (GNU). Install gnu-time and alias. - brew install gnu-time - # symlink so `time` in scripts hits gnu-time - mkdir -p "$HOME/bin" - ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" - echo "$HOME/bin" >> $GITHUB_PATH - - name: Verify tools - run: | - hyperfine --version - utoo --version - bun --version - - # ============================================================ - # Helper: wipe everything between registry runs so cache + state - # from the previous registry doesn't leak into the next one. - # The PROJECT clone is removed too — the bench script re-clones. - # ============================================================ - - name: Define cache wipe function - run: | - cat > /tmp/wipe-bench-state.sh <<'WIPE' - #!/usr/bin/env bash - # Wipe ALL bench state — caches, project clone, results, lockfiles. - # Used between registry runs so the second one starts truly cold. - rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/bun-bench-cache \ - ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true - mkdir -p /tmp/pm-bench-output - WIPE - chmod +x /tmp/wipe-bench-state.sh - - # ------------------------------------------------------------ - # Run 1: npmjs.org (the primary upstream) - # ------------------------------------------------------------ - - name: Wipe bench state before npmjs - run: bash /tmp/wipe-bench-state.sh - - - name: Run phase-isolated benchmark · npmjs - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - BENCH_RUNS: ${{ github.event.inputs.runs || '3' }} - PM_LIST: 'utoo,bun' - run: | - chmod +x e2e/pm-bench-phases.sh - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - - name: Standalone manifest-bench · npmjs - env: - REGISTRY: 'https://registry.npmjs.org' - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - run: bash .github/scripts/run-manifest-bench.sh 2>&1 | tee /tmp/pm-bench-output/manifest-bench-npmjs.log - - # ------------------------------------------------------------ - # Run 2: npmmirror.com (Asia-Pacific, semver-supporting registry) - # ------------------------------------------------------------ - # Save the npmjs logs out of /tmp/pm-bench-output before wipe - # (the wipe also clears the output dir for cleanliness). - - name: Stash npmjs logs and wipe state before npmmirror - run: | - mkdir -p /tmp/bench-logs-stash - cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true - bash /tmp/wipe-bench-state.sh - mkdir -p /tmp/pm-bench-output - cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - - - name: Run phase-isolated benchmark · npmmirror - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmmirror.com' - BENCH_RUNS: ${{ github.event.inputs.runs || '3' }} - PM_LIST: 'utoo,bun' - run: | - mkdir -p /tmp/pm-bench-output - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - - - name: Standalone manifest-bench · npmmirror - env: - REGISTRY: 'https://registry.npmmirror.com' - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - run: bash .github/scripts/run-manifest-bench.sh 2>&1 | tee /tmp/pm-bench-output/manifest-bench-npmmirror.log - - # PCAP only on linux (mac runners lack tcpdump + raw socket perms). - - name: Capture pcap (resolve phase, single run per PM) - if: matrix.platform == 'linux' - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - run: | - chmod +x e2e/pm-bench-pcap.sh - bash e2e/pm-bench-pcap.sh - - - name: Upload pcap artifact - if: always() && matrix.platform == 'linux' - uses: actions/upload-artifact@v4 - with: - name: pm-bench-pcap - path: /tmp/pm-bench-pcap - retention-days: 3 - - - name: Upload bench logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: pm-bench-logs-${{ matrix.platform }} - path: /tmp/pm-bench-output - retention-days: 7 - - # Build the comment body from BOTH registry runs and BOTH phases' - # output. Strips ANSI escape codes; pulls out per-phase summary - # tables for each registry under a labeled section. - - name: Build PR comment body - if: always() - run: | - set -eu - OUT=/tmp/pm-bench-output/pr_comment.md - - SHA="${GITHUB_SHA:0:7}" - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - PLATFORM='${{ matrix.platform }}' - OS='${{ matrix.os }}' - - { - echo "## 📊 pm-bench-phases · \`$SHA\` · ${PLATFORM} (\`${OS}\`)" - echo "" - echo "[Workflow run]($RUN_URL) — ant-design" - echo "" - } > "$OUT" - - # Helper to extract `## p_` sections from a log file - # and emit them under a labeled heading. - render_log() { - local label="$1" - local file="$2" - if [ ! -f "$file" ]; then - echo "_${label}: no output captured._" - echo "" - return - fi - echo "### ${label}" - echo "" - local cleaned - cleaned=$(mktemp) - sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$file" > "$cleaned" - awk ' - /^## p[0-9]+_[a-z0-9_]+/ { - if (in_section) print "```\n"; - in_section = 1; - print "```"; - print $0; - next; - } - /^## / && in_section { - print "```\n"; - in_section = 0; - } - in_section { print } - END { if (in_section) print "```" } - ' "$cleaned" - echo "" - rm -f "$cleaned" - } - - render_log "npmjs.org" /tmp/pm-bench-output/bench-phases-npmjs.log >> "$OUT" - render_log "npmmirror.com" /tmp/pm-bench-output/bench-phases-npmmirror.log >> "$OUT" - - # Manifest-bench: include only the section markers + rep - # summary lines so the comment doesn't balloon. - { - echo "### manifest-bench (HTTP-only fetch sweep)" - echo "" - for label in npmjs npmmirror; do - mb_log="/tmp/pm-bench-output/manifest-bench-${label}.log" - if [ -f "$mb_log" ]; then - echo "**${label}**" - echo "" - echo '```' - sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$mb_log" \ - | grep -E '^---|rep [12]\] n=' || echo '(no manifest-bench output)' - echo '```' - echo "" - fi - done - } >> "$OUT" - - - name: Find Current PR - id: findPr - if: always() - run: | - PR_NUMBER="" - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) - fi - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - - # Each platform posts its own sticky comment (different `header`) - # so linux + mac results live as two separate comments and don't - # race / overwrite each other. - - name: Post PR Comment - if: always() && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-${{ matrix.platform }} - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pm-bench.yml b/.github/workflows/pm-bench.yml deleted file mode 100644 index 64277e104d..0000000000 --- a/.github/workflows/pm-bench.yml +++ /dev/null @@ -1,338 +0,0 @@ -name: utoopm-benchmark - -on: - workflow_dispatch: - inputs: - registry: - description: 'Registry to test' - required: true - default: 'both' - type: choice - options: - - both - - npmjs - - npmmirror - pm_list: - description: 'Comma-separated list (e.g. utoo,aube or utoo,pnpm,bun,aube); "all" expands to utoo,pnpm,yarn,bun,aube' - required: true - default: 'utoo,bun' - type: string - baseline: - description: 'Compare with published utoo from npm' - required: false - default: true - type: boolean - build_current: - description: 'Build utoo from the current branch (disable to benchmark only the npm baseline + other PMs)' - required: false - default: true - type: boolean - build_next: - description: 'Compare with utoo built from next branch' - required: false - default: false - type: boolean - cold_runs: - description: 'Number of cold install runs' - required: false - default: '3' - type: string - warm_runs: - description: 'Number of warm install runs' - required: false - default: '3' - type: string - pull_request: - types: [labeled] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build-linux: - if: > - (github.event_name == 'workflow_dispatch' && (github.event.inputs.build_current != 'false' || github.event.inputs.build_next == 'true')) || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: ubuntu-latest - container: - image: ubuntu:20.04 - steps: - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git - - - uses: actions/checkout@v4 - - - name: Configure git safe directory - run: git config --global --add safe.directory /__w/utoo/utoo - - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-linux - - - name: Build current branch utoo - if: github.event.inputs.build_current != 'false' - run: cargo build --release --bin utoo -p utoo-pm - - - name: Upload current binary - if: github.event.inputs.build_current != 'false' - uses: actions/upload-artifact@v4 - with: - name: utoo-linux-bench-current - path: target/release/utoo - retention-days: 1 - - - name: Build next branch utoo - if: github.event.inputs.build_next == 'true' - run: | - git fetch origin next --depth=1 - git checkout origin/next - git submodule update --init --recursive --depth 1 - cargo build --release --bin utoo -p utoo-pm - - - name: Upload next binary - if: github.event.inputs.build_next == 'true' - uses: actions/upload-artifact@v4 - with: - name: utoo-linux-bench-next - path: target/release/utoo - retention-days: 1 - - benchmark-linux: - needs: [build-linux] - if: > - always() && - (needs.build-linux.result == 'success' || needs.build-linux.result == 'skipped') && - (github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark')) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Download current binary - if: github.event.inputs.build_current != 'false' - uses: actions/download-artifact@v4 - with: - name: utoo-linux-bench-current - path: dist - - - name: Setup current binary - if: github.event.inputs.build_current != 'false' - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - - name: Create ut symlink - if: github.event.inputs.build_current != 'false' - run: ln -sf utoo $(dirname $(which utoo))/ut - - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - if [ "${{ github.event.inputs.build_current }}" != "false" ]; then - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV - else - ln -sf "$(which utoo)" "$(dirname "$(which utoo)")/ut" - echo "utoo (npm, primary): $(utoo --version)" - fi - - - name: Setup next binary - if: github.event.inputs.build_next == 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-linux-bench-next - path: dist-next - - - name: Install next binary - if: github.event.inputs.build_next == 'true' - run: | - chmod +x dist-next/utoo - cp dist-next/utoo /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - - name: Install hyperfine and GNU time - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - - name: Verify tools - run: | - echo "hyperfine: $(hyperfine --version)" - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - - name: Run benchmark - run: | - chmod +x e2e/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} - - benchmark: - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - - name: Init git submodules - if: github.event.inputs.build_current != 'false' || github.event.inputs.build_next == 'true' - run: git submodule update --init --recursive --depth 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install Rust - if: github.event.inputs.build_current != 'false' || github.event.inputs.build_next == 'true' - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - - name: Cache cargo - if: github.event.inputs.build_current != 'false' || github.event.inputs.build_next == 'true' - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-macos-latest - - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - if [ "${{ github.event.inputs.build_current }}" != "false" ]; then - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV - else - ln -sf "$(which utoo)" "$(dirname "$(which utoo)")/ut" - echo "utoo (npm, primary): $(utoo --version)" - fi - - - name: Build baseline utoo from next branch - if: github.event.inputs.build_next == 'true' - run: | - CURRENT_SHA=$(git rev-parse HEAD) - git fetch origin next --depth=1 - git checkout origin/next - git submodule update --init --recursive --depth 1 - cargo build --release --bin utoo -p utoo-pm - cp target/release/utoo /tmp/utoo-next - chmod +x /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - git checkout "$CURRENT_SHA" - git submodule update --init --recursive --depth 1 - - - name: Build utoo-pm - if: github.event.inputs.build_current != 'false' - run: cargo build --release --bin utoo -p utoo-pm - - - name: Update PATH - if: github.event.inputs.build_current != 'false' - run: echo "${{ github.workspace }}/target/release" >> $GITHUB_PATH - - - name: Create ut symlink - if: github.event.inputs.build_current != 'false' - run: ln -sf utoo $(dirname $(which utoo))/ut - - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - - name: Install hyperfine - run: brew install hyperfine - - - name: Verify tools - run: | - echo "hyperfine: $(hyperfine --version)" - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - - name: Run benchmark - run: | - chmod +x e2e/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml new file mode 100644 index 0000000000..11d1720868 --- /dev/null +++ b/.github/workflows/pm-build.yml @@ -0,0 +1,921 @@ +name: utoopm-build + +# Single workflow that builds utoo once per target, then fans out to +# every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Replaces +# the old pm-e2e.yml + pm-bench.yml + pm-bench-phases.yml trio so each +# target builds exactly once per commit and downstream jobs reuse the +# uploaded artifact. +# +# Triggers (event → consumers): +# pull_request / push with `(pm)` in title/msg → e2e jobs +# pull_request / push with `(pm` (loose) in title/msg → bench-phases +# pull_request labeled `benchmark` → pm-bench (manual perf compare) +# workflow_dispatch with `target` choice → manual one-off runs +# +# Build job gates are the union of their downstream consumers' gates. +# Each downstream job is pure download-and-run, no `cargo build`. +on: + push: + branches: ["next", "e2e-pm"] + pull_request: + types: [opened, synchronize, reopened, edited, labeled] + workflow_dispatch: + inputs: + target: + description: 'Manual target to run' + required: true + type: choice + options: + - pm-bench + - pm-bench-phases + - pm-bench-pcap + default: pm-bench + # ----- pm-bench inputs ----- + registry: + description: '[pm-bench] Registry to test' + required: false + default: 'both' + type: choice + options: [both, npmjs, npmmirror] + pm_list: + description: '[pm-bench] Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' + required: false + default: 'utoo,bun' + type: string + baseline: + description: '[pm-bench] Compare with published utoo from npm' + required: false + default: true + type: boolean + build_next: + description: '[pm-bench] Also compare with utoo built from `next` branch' + required: false + default: false + type: boolean + cold_runs: + description: '[pm-bench] Number of cold install runs' + required: false + default: '3' + type: string + warm_runs: + description: '[pm-bench] Number of warm install runs' + required: false + default: '3' + type: string + # ----- pm-bench-phases / pcap inputs ----- + project: + description: '[phases/pcap] Project to bench (under github.com/ant-design/)' + required: false + default: 'ant-design' + type: string + bench_runs: + description: '[phases] Runs per phase' + required: false + default: '3' + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # ============================================================ + # BUILD JOBS + # One per target, runs whenever any downstream consumer needs it. + # Downstream jobs `needs:` the relevant build job and download + # the uploaded artifact. No downstream rebuilds. + # ============================================================ + + build-linux: + # Linux serves: e2e-linux, bench-phases-linux, pm-bench-linux, pm-bench-pcap-linux. + # Gate is the union of all linux consumers' gates. + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + container: + image: ubuntu:20.04 + steps: + - name: Install dependencies + timeout-minutes: 6 + env: + DEBIAN_FRONTEND: noninteractive + # Short per-apt timeouts: on GHA runners that can't route to + # archive/security.ubuntu.com, apt otherwise sits in 90s × 8-IP SYN + # timeouts. Bail after 60s on the default mirror, swap to the + # Fastly-backed kernel.org mirror, and retry once. + run: | + install_pkgs() { + timeout 60 apt-get update \ + && timeout 60 apt-get install -y build-essential curl pkg-config git + } + install_pkgs && exit 0 + echo "apt default mirror failed/timed out, falling back to mirrors.edge.kernel.org" + dpkg --configure -a || true + sed -i 's|http://archive\.ubuntu\.com|http://mirrors.edge.kernel.org|g; s|http://security\.ubuntu\.com|http://mirrors.edge.kernel.org|g' /etc/apt/sources.list + install_pkgs + - uses: actions/checkout@v4 + - name: Configure git safe directory + run: git config --global --add safe.directory /__w/utoo/utoo + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + targets: x86_64-unknown-linux-gnu + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-build-linux + - name: Build utoo-pm + run: cargo build --release --target x86_64-unknown-linux-gnu --bin utoo -p utoo-pm + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-linux-x64 + path: target/x86_64-unknown-linux-gnu/release/utoo + retention-days: 1 + + build-mac-arm64: + # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-mac. + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || + (github.event_name == 'workflow_dispatch' && inputs.target != 'pm-bench-pcap') + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + targets: aarch64-apple-darwin + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-build-mac-arm64 + - name: Build utoo-pm + run: cargo build --release --target aarch64-apple-darwin --bin utoo -p utoo-pm + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-mac-arm64 + path: target/aarch64-apple-darwin/release/utoo + retention-days: 1 + + build-mac-x64: + # Mac x86_64 serves: e2e-mac-x64 only. Cross-compiled on macos-latest + # (arm64 host) via `rustup target add x86_64-apple-darwin`. + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + targets: x86_64-apple-darwin + - name: Add cross-compilation target + run: rustup target add x86_64-apple-darwin + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-build-mac-x64 + - name: Build utoo-pm + run: cargo build --release --target x86_64-apple-darwin --bin utoo -p utoo-pm + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-mac-x64 + path: target/x86_64-apple-darwin/release/utoo + retention-days: 1 + + build-windows: + # Windows serves: e2e-windows only. + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Disable Windows Defender + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $true + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + targets: x86_64-pc-windows-msvc + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-build-windows + - name: Build utoo-pm + run: cargo build --release --target x86_64-pc-windows-msvc --bin utoo -p utoo-pm + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-windows-x64 + path: target/x86_64-pc-windows-msvc/release/utoo.exe + retention-days: 1 + + # ============================================================ + # E2E JOBS + # Auto-trigger on `(pm)` (strict) PR title / commit msg. + # ============================================================ + + e2e-linux: + needs: [build-linux] + name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-arm64: + needs: [build-mac-arm64] + name: utoopm-e2e-macos-latest-aarch64-apple-darwin + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-aarch64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-x64: + needs: [build-mac-x64] + name: utoopm-e2e-macos-latest-x86_64-apple-darwin + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install x86_64 Node.js via nvm + run: | + arch -x86_64 bash -c ' + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm install 22 + nvm use 22 + nvm alias default 22 + echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) + echo "$NODE_PATH/bin" >> $GITHUB_PATH + node -p process.arch + npm -v + ' + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/utoo-pm.sh + ' + - name: Run arborist e2e tests + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/pm-arborist.sh + ' + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-x86_64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-windows: + needs: [build-windows] + name: utoopm-e2e-windows-latest-x86_64-pc-windows-msvc + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Disable Windows Defender + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $true + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-windows-x64 + path: dist + - name: Install utoo + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + # ut.exe is just a copy of utoo.exe (Windows has no symlinks). + Copy-Item "$binPath\utoo.exe" "$binPath\ut.exe" -Force + echo "$binPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Verify tools + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + Get-Command utoo | Select-Object -ExpandProperty Source + Get-Command ut | Select-Object -ExpandProperty Source + utoo --version + ut --version + - name: Run e2e tests + shell: pwsh + run: | + git config --system core.longpaths true + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + pwsh -File e2e/utoo-pm.ps1 --verbose + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-windows-latest-x86_64-pc-windows-msvc + path: ${{ runner.temp }}/utoo-* + if-no-files-found: ignore + + e2e-success: + name: utoopm-e2e-success + runs-on: ubuntu-latest + if: always() + needs: [build-linux, build-mac-arm64, build-mac-x64, build-windows, e2e-linux, e2e-mac-arm64, e2e-mac-x64, e2e-windows] + steps: + - name: Check success + run: | + for result in \ + "${{ needs.build-linux.result }}" \ + "${{ needs.build-mac-arm64.result }}" \ + "${{ needs.build-mac-x64.result }}" \ + "${{ needs.build-windows.result }}" \ + "${{ needs.e2e-linux.result }}" \ + "${{ needs.e2e-mac-arm64.result }}" \ + "${{ needs.e2e-mac-x64.result }}" \ + "${{ needs.e2e-windows.result }}"; do + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "Tests failed or were cancelled" + exit 1 + fi + done + echo "Tests passed or were skipped" + + # ============================================================ + # BENCH-PHASES JOBS + # Auto-trigger on `(pm` (loose) — covers `(pm)`, `(pm-bench-phases)`, + # `(pm-foo)`, etc. Posts a sticky PR comment per platform with the + # phase-isolated bench results across npmjs + npmmirror. + # ============================================================ + + bench-phases-linux: + needs: [build-linux] + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Verify tools + run: | + hyperfine --version + utoo --version + bun --version + - name: Define cache wipe function + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + chmod +x e2e/pm-bench-phases.sh + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-linux + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() + run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest + - name: Find Current PR + id: findPr + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-linux + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + bench-phases-mac: + needs: [build-mac-arm64] + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') + runs-on: macos-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + # macOS /usr/bin/time isn't GNU; e2e/pm-bench-phases.sh uses + # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin + # symlink earlier on PATH. + run: | + brew install hyperfine gnu-time + mkdir -p "$HOME/bin" + ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" + echo "$HOME/bin" >> $GITHUB_PATH + - name: Verify tools + run: | + hyperfine --version + utoo --version + bun --version + - name: Define cache wipe function + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + chmod +x e2e/pm-bench-phases.sh + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-mac + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() + run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest + - name: Find Current PR + id: findPr + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-mac + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ============================================================ + # PM-BENCH JOBS (manual perf compare) + # workflow_dispatch with target=pm-bench, or PR labeled `benchmark`. + # Compares utoo against other PMs (pnpm/yarn/bun/aube) on cold + warm + # installs across configurable registries. + # ============================================================ + + pm-bench-linux: + needs: [build-linux] + if: > + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + npm install -g utoo + BASELINE_PATH=$(which utoo) + cp "$BASELINE_PATH" /tmp/utoo-npm + chmod +x /tmp/utoo-npm + echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" + echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + - name: Build next branch utoo + if: github.event.inputs.build_next == 'true' + run: | + # The PATH already points at dist/utoo (current branch) — build + # `next` separately to /tmp so it doesn't shadow. + git fetch origin next --depth=1 + WORKDIR=$(mktemp -d) + git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . + (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) + cp "$WORKDIR/target/release/utoo" /tmp/utoo-next + chmod +x /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine and GNU time + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x e2e/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + + pm-bench-mac: + needs: [build-mac-arm64] + if: > + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + npm install -g utoo + BASELINE_PATH=$(which utoo) + cp "$BASELINE_PATH" /tmp/utoo-npm + chmod +x /tmp/utoo-npm + echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" + echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + - name: Build next branch utoo + if: github.event.inputs.build_next == 'true' + run: | + git fetch origin next --depth=1 + WORKDIR=$(mktemp -d) + git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . + (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) + cp "$WORKDIR/target/release/utoo" /tmp/utoo-next + chmod +x /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine + run: brew install hyperfine + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x e2e/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + + # ============================================================ + # PM-BENCH-PCAP (manual perf-debug only) + # workflow_dispatch with target=pm-bench-pcap. Captures tcpdump + # of the resolve phase per PM (linux only — sudo + raw sockets). + # ============================================================ + + pm-bench-pcap-linux: + needs: [build-linux] + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-pcap' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install pcap tools + run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time + - name: Capture pcap + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + run: | + chmod +x e2e/pm-bench-pcap.sh + bash e2e/pm-bench-pcap.sh + - name: Upload pcap artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-pcap + path: /tmp/pm-bench-pcap + retention-days: 7 diff --git a/.github/workflows/pm-e2e.yml b/.github/workflows/pm-e2e.yml deleted file mode 100644 index e4d7815c53..0000000000 --- a/.github/workflows/pm-e2e.yml +++ /dev/null @@ -1,321 +0,0 @@ -name: utoopm-e2e - -on: - push: - branches: ["next", "e2e-pm"] - pull_request: - types: [opened, synchronize, reopened, edited] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build-linux-x64: - if: (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - runs-on: ubuntu-latest - container: - image: ubuntu:20.04 - steps: - - name: Install dependencies - timeout-minutes: 6 - env: - DEBIAN_FRONTEND: noninteractive - # Short per-apt timeouts: on GHA runners that can't route to - # archive/security.ubuntu.com, apt otherwise sits in 90s × 8-IP SYN - # timeouts. Bail after 60s on the default mirror, swap to the - # Fastly-backed kernel.org mirror, and retry once. - # - # If the first attempt times out mid-unpack (e.g. libc6 upgrade), - # dpkg is left half-configured and the retry fails with - # "dpkg was interrupted". `dpkg --configure -a` finalises any - # pending unpack before we retry on the fallback mirror. - run: | - install_pkgs() { - timeout 60 apt-get update \ - && timeout 60 apt-get install -y build-essential curl pkg-config git - } - install_pkgs && exit 0 - echo "apt default mirror failed/timed out, falling back to mirrors.edge.kernel.org" - dpkg --configure -a || true - sed -i 's|http://archive\.ubuntu\.com|http://mirrors.edge.kernel.org|g; s|http://security\.ubuntu\.com|http://mirrors.edge.kernel.org|g' /etc/apt/sources.list - install_pkgs - - - uses: actions/checkout@v4 - - - name: Configure git safe directory - run: git config --global --add safe.directory /__w/utoo/utoo - - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - targets: x86_64-unknown-linux-gnu - - - name: Build utoo-pm - run: cargo build --release --target x86_64-unknown-linux-gnu --bin utoo -p utoo-pm - - - name: Upload binary - uses: actions/upload-artifact@v4 - with: - name: utoo-linux-x64 - path: target/x86_64-unknown-linux-gnu/release/utoo - retention-days: 1 - - e2e-linux: - needs: [build-linux-x64] - name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Download linux binary - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - - - name: Update PATH - run: | - chmod +x dist/utoo - echo "${{ github.workspace }}/dist" >> $GITHUB_PATH - - - name: Create ut symlink - run: ln -sf utoo $(dirname $(which utoo))/ut - - - name: Verify tools - run: | - echo "utoo: $(which utoo)" - echo "ut: $(which ut)" - utoo --version - ut --version - - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - - name: Run e2e tests - run: bash e2e/utoo-pm.sh - - - name: Run arborist e2e tests - run: bash e2e/pm-arborist.sh - - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e: - if: (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - strategy: - fail-fast: false - matrix: - settings: - - host: macos-latest - target: aarch64-apple-darwin - - host: macos-latest - target: x86_64-apple-darwin - - host: windows-latest - target: x86_64-pc-windows-msvc - - name: utoopm-e2e-${{ matrix.settings.host }}-${{ matrix.settings.target }} - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v4 - - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - # Disable Windows Defender real-time scanning to speed up I/O-heavy builds (~30-50% faster) - - name: Disable Windows Defender - if: runner.os == 'Windows' - shell: powershell - run: Set-MpPreference -DisableRealtimeMonitoring $true - - - name: Install x86_64 Node.js via nvm - if: matrix.settings.target == 'x86_64-apple-darwin' - run: | - arch -x86_64 bash -c ' - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 22 - nvm use 22 - nvm alias default 22 - echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV - NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) - echo "$NODE_PATH/bin" >> $GITHUB_PATH - node -p process.arch - npm -v - ' - - - name: Setup node - if: matrix.settings.target != 'x86_64-apple-darwin' - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - targets: ${{ matrix.settings.target }} - - - name: Add cross-compilation target - if: matrix.settings.target == 'x86_64-apple-darwin' - run: rustup target add x86_64-apple-darwin - - - name: Build utoo-pm - run: cargo build --release --target ${{ matrix.settings.target }} --bin utoo -p utoo-pm - - - name: Update PATH (Unix) - if: runner.os != 'Windows' - run: echo "${{ github.workspace }}/target/${{ matrix.settings.target }}/release" >> $GITHUB_PATH - - - name: Update PATH (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\target\${{ matrix.settings.target }}\release" - echo $binPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - Write-Host "Added to PATH: $binPath" - - - name: Create ut symlink for utoo (Unix) - if: runner.os != 'Windows' - run: ln -sf utoo $(dirname $(which utoo))/ut - - - name: Create ut copy for utoo (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\target\${{ matrix.settings.target }}\release" - $utooPath = Join-Path $binPath "utoo.exe" - $utPath = Join-Path $binPath "ut.exe" - - if (Test-Path $utooPath) { - Copy-Item $utooPath $utPath -Force - Write-Host "Created ut.exe at: $utPath" - } else { - Write-Error "utoo.exe not found at: $utooPath" - exit 1 - } - - - name: Verify tools (Unix) - if: runner.os != 'Windows' - run: | - echo "utoo: $(which utoo)" - echo "ut: $(which ut)" - utoo --version - ut --version - - - name: Verify tools (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\target\${{ matrix.settings.target }}\release" - Write-Host "Binary path: $binPath" - Write-Host "utoo.exe: $(Test-Path "$binPath\utoo.exe")" - Write-Host "ut.exe: $(Test-Path "$binPath\ut.exe")" - - $env:PATH = "$binPath;$env:PATH" - - Get-Command utoo | Select-Object -ExpandProperty Source - Get-Command ut | Select-Object -ExpandProperty Source - utoo --version - ut --version - - - name: Make scripts executable - if: runner.os != 'Windows' - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - - name: Run e2e tests (Unix) - if: runner.os != 'Windows' - run: | - if [ "${{ matrix.settings.target }}" = "x86_64-apple-darwin" ]; then - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/utoo-pm.sh - ' - else - bash e2e/utoo-pm.sh - fi - shell: bash - - - name: Run arborist e2e tests (Unix) - if: runner.os != 'Windows' - run: | - if [ "${{ matrix.settings.target }}" = "x86_64-apple-darwin" ]; then - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/pm-arborist.sh - ' - else - bash e2e/pm-arborist.sh - fi - shell: bash - - - name: Run e2e tests (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - git config --system core.longpaths true - - $binPath = "${{ github.workspace }}\target\${{ matrix.settings.target }}\release" - $env:PATH = "$binPath;$env:PATH" - - Write-Host "Running e2e tests..." - Write-Host "Working directory: $(Get-Location)" - - pwsh -File e2e/utoo-pm.ps1 --verbose - - - name: Upload e2e artifacts on failure (Unix) - if: failure() && runner.os != 'Windows' - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-${{ matrix.settings.host }}-${{ matrix.settings.target }} - path: /tmp/utoo-* - if-no-files-found: ignore - - - name: Upload e2e artifacts on failure (Windows) - if: failure() && runner.os == 'Windows' - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-${{ matrix.settings.host }}-${{ matrix.settings.target }} - path: ${{ runner.temp }}/utoo-* - if-no-files-found: ignore - - success: - name: utoopm-e2e-success - runs-on: ubuntu-latest - if: always() - needs: - - build-linux-x64 - - e2e-linux - - e2e - steps: - - name: Check success - run: | - for result in "${{ needs.build-linux-x64.result }}" "${{ needs.e2e-linux.result }}" "${{ needs.e2e.result }}"; do - if [ "$result" == "failure" ] || [ "$result" == "cancelled" ]; then - echo "Tests failed or were cancelled" - exit 1 - fi - done - echo "Tests passed or were skipped" diff --git a/Cargo.lock b/Cargo.lock index 9207794977..46d2141e2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,21 +61,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -588,27 +573,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "browserslist-data" version = "0.1.4" @@ -1146,7 +1110,6 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" dependencies = [ - "brotli", "compression-core", "flate2", "memchr", @@ -4938,21 +4901,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" -[[package]] -name = "manifest-bench" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "futures", - "reqwest 0.12.24", - "rustls", - "rustls-native-certs", - "serde", - "serde_json", - "tokio", -] - [[package]] name = "markdown" version = "1.0.0" @@ -7296,7 +7244,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", diff --git a/Cargo.toml b/Cargo.toml index 769cac33c7..3caf94264e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] resolver = "2" members = [ - "crates/manifest-bench", "crates/pack-api", "crates/pack-cli", "crates/pack-core", diff --git a/crates/manifest-bench/Cargo.toml b/crates/manifest-bench/Cargo.toml deleted file mode 100644 index 0b4e9657de..0000000000 --- a/crates/manifest-bench/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "manifest-bench" -version = "0.0.0" -edition = "2024" -description = "Standalone HTTP-only manifest fetch benchmark, isolating network behaviour from ruborist's resolver pipeline." -license = "MIT" -publish = false - -[[bin]] -name = "manifest-bench" -path = "src/main.rs" - -# tombi: format.rules.table-keys-order.disabled = true -[dependencies] -anyhow = { workspace = true } -clap = { workspace = true } -futures = "0.3" -serde = { version = "1", features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "time"] } -# Identical TLS / DNS choices to ruborist so we measure the *protocol* -# characteristics of the same stack, not a different implementation. -reqwest = { version = "0.12", default-features = false, features = [ - "brotli", - "gzip", - "http2", - "rustls-tls-native-roots-no-provider", - "socks" -] } -rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "logging", "std", "tls12"] } -rustls-native-certs = "0.8" diff --git a/crates/manifest-bench/src/main.rs b/crates/manifest-bench/src/main.rs deleted file mode 100644 index fa70f3fe42..0000000000 --- a/crates/manifest-bench/src/main.rs +++ /dev/null @@ -1,371 +0,0 @@ -//! Standalone HTTP-only manifest fetch benchmark. -//! -//! Isolates the network behaviour of `reqwest + rustls + tokio` from -//! ruborist's resolver pipeline (BFS, dedup, parse, lockfile, project -//! cache). Reads a list of package names, builds manifest URLs, fires -//! parallel `GET` requests, records `(start, end)` per request, and -//! reports the same diag shape as ruborist's `Preload HTTP diag` line. -//! -//! Two input modes: -//! - `--names-file ` — newline-separated package names -//! - `--lockfile ` — a npm-style package-lock.json; we extract -//! the `packages.*` (v3) or `dependencies.*` (v2) keys -//! -//! Two registry modes: -//! - `/` — full manifest endpoint (default, npmjs) -//! - `//latest` — single-version endpoint -//! (gated behind `--single-version`) -//! -//! Each request reads the body to completion (we only measure I/O, no -//! parse). Output: same fields as preload's HTTP diag for direct -//! comparison. - -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Instant; - -use anyhow::{Context, Result, anyhow}; -use clap::Parser; -use futures::stream::{FuturesUnordered, StreamExt}; - -#[derive(Parser, Debug)] -#[command( - name = "manifest-bench", - about = "HTTP-only manifest fetch bench (no parse, no resolver)" -)] -struct Args { - /// Registry base URL. - #[arg(long, default_value = "https://registry.npmjs.org")] - registry: String, - - /// File of newline-separated package names. Mutually exclusive with `--lockfile`. - #[arg(long, conflicts_with = "lockfile")] - names_file: Option, - - /// `package-lock.json` file. Reads top-level `packages.*.name` keys. - #[arg(long)] - lockfile: Option, - - /// Maximum concurrent in-flight requests. - #[arg(long, default_value_t = 128)] - concurrency: usize, - - /// Number of times to repeat the whole sweep (each iteration is a - /// fresh `reqwest::Client`, so connection pool / TLS handshake - /// costs are paid each time, matching `hyperfine` cold-start). - #[arg(long, default_value_t = 1)] - reps: usize, - - /// Use the single-version endpoint `//latest` instead of the - /// full-manifest endpoint `/`. Smaller bodies, more requests - /// served per byte. - #[arg(long)] - single_version: bool, - - /// Override `Accept` header. Default mimics ruborist's preload - /// (`application/vnd.npm.install-v1+json` — abbreviated metadata). - #[arg(long)] - accept: Option, - - /// Override `User-Agent`. Default uses reqwest's default. Try - /// `Bun/1.x.x` to test whether Cloudflare differentiates by UA. - #[arg(long)] - user_agent: Option, - - /// Force HTTP/1.1 (no H2 negotiation). Default lets ALPN decide. - #[arg(long)] - http1_only: bool, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Args::parse(); - - let names = load_names(&args)?; - if names.is_empty() { - return Err(anyhow!("no package names found in input")); - } - - println!( - "manifest-bench: registry={} concurrency={} reps={} names={} h1_only={} single_version={} accept={} ua={}", - args.registry, - args.concurrency, - args.reps, - names.len(), - args.http1_only, - args.single_version, - args.accept.as_deref().unwrap_or(""), - args.user_agent.as_deref().unwrap_or(""), - ); - - for rep in 1..=args.reps { - run_once(&args, &names, rep).await?; - } - - Ok(()) -} - -fn load_names(args: &Args) -> Result> { - if let Some(path) = &args.names_file { - let raw = std::fs::read_to_string(path).with_context(|| format!("read {path:?}"))?; - return Ok(raw - .lines() - .map(str::trim) - .filter(|s| !s.is_empty() && !s.starts_with('#')) - .map(str::to_string) - .collect()); - } - - if let Some(path) = &args.lockfile { - let raw = std::fs::read_to_string(path).with_context(|| format!("read {path:?}"))?; - return extract_lockfile_names(&raw); - } - - Err(anyhow!("provide --names-file or --lockfile")) -} - -/// Pull unique package names from an npm v3 lockfile (`packages.*`) -/// or an older v2 lockfile (`dependencies.*`). -fn extract_lockfile_names(raw: &str) -> Result> { - use std::collections::BTreeSet; - - let v: serde_json::Value = serde_json::from_str(raw).context("parse lockfile JSON")?; - let mut names: BTreeSet = BTreeSet::new(); - - if let Some(packages) = v.get("packages").and_then(|p| p.as_object()) { - for key in packages.keys() { - if key.is_empty() { - continue; - } - // npm v3 packages key like "node_modules/foo" or - // "node_modules/@scope/bar/node_modules/baz" — take the - // last path segment (or @scope/name pair). - let last = last_module_name(key); - if !last.is_empty() { - names.insert(last); - } - } - } else if let Some(deps) = v.get("dependencies").and_then(|d| d.as_object()) { - for key in deps.keys() { - names.insert(key.clone()); - } - } - - Ok(names.into_iter().collect()) -} - -fn last_module_name(key: &str) -> String { - let parts: Vec<&str> = key.split("node_modules/").collect(); - let tail = parts.last().copied().unwrap_or(""); - tail.to_string() -} - -#[derive(Debug)] -struct ReqResult { - start: Instant, - end: Instant, - bytes: usize, - status: u16, -} - -async fn run_once(args: &Args, names: &[String], rep: usize) -> Result<()> { - // Build a fresh client per rep — matches hyperfine's cold-start - // assumption that each iteration pays the TLS handshake cost. - let client = build_client(args)?; - let registry = Arc::new(args.registry.trim_end_matches('/').to_string()); - let accept = Arc::new( - args.accept - .clone() - .unwrap_or_else(|| "application/vnd.npm.install-v1+json".to_string()), - ); - - let single_version = args.single_version; - let concurrency = args.concurrency; - - let phase_start = Instant::now(); - let mut futs = FuturesUnordered::new(); - let mut idx = 0usize; - let mut results: Vec = Vec::with_capacity(names.len()); - - while idx < names.len() && futs.len() < concurrency { - spawn_one( - &client, - ®istry, - &names[idx], - &accept, - single_version, - &mut futs, - ); - idx += 1; - } - - while let Some(res) = futs.next().await { - results.push(res); - if idx < names.len() { - spawn_one( - &client, - ®istry, - &names[idx], - &accept, - single_version, - &mut futs, - ); - idx += 1; - } - } - let phase_wall_ms = phase_start.elapsed().as_millis(); - - report(rep, &results, phase_wall_ms); - Ok(()) -} - -type Fut = std::pin::Pin + Send>>; - -fn spawn_one( - client: &reqwest::Client, - registry: &Arc, - name: &str, - accept: &Arc, - single_version: bool, - futs: &mut FuturesUnordered, -) { - let url = if single_version { - format!("{registry}/{name}/latest") - } else { - format!("{registry}/{name}") - }; - let client = client.clone(); - let accept = Arc::clone(accept); - futs.push(Box::pin(async move { - let start = Instant::now(); - let req = client.get(&url).header("accept", accept.as_str()).send(); - let (bytes, status) = match req.await { - Ok(resp) => { - let status = resp.status().as_u16(); - let body = resp.bytes().await.map(|b| b.len()).unwrap_or(0); - (body, status) - } - Err(_) => (0, 0), - }; - let end = Instant::now(); - ReqResult { - start, - end, - bytes, - status, - } - })); -} - -fn build_client(args: &Args) -> Result { - // Install aws-lc-rs as the default crypto provider (idempotent — - // first call wins). Matches ruborist's `service::http` setup. - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - - let mut roots = rustls::RootCertStore::empty(); - let native = rustls_native_certs::load_native_certs(); - for cert in native.certs { - let _ = roots.add(cert); - } - - let tls_config = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new( - rustls::crypto::aws_lc_rs::default_provider(), - )) - .with_safe_default_protocol_versions() - .map_err(|e| anyhow!("rustls protocol versions: {e}"))? - .with_root_certificates(roots) - .with_no_client_auth(); - - let mut builder = reqwest::Client::builder() - .use_preconfigured_tls(tls_config) - .no_proxy() - .pool_max_idle_per_host(256); - if args.http1_only { - builder = builder.http1_only(); - } - if let Some(ua) = &args.user_agent { - builder = builder.user_agent(ua); - } - builder.build().context("build reqwest client") -} - -fn report(rep: usize, results: &[ReqResult], wall_ms: u128) { - if results.is_empty() { - eprintln!("[rep {rep}] no results"); - return; - } - - let mut spans: Vec<(Instant, Instant)> = results.iter().map(|r| (r.start, r.end)).collect(); - spans.sort_by_key(|(s, _)| *s); - - let first_start = spans.first().unwrap().0; - let last_end = spans.iter().map(|(_, e)| *e).max().unwrap(); - let win_wall = last_end.duration_since(first_start).as_millis(); - - let mut per_us: Vec = spans - .iter() - .map(|(s, e)| e.duration_since(*s).as_micros()) - .collect(); - per_us.sort_unstable(); - let n = per_us.len(); - let pct = |p: usize| per_us[(n * p).div_ceil(100).saturating_sub(1)]; - let sum: u128 = per_us.iter().sum(); - let p50 = per_us[n / 2]; - - let mut busy_us: u128 = 0; - let (mut cur_s, mut cur_e) = spans[0]; - for &(s, e) in &spans[1..] { - if s <= cur_e { - if e > cur_e { - cur_e = e; - } - } else { - busy_us += cur_e.duration_since(cur_s).as_micros(); - cur_s = s; - cur_e = e; - } - } - busy_us += cur_e.duration_since(cur_s).as_micros(); - - let bytes_total: usize = results.iter().map(|r| r.bytes).sum(); - let ok = results.iter().filter(|r| r.status == 200).count(); - let err = results.iter().filter(|r| r.status == 0).count(); - let four_xx = results - .iter() - .filter(|r| (400..500).contains(&r.status)) - .count(); - let five_xx = results - .iter() - .filter(|r| (500..600).contains(&r.status)) - .count(); - - let avg_conc = if busy_us > 0 { - sum as f64 / busy_us as f64 - } else { - 0.0 - }; - - println!( - "[rep {rep}] n={} phase_wall={}ms win_wall={}ms busy={}ms ({:.0}%) sum={}ms avg_conc={:.1} p50={}ms p95={}ms p99={}ms max={}ms bytes={} 200={} 4xx={} 5xx={} err={}", - n, - wall_ms, - win_wall, - busy_us / 1000, - if win_wall > 0 { - 100.0 * (busy_us as f64 / 1000.0) / win_wall as f64 - } else { - 0.0 - }, - sum / 1000, - avg_conc, - p50 / 1000, - pct(95) / 1000, - pct(99) / 1000, - per_us.last().unwrap() / 1000, - bytes_total, - ok, - four_xx, - five_xx, - err, - ); -} From cdb1b51a4f7dd2023608011d8cd89eb5c2a467b3 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 10:32:22 +0800 Subject: [PATCH 09/19] feat(pm-bench-phases): add utoo@npm baseline column + render as markdown tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to make the PR comment more useful: 1. Add `utoo-npm` (latest published utoo from npm) as a third PM column alongside `utoo` (this branch) and `bun`. Lets reviewers see whether a perf change in this PR is a regression vs the published baseline, not just a delta vs bun. Implementation: pm-bench-phases.sh now recognises `utoo-npm` as a distinct PM with its own bench-scoped cache (UTOO_NPM_CACHE) and binary path (UTOO_NPM_BIN). The bench job installs latest utoo from npm into a temp dir BEFORE running the bench so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH; the script invokes it via the absolute UTOO_NPM_BIN path. If UTOO_NPM_BIN is unset (local runs), `utoo-npm` is silently filtered out of PM_LIST. 2. Render the sticky comment as markdown tables instead of fenced code blocks. The fixed-width tables emitted by the bench script's summary are parsed by awk and converted to one markdown table per phase, with PMs as rows and metrics as columns. Renamed `net RX`/`net TX` columns to `netRX`/`netTX` so the header tokenises cleanly. 3. (No code change) Idempotency: the comment is already idempotent — marocchino/sticky-pull-request-comment uses `header: pm-bench-phases-{linux,mac}` so each commit edits the same two comments per platform rather than appending new ones. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scripts/render-bench-phases-comment.sh | 91 +++++++++++++++---- .github/workflows/pm-build.yml | 34 +++++-- e2e/pm-bench-phases.sh | 72 ++++++++++++--- 3 files changed, 163 insertions(+), 34 deletions(-) diff --git a/.github/scripts/render-bench-phases-comment.sh b/.github/scripts/render-bench-phases-comment.sh index 312fbd0d03..9fd8531ab7 100755 --- a/.github/scripts/render-bench-phases-comment.sh +++ b/.github/scripts/render-bench-phases-comment.sh @@ -1,11 +1,26 @@ #!/usr/bin/env bash -# Render the pm-bench-phases sticky PR comment from the per-registry log -# files in /tmp/pm-bench-output/. Called by pm-build.yml's bench-phases-* -# jobs. Output: /tmp/pm-bench-output/pr_comment.md +# Render the pm-bench-phases sticky PR comment from per-registry log files +# in /tmp/pm-bench-output/. Called by pm-build.yml's bench-phases-* jobs. +# +# Output: /tmp/pm-bench-output/pr_comment.md # # Args: # $1 PLATFORM e.g. linux, mac # $2 OS e.g. ubuntu-latest, macos-latest +# +# Each phase block in the bench output looks like: +# +# ## p0_full_cold +# PM wall ±σ user sys RSS pgMinor +# utoo 5.78s 0.12s 2.34s 1.23s 456M 12.3K +# bun 4.32s 0.08s 1.98s 0.87s 234M 8.5K +# PM vCtx iCtx net RX net TX cache node_mod lock +# utoo 45 12 234M 12K 450M 400M 3M +# bun 38 8 220M 11K 220M 400M 2M +# +# We emit one markdown subsection per phase containing two tables (timing +# and resource), one row per PM. Sticky-PR-comment dedup is upstream of +# this script — we just produce the body. set -eu PLATFORM="$1" @@ -20,9 +35,11 @@ mkdir -p /tmp/pm-bench-output echo "" echo "[Workflow run]($RUN_URL) — ant-design" echo "" + echo "_PMs: \`utoo\` (this branch) · \`utoo-npm\` (latest published) · \`bun\` (latest)_" + echo "" } > "$OUT" -# Extract `## p_` sections from a log under a labeled heading. +# Convert one log file's fixed-width tables into markdown. render_log() { local label="$1" local file="$2" @@ -31,27 +48,69 @@ render_log() { echo "" return fi + echo "### ${label}" echo "" + + # Strip ANSI colour codes so awk sees clean text. local cleaned cleaned=$(mktemp) sed -E 's/\x1B\[[0-9;]*[a-zA-Z]//g' "$file" > "$cleaned" + awk ' - /^## p[0-9]+_[a-z0-9_]+/ { - if (in_section) print "```\n"; - in_section = 1; - print "```"; - print $0; - next; + function flush_table() { + if (n_rows == 0) return + # markdown header + printf "| " + for (i = 1; i <= n_cols; i++) printf "%s | ", header[i] + printf "\n|" + for (i = 1; i <= n_cols; i++) printf "---|" + printf "\n" + # data rows + for (r = 1; r <= n_rows; r++) { + printf "| " + for (i = 1; i <= n_cols; i++) printf "%s | ", rows[r, i] + printf "\n" + } + printf "\n" + n_rows = 0 + n_cols = 0 + } + + /^## p[0-9]+_/ { + flush_table() + # phase heading (drop the leading `## `) + sub(/^## /, "", $0) + printf "#### %s\n\n", $0 + in_phase = 1 + next } - /^## / && in_section { - print "```\n"; - in_section = 0; + + in_phase && /^PM[ \t]+/ { + # New table header — flush any preceding one. + flush_table() + n_cols = NF + for (i = 1; i <= NF; i++) header[i] = $i + next } - in_section { print } - END { if (in_section) print "```" } + + # Data rows: indented or pm-name-leading lines while a header is open. + in_phase && n_cols > 0 && NF == n_cols && $1 !~ /^(PM|##)$/ { + n_rows++ + for (i = 1; i <= NF; i++) rows[n_rows, i] = $i + next + } + + # End-of-phase marker: another `##` heading or blank-then-banner. + in_phase && /^##/ && !/^## p[0-9]+_/ { + flush_table() + in_phase = 0 + next + } + + END { flush_table() } ' "$cleaned" - echo "" + rm -f "$cleaned" } diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index 11d1720868..151a61ba94 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -501,17 +501,29 @@ jobs: bun-version: latest - name: Install bench tools run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Install utoo@npm baseline + # Install latest published utoo to a temp dir (NOT the global prefix) + # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench + # script invokes it via $UTOO_NPM_BIN absolute path. + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/.bin/utoo" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - name: Verify tools run: | hyperfine --version utoo --version + "$UTOO_NPM_BIN" --version bun --version - name: Define cache wipe function run: | cat > /tmp/wipe-bench-state.sh <<'WIPE' #!/usr/bin/env bash rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/bun-bench-cache \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true mkdir -p /tmp/pm-bench-output WIPE @@ -523,7 +535,7 @@ jobs: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} REGISTRY: 'https://registry.npmjs.org' BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,bun' + PM_LIST: 'utoo,utoo-npm,bun' run: | chmod +x e2e/pm-bench-phases.sh bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log @@ -539,7 +551,7 @@ jobs: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} REGISTRY: 'https://registry.npmmirror.com' BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,bun' + PM_LIST: 'utoo,utoo-npm,bun' run: | mkdir -p /tmp/pm-bench-output bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log @@ -620,17 +632,27 @@ jobs: mkdir -p "$HOME/bin" ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" echo "$HOME/bin" >> $GITHUB_PATH + - name: Install utoo@npm baseline + # See linux job — same rationale. + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/.bin/utoo" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - name: Verify tools run: | hyperfine --version utoo --version + "$UTOO_NPM_BIN" --version bun --version - name: Define cache wipe function run: | cat > /tmp/wipe-bench-state.sh <<'WIPE' #!/usr/bin/env bash rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/bun-bench-cache \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true mkdir -p /tmp/pm-bench-output WIPE @@ -642,7 +664,7 @@ jobs: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} REGISTRY: 'https://registry.npmjs.org' BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,bun' + PM_LIST: 'utoo,utoo-npm,bun' run: | chmod +x e2e/pm-bench-phases.sh bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log @@ -658,7 +680,7 @@ jobs: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} REGISTRY: 'https://registry.npmmirror.com' BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,bun' + PM_LIST: 'utoo,utoo-npm,bun' run: | mkdir -p /tmp/pm-bench-output bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log diff --git a/e2e/pm-bench-phases.sh b/e2e/pm-bench-phases.sh index 3f39b7c58a..4ca4cc4780 100755 --- a/e2e/pm-bench-phases.sh +++ b/e2e/pm-bench-phases.sh @@ -17,9 +17,25 @@ mkdir -p "$BENCH_DIR" "$RESULTS_DIR" # depend on what each PM considers the default (which varies by HOME, OS # image, and previously-set user config). UTOO_CACHE="${UTOO_CACHE:-/tmp/utoo-bench-cache}" +UTOO_NPM_CACHE="${UTOO_NPM_CACHE:-/tmp/utoo-npm-bench-cache}" BUN_CACHE="${BUN_CACHE:-/tmp/bun-bench-cache}" export BUN_INSTALL_CACHE_DIR="$BUN_CACHE" +# Drop `utoo-npm` from the PM list when no published-baseline binary is +# wired up — UTOO_NPM_BIN is set by CI's "Install utoo@npm" step. Local +# runs without it just skip the comparison instead of erroring. +if [ -z "${UTOO_NPM_BIN:-}" ]; then + FILTERED=() + for pm in "${PACKAGE_MANAGERS[@]}"; do + if [ "$pm" = "utoo-npm" ]; then + echo "skip utoo-npm: UTOO_NPM_BIN not set" >&2 + continue + fi + FILTERED+=("$pm") + done + PACKAGE_MANAGERS=("${FILTERED[@]}") +fi + GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' @@ -105,7 +121,11 @@ du_bytes() { capture_footprint() { local phase=$1 pm=$2 out=$3 local cache - case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + case "$pm" in + utoo) cache=$UTOO_CACHE ;; + utoo-npm) cache=$UTOO_NPM_CACHE ;; + bun) cache=$BUN_CACHE ;; + esac printf '{"cache":%d,"node_modules":%d,"lockfile":%d}\n' \ "$(du_bytes "$cache")" \ "$(du_bytes "$PROJECT_DIR/node_modules")" \ @@ -127,7 +147,11 @@ PROJECT_DIR="$BENCH_DIR/$PROJECT" write_prepare() { local path=$1 phase=$2 pm=$3 local cache - case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + case "$pm" in + utoo) cache=$UTOO_CACHE ;; + utoo-npm) cache=$UTOO_NPM_CACHE ;; + bun) cache=$BUN_CACHE ;; + esac cat > "$path" <> "$path" <> "$path" <> "$path" <> "$path" <> "$path" <> "$path" <> "$path" < "$RESULTS_DIR/seed_${phase}_${pm}.log" 2>&1 fi ;; + p3_*:utoo-npm|p4_*:utoo-npm) + if [ ! -f package-lock.json ]; then + echo -e " ${CYAN}seed: running \`utoo-npm deps\` to generate package-lock.json${NC}" + "$UTOO_NPM_BIN" deps --registry="$REGISTRY" --cache-dir="$UTOO_NPM_CACHE" > "$RESULTS_DIR/seed_${phase}_${pm}.log" 2>&1 + fi + ;; p3_*:bun|p4_*:bun) if [ ! -f bun.lock ]; then echo -e " ${CYAN}seed: running \`bun install --lockfile-only\` to generate bun.lock${NC}" @@ -222,13 +263,18 @@ seed_for_phase() { # Phase 4 also needs a pre-warmed cache. if [[ "$phase" == p4_* ]]; then local cache - case "$pm" in utoo) cache=$UTOO_CACHE ;; bun) cache=$BUN_CACHE ;; esac + case "$pm" in + utoo) cache=$UTOO_CACHE ;; + utoo-npm) cache=$UTOO_NPM_CACHE ;; + bun) cache=$BUN_CACHE ;; + esac if [ ! -d "$cache" ] || [ -z "$(ls -A "$cache" 2>/dev/null)" ]; then echo -e " ${CYAN}seed: warming $pm cache via full install${NC}" rm -rf node_modules case "$pm" in - utoo) utoo install --ignore-scripts --registry="$REGISTRY" --cache-dir="$UTOO_CACHE" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; - bun) bun install --ignore-scripts --registry="$REGISTRY" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; + utoo) utoo install --ignore-scripts --registry="$REGISTRY" --cache-dir="$UTOO_CACHE" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; + utoo-npm) "$UTOO_NPM_BIN" install --ignore-scripts --registry="$REGISTRY" --cache-dir="$UTOO_NPM_CACHE" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; + bun) bun install --ignore-scripts --registry="$REGISTRY" > "$RESULTS_DIR/seed_warmup_${pm}.log" 2>&1 ;; esac rm -rf node_modules fi @@ -237,15 +283,17 @@ seed_for_phase() { install_cmd() { case "$1" in - utoo) echo "utoo install --ignore-scripts --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; - bun) echo "bun install --ignore-scripts --registry=$REGISTRY" ;; + utoo) echo "utoo install --ignore-scripts --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; + utoo-npm) echo "$UTOO_NPM_BIN install --ignore-scripts --registry=$REGISTRY --cache-dir=$UTOO_NPM_CACHE" ;; + bun) echo "bun install --ignore-scripts --registry=$REGISTRY" ;; esac } resolve_cmd() { case "$1" in - utoo) echo "utoo deps --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; - bun) echo "bun install --lockfile-only --registry=$REGISTRY" ;; + utoo) echo "utoo deps --registry=$REGISTRY --cache-dir=$UTOO_CACHE" ;; + utoo-npm) echo "$UTOO_NPM_BIN deps --registry=$REGISTRY --cache-dir=$UTOO_NPM_CACHE" ;; + bun) echo "bun install --lockfile-only --registry=$REGISTRY" ;; esac } @@ -392,7 +440,7 @@ RESULTS_DIR="$RESULTS_DIR" node -e " } // Table B: context switches + network + final on-disk footprint - console.log(pad('PM', 6) + ' ' + padR('vCtx', 8) + ' ' + padR('iCtx', 8) + ' ' + padR('net RX', 8) + ' ' + padR('net TX', 8) + ' ' + padR('cache', 8) + ' ' + padR('node_mod', 9) + ' ' + padR('lock', 7)); + console.log(pad('PM', 6) + ' ' + padR('vCtx', 8) + ' ' + padR('iCtx', 8) + ' ' + padR('netRX', 8) + ' ' + padR('netTX', 8) + ' ' + padR('cache', 8) + ' ' + padR('node_mod', 9) + ' ' + padR('lock', 7)); for (const pm of pms) { const m = mp[pm] || {}; console.log( From 3eb533cb4badc2575af912d6375b090e199d4b4a Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 10:46:50 +0800 Subject: [PATCH 10/19] fix(pm-build): point UTOO_NPM_BIN at node_modules/utoo/bin/utoo npm install for the utoo wrapper package didn't populate node_modules/.bin/utoo on the runner (npm's local-install bin-link behavior depends on version/flags). The wrapper's package.json declares `bin: { utoo: 'bin/utoo' }`, so point at the actual file under the package directory instead of relying on the symlink. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index 151a61ba94..c6a8783a1a 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -504,12 +504,15 @@ jobs: - name: Install utoo@npm baseline # Install latest published utoo to a temp dir (NOT the global prefix) # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench - # script invokes it via $UTOO_NPM_BIN absolute path. + # script invokes it via $UTOO_NPM_BIN absolute path. Points directly + # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's + # node_modules/.bin/ symlink isn't always created for local installs. run: | mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg npm init -y >/dev/null npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/.bin/utoo" + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" "$UTOO_NPM_BIN" --version echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - name: Verify tools From b0a12f8e284e68d465b1197b4b37bab992a35f7e Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 11:04:36 +0800 Subject: [PATCH 11/19] fix(pm-build): apply UTOO_NPM_BIN path fix to mac job too Earlier replace_all only matched the linux block (the comments diverged between linux and mac). Mac was still pointing at the non-existent node_modules/.bin/utoo symlink. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index c6a8783a1a..d4d25ac078 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -641,7 +641,8 @@ jobs: mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg npm init -y >/dev/null npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/.bin/utoo" + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" "$UTOO_NPM_BIN" --version echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - name: Verify tools From 993261d8f498122130ea06164f5353b74e49776c Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 22:04:38 +0800 Subject: [PATCH 12/19] refactor(bench): move bench scripts out of e2e/ into bench/ The pm-bench, pm-bench-phases and pm-bench-pcap scripts are perf benchmarks, not e2e tests. Group them under bench/ so the directory intent is obvious. Update pm-build.yml's path references. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-build.yml | 26 +++++++++++++------------- {e2e => bench}/pm-bench-pcap.sh | 0 {e2e => bench}/pm-bench-phases.sh | 0 {e2e => bench}/pm-bench.sh | 0 4 files changed, 13 insertions(+), 13 deletions(-) rename {e2e => bench}/pm-bench-pcap.sh (100%) rename {e2e => bench}/pm-bench-phases.sh (100%) rename {e2e => bench}/pm-bench.sh (100%) diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index d4d25ac078..97e309a44a 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -540,8 +540,8 @@ jobs: BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} PM_LIST: 'utoo,utoo-npm,bun' run: | - chmod +x e2e/pm-bench-phases.sh - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - name: Stash npmjs logs and wipe state before npmmirror run: | mkdir -p /tmp/bench-logs-stash @@ -557,7 +557,7 @@ jobs: PM_LIST: 'utoo,utoo-npm,bun' run: | mkdir -p /tmp/pm-bench-output - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - name: Upload bench logs if: always() uses: actions/upload-artifact@v4 @@ -627,7 +627,7 @@ jobs: with: bun-version: latest - name: Install bench tools - # macOS /usr/bin/time isn't GNU; e2e/pm-bench-phases.sh uses + # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin # symlink earlier on PATH. run: | @@ -670,8 +670,8 @@ jobs: BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} PM_LIST: 'utoo,utoo-npm,bun' run: | - chmod +x e2e/pm-bench-phases.sh - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - name: Stash npmjs logs and wipe state before npmmirror run: | mkdir -p /tmp/bench-logs-stash @@ -687,7 +687,7 @@ jobs: PM_LIST: 'utoo,utoo-npm,bun' run: | mkdir -p /tmp/pm-bench-output - bash e2e/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - name: Upload bench logs if: always() uses: actions/upload-artifact@v4 @@ -813,10 +813,10 @@ jobs: done - name: Run benchmark run: | - chmod +x e2e/pm-bench.sh + chmod +x bench/pm-bench.sh BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} pm-bench-mac: needs: [build-mac-arm64] @@ -895,10 +895,10 @@ jobs: done - name: Run benchmark run: | - chmod +x e2e/pm-bench.sh + chmod +x bench/pm-bench.sh BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} # ============================================================ # PM-BENCH-PCAP (manual perf-debug only) @@ -936,8 +936,8 @@ jobs: PROJECT: ${{ github.event.inputs.project || 'ant-design' }} REGISTRY: 'https://registry.npmjs.org' run: | - chmod +x e2e/pm-bench-pcap.sh - bash e2e/pm-bench-pcap.sh + chmod +x bench/pm-bench-pcap.sh + bash bench/pm-bench-pcap.sh - name: Upload pcap artifact if: always() uses: actions/upload-artifact@v4 diff --git a/e2e/pm-bench-pcap.sh b/bench/pm-bench-pcap.sh similarity index 100% rename from e2e/pm-bench-pcap.sh rename to bench/pm-bench-pcap.sh diff --git a/e2e/pm-bench-phases.sh b/bench/pm-bench-phases.sh similarity index 100% rename from e2e/pm-bench-phases.sh rename to bench/pm-bench-phases.sh diff --git a/e2e/pm-bench.sh b/bench/pm-bench.sh similarity index 100% rename from e2e/pm-bench.sh rename to bench/pm-bench.sh From 70f8df5dfecfc9556f5180bef03d51681cb29388 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 22:24:25 +0800 Subject: [PATCH 13/19] ci(pm): split pm-build into build-only + per-consumer workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure: pm-build.yml is now build-only — builds utoo for the four targets and uploads each as an artifact. Each downstream concern lives in its own workflow file: - pm-e2e.yml workflow_run after pm-build, runs e2e per target - pm-bench-phases.yml workflow_run after pm-build, posts sticky PR comment - pm-bench.yml workflow_dispatch + label trigger, builds inline - pm-bench-pcap.yml workflow_dispatch only, builds inline pm-e2e and pm-bench-phases reuse pm-build's artifact via cross-workflow download (`actions/download-artifact@v4` with `run-id` from the workflow_run event). Each per-target/per-platform job first checks `gh api .../actions/runs/.../artifacts` for the matching artifact — if pm-build skipped that target (e.g. mac-x64 + windows are gated on `(pm)` strict only), the downstream job no-ops cleanly. The `utoopm-e2e-success` aggregator + per-target check names are preserved verbatim, so any branch protection rules continue to match. Note: workflow_run-triggered workflows use the file on the default branch. After this branch merges to `next`, pm-e2e/pm-bench-phases fire on every pm-build completion. Until then, the OLD pm-e2e.yml on `next` remains the canonical e2e check. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-bench-pcap.yml | 88 +++ .github/workflows/pm-bench-phases.yml | 324 +++++++++++ .github/workflows/pm-bench.yml | 278 +++++++++ .github/workflows/pm-build.yml | 804 +------------------------- .github/workflows/pm-e2e.yml | 324 +++++++++++ 5 files changed, 1042 insertions(+), 776 deletions(-) create mode 100644 .github/workflows/pm-bench-pcap.yml create mode 100644 .github/workflows/pm-bench-phases.yml create mode 100644 .github/workflows/pm-bench.yml create mode 100644 .github/workflows/pm-e2e.yml diff --git a/.github/workflows/pm-bench-pcap.yml b/.github/workflows/pm-bench-pcap.yml new file mode 100644 index 0000000000..b840f02f38 --- /dev/null +++ b/.github/workflows/pm-bench-pcap.yml @@ -0,0 +1,88 @@ +name: utoopm-bench-pcap + +# Manual perf-debug only: captures tcpdump of the resolve phase per PM +# (linux only — sudo + raw sockets aren't available on macos GH runners). +# Self-contained — builds utoo inline so the workflow doesn't depend on +# pm-build's state. +on: + workflow_dispatch: + inputs: + project: + description: 'Project to bench (under github.com/ant-design/)' + required: false + default: 'ant-design' + type: string + +permissions: + contents: read + +jobs: + build-linux: + runs-on: ubuntu-latest + container: + image: ubuntu:20.04 + steps: + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git + - uses: actions/checkout@v4 + - name: Configure git safe directory + run: git config --global --add safe.directory /__w/utoo/utoo + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-bench-pcap-linux + - name: Build utoo + run: cargo build --release --bin utoo -p utoo-pm + - name: Upload utoo binary + uses: actions/upload-artifact@v4 + with: + name: utoo-pcap-linux + path: target/release/utoo + retention-days: 1 + + pcap-linux: + needs: [build-linux] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-pcap-linux + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install pcap tools + run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time + - name: Capture pcap + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + run: | + chmod +x bench/pm-bench-pcap.sh + bash bench/pm-bench-pcap.sh + - name: Upload pcap artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-pcap + path: /tmp/pm-bench-pcap + retention-days: 7 diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml new file mode 100644 index 0000000000..91e2838803 --- /dev/null +++ b/.github/workflows/pm-bench-phases.yml @@ -0,0 +1,324 @@ +name: utoopm-bench-phases + +# Phase-isolated benchmark. Triggered by `utoopm-build` completing — +# downloads each platform's `utoo` artifact, runs the bench across npmjs +# + npmmirror, and posts a sticky PR comment per platform with the +# results. Includes utoo@npm (latest published) as a baseline column +# alongside utoo (this branch) and bun (latest). +on: + workflow_run: + workflows: ["utoopm-build"] + types: [completed] + +permissions: + contents: read + actions: read + pull-requests: write + +env: + UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} + UPSTREAM_REPO: ${{ github.repository }} + +jobs: + bench-phases-linux: + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Check artifact exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-linux-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping linux bench-phases." + fi + - name: Setup Node.js + if: steps.check.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + if: steps.check.outputs.skip != 'true' + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + if: steps.check.outputs.skip != 'true' + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Install utoo@npm baseline + # Install latest published utoo to a temp dir (NOT the global prefix) + # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench + # script invokes it via $UTOO_NPM_BIN absolute path. Points directly + # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's + # node_modules/.bin/ symlink isn't always created for local installs. + if: steps.check.outputs.skip != 'true' + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Verify tools + if: steps.check.outputs.skip != 'true' + run: | + hyperfine --version + utoo --version + "$UTOO_NPM_BIN" --version + bun --version + - name: Define cache wipe function + if: steps.check.outputs.skip != 'true' + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + if: steps.check.outputs.skip != 'true' + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + if: steps.check.outputs.skip != 'true' + env: + PROJECT: 'ant-design' + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: '3' + PM_LIST: 'utoo,utoo-npm,bun' + run: | + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + if: steps.check.outputs.skip != 'true' + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + if: steps.check.outputs.skip != 'true' + env: + PROJECT: 'ant-design' + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: '3' + PM_LIST: 'utoo,utoo-npm,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() && steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-linux + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() && steps.check.outputs.skip != 'true' + env: + GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} + run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest + - name: Find Current PR + id: findPr + if: always() && steps.check.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + # Try the upstream workflow_run's pull_requests payload first + # (only populated for same-repo PRs). Fall back to PR list lookup. + PR_NUMBER="" + PRS_JSON='${{ toJson(github.event.workflow_run.pull_requests) }}' + PR_NUMBER=$(echo "$PRS_JSON" | jq -r '.[0].number // empty' 2>/dev/null || true) + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --state open --json number --jq '.[0].number' || true) + fi + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number' || true) + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.check.outputs.skip != 'true' && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-linux + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + bench-phases-mac: + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Check artifact exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-mac-arm64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-mac-arm64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac bench-phases." + fi + - name: Setup Node.js + if: steps.check.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + if: steps.check.outputs.skip != 'true' + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses + # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin + # symlink earlier on PATH. + if: steps.check.outputs.skip != 'true' + run: | + brew install hyperfine gnu-time + mkdir -p "$HOME/bin" + ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" + echo "$HOME/bin" >> $GITHUB_PATH + - name: Install utoo@npm baseline + # See linux job — same rationale. + if: steps.check.outputs.skip != 'true' + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Verify tools + if: steps.check.outputs.skip != 'true' + run: | + hyperfine --version + utoo --version + "$UTOO_NPM_BIN" --version + bun --version + - name: Define cache wipe function + if: steps.check.outputs.skip != 'true' + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + if: steps.check.outputs.skip != 'true' + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + if: steps.check.outputs.skip != 'true' + env: + PROJECT: 'ant-design' + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: '3' + PM_LIST: 'utoo,utoo-npm,bun' + run: | + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + if: steps.check.outputs.skip != 'true' + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + if: steps.check.outputs.skip != 'true' + env: + PROJECT: 'ant-design' + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: '3' + PM_LIST: 'utoo,utoo-npm,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() && steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-mac + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() && steps.check.outputs.skip != 'true' + env: + GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} + run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest + - name: Find Current PR + id: findPr + if: always() && steps.check.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + PRS_JSON='${{ toJson(github.event.workflow_run.pull_requests) }}' + PR_NUMBER=$(echo "$PRS_JSON" | jq -r '.[0].number // empty' 2>/dev/null || true) + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --state open --json number --jq '.[0].number' || true) + fi + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number' || true) + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.check.outputs.skip != 'true' && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-mac + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pm-bench.yml b/.github/workflows/pm-bench.yml new file mode 100644 index 0000000000..fec5615459 --- /dev/null +++ b/.github/workflows/pm-bench.yml @@ -0,0 +1,278 @@ +name: utoopm-bench + +# Manual perf comparison: utoo (current branch) vs published utoo from +# npm vs other PMs (pnpm/yarn/bun/aube) on cold + warm installs across +# configurable registries. +# +# Self-contained — builds utoo inline rather than reusing pm-build's +# artifact, so the workflow Just Works regardless of pm-build's state. +# Triggered manually via workflow_dispatch (or by labelling a PR with +# `benchmark`). +on: + workflow_dispatch: + inputs: + registry: + description: 'Registry to test' + required: true + default: 'both' + type: choice + options: [both, npmjs, npmmirror] + pm_list: + description: 'Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' + required: true + default: 'utoo,bun' + type: string + baseline: + description: 'Compare with published utoo from npm' + required: false + default: true + type: boolean + build_next: + description: 'Also compare with utoo built from `next` branch' + required: false + default: false + type: boolean + cold_runs: + description: 'Number of cold install runs' + required: false + default: '3' + type: string + warm_runs: + description: 'Number of warm install runs' + required: false + default: '3' + type: string + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build-linux: + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: ubuntu-latest + container: + image: ubuntu:20.04 + steps: + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git + - uses: actions/checkout@v4 + - name: Configure git safe directory + run: git config --global --add safe.directory /__w/utoo/utoo + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-bench-linux + - name: Build current branch utoo + run: cargo build --release --bin utoo -p utoo-pm + - name: Upload current binary + uses: actions/upload-artifact@v4 + with: + name: utoo-bench-current-linux + path: target/release/utoo + retention-days: 1 + - name: Build next branch utoo + if: github.event.inputs.build_next == 'true' + run: | + git fetch origin next --depth=1 + git checkout origin/next + git submodule update --init --recursive --depth 1 + cargo build --release --bin utoo -p utoo-pm + - name: Upload next binary + if: github.event.inputs.build_next == 'true' + uses: actions/upload-artifact@v4 + with: + name: utoo-bench-next-linux + path: target/release/utoo + retention-days: 1 + + bench-linux: + needs: [build-linux] + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download current binary + uses: actions/download-artifact@v4 + with: + name: utoo-bench-current-linux + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Download next binary + if: github.event.inputs.build_next == 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-bench-next-linux + path: dist-next + - name: Install next binary + if: github.event.inputs.build_next == 'true' + run: | + chmod +x dist-next/utoo + cp dist-next/utoo /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine and GNU time + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x bench/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + + bench-mac: + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-04-02 + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + with: + shared-key: pm-bench-macos-latest + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Build baseline utoo from next branch + if: github.event.inputs.build_next == 'true' + run: | + CURRENT_SHA=$(git rev-parse HEAD) + git fetch origin next --depth=1 + git checkout origin/next + git submodule update --init --recursive --depth 1 + cargo build --release --bin utoo -p utoo-pm + cp target/release/utoo /tmp/utoo-next + chmod +x /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + git checkout "$CURRENT_SHA" + git submodule update --init --recursive --depth 1 + - name: Build utoo-pm + run: cargo build --release --bin utoo -p utoo-pm + - name: Update PATH + run: echo "${{ github.workspace }}/target/release" >> $GITHUB_PATH + - name: Create ut symlink + run: ln -sf utoo $(dirname $(which utoo))/ut + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine + run: brew install hyperfine + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x bench/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index 97e309a44a..465c610dc1 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -1,78 +1,22 @@ name: utoopm-build -# Single workflow that builds utoo once per target, then fans out to -# every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Replaces -# the old pm-e2e.yml + pm-bench.yml + pm-bench-phases.yml trio so each -# target builds exactly once per commit and downstream jobs reuse the -# uploaded artifact. +# Build-only workflow. Builds `utoo` once per target and uploads each as +# an artifact. Downstream workflows (pm-e2e, pm-bench-phases, pm-bench, +# pm-bench-pcap) trigger on this workflow's completion via `workflow_run` +# and download the artifacts they need — so each target's +# `cargo build --release` runs at most once per commit. # -# Triggers (event → consumers): -# pull_request / push with `(pm)` in title/msg → e2e jobs -# pull_request / push with `(pm` (loose) in title/msg → bench-phases -# pull_request labeled `benchmark` → pm-bench (manual perf compare) -# workflow_dispatch with `target` choice → manual one-off runs -# -# Build job gates are the union of their downstream consumers' gates. -# Each downstream job is pure download-and-run, no `cargo build`. +# Triggers: +# push to next/e2e-pm — only when commit msg contains `(pm` +# pull_request — only when title contains `(pm`, OR labeled `benchmark` +# workflow_dispatch — always builds linux + mac arm64 (skips mac x64 + +# windows since they only feed e2e and dispatch is for bench/pcap) on: push: branches: ["next", "e2e-pm"] pull_request: types: [opened, synchronize, reopened, edited, labeled] workflow_dispatch: - inputs: - target: - description: 'Manual target to run' - required: true - type: choice - options: - - pm-bench - - pm-bench-phases - - pm-bench-pcap - default: pm-bench - # ----- pm-bench inputs ----- - registry: - description: '[pm-bench] Registry to test' - required: false - default: 'both' - type: choice - options: [both, npmjs, npmmirror] - pm_list: - description: '[pm-bench] Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' - required: false - default: 'utoo,bun' - type: string - baseline: - description: '[pm-bench] Compare with published utoo from npm' - required: false - default: true - type: boolean - build_next: - description: '[pm-bench] Also compare with utoo built from `next` branch' - required: false - default: false - type: boolean - cold_runs: - description: '[pm-bench] Number of cold install runs' - required: false - default: '3' - type: string - warm_runs: - description: '[pm-bench] Number of warm install runs' - required: false - default: '3' - type: string - # ----- pm-bench-phases / pcap inputs ----- - project: - description: '[phases/pcap] Project to bench (under github.com/ant-design/)' - required: false - default: 'ant-design' - type: string - bench_runs: - description: '[phases] Runs per phase' - required: false - default: '3' - type: string concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -83,15 +27,13 @@ permissions: jobs: # ============================================================ - # BUILD JOBS - # One per target, runs whenever any downstream consumer needs it. - # Downstream jobs `needs:` the relevant build job and download - # the uploaded artifact. No downstream rebuilds. + # Per-target build jobs. Gates are unions of every downstream + # consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Linux + + # mac-arm64 have the broadest set of consumers; mac-x64 and + # windows only feed e2e (strict `(pm)` scope). # ============================================================ build-linux: - # Linux serves: e2e-linux, bench-phases-linux, pm-bench-linux, pm-bench-pcap-linux. - # Gate is the union of all linux consumers' gates. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || @@ -143,12 +85,11 @@ jobs: retention-days: 1 build-mac-arm64: - # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-mac. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || - (github.event_name == 'workflow_dispatch' && inputs.target != 'pm-bench-pcap') + github.event_name == 'workflow_dispatch' runs-on: macos-latest steps: - uses: actions/checkout@v4 @@ -173,8 +114,8 @@ jobs: retention-days: 1 build-mac-x64: - # Mac x86_64 serves: e2e-mac-x64 only. Cross-compiled on macos-latest - # (arm64 host) via `rustup target add x86_64-apple-darwin`. + # Only feeds e2e-mac-x64 — strict `(pm)` scope, no bench consumers. + # Cross-compiled on macos-latest (arm64 host). if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) @@ -204,7 +145,7 @@ jobs: retention-days: 1 build-windows: - # Windows serves: e2e-windows only. + # Only feeds e2e-windows — strict `(pm)` scope. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) @@ -234,714 +175,25 @@ jobs: path: target/x86_64-pc-windows-msvc/release/utoo.exe retention-days: 1 - # ============================================================ - # E2E JOBS - # Auto-trigger on `(pm)` (strict) PR title / commit msg. - # ============================================================ - - e2e-linux: - needs: [build-linux] - name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - run: | - utoo --version - ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - run: bash e2e/pm-arborist.sh - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-mac-arm64: - needs: [build-mac-arm64] - name: utoopm-e2e-macos-latest-aarch64-apple-darwin - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - run: | - utoo --version - ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - run: bash e2e/pm-arborist.sh - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-macos-latest-aarch64-apple-darwin - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-mac-x64: - needs: [build-mac-x64] - name: utoopm-e2e-macos-latest-x86_64-apple-darwin - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Install x86_64 Node.js via nvm - run: | - arch -x86_64 bash -c ' - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 22 - nvm use 22 - nvm alias default 22 - echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV - NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) - echo "$NODE_PATH/bin" >> $GITHUB_PATH - node -p process.arch - npm -v - ' - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-mac-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - run: | - utoo --version - ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - shell: bash - run: | - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/utoo-pm.sh - ' - - name: Run arborist e2e tests - shell: bash - run: | - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/pm-arborist.sh - ' - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-macos-latest-x86_64-apple-darwin - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-windows: - needs: [build-windows] - name: utoopm-e2e-windows-latest-x86_64-pc-windows-msvc - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - name: Disable Windows Defender - shell: powershell - run: Set-MpPreference -DisableRealtimeMonitoring $true - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-windows-x64 - path: dist - - name: Install utoo - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\dist" - # ut.exe is just a copy of utoo.exe (Windows has no symlinks). - Copy-Item "$binPath\utoo.exe" "$binPath\ut.exe" -Force - echo "$binPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - name: Verify tools - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\dist" - $env:PATH = "$binPath;$env:PATH" - Get-Command utoo | Select-Object -ExpandProperty Source - Get-Command ut | Select-Object -ExpandProperty Source - utoo --version - ut --version - - name: Run e2e tests - shell: pwsh - run: | - git config --system core.longpaths true - $binPath = "${{ github.workspace }}\dist" - $env:PATH = "$binPath;$env:PATH" - pwsh -File e2e/utoo-pm.ps1 --verbose - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-windows-latest-x86_64-pc-windows-msvc - path: ${{ runner.temp }}/utoo-* - if-no-files-found: ignore - - e2e-success: - name: utoopm-e2e-success + # Always-runs aggregator so this workflow reports a definite check + # even on PRs where every build was gated out. Downstream workflows + # use `workflow_run` and react to this run's conclusion. + build-success: + name: utoopm-build-success runs-on: ubuntu-latest if: always() - needs: [build-linux, build-mac-arm64, build-mac-x64, build-windows, e2e-linux, e2e-mac-arm64, e2e-mac-x64, e2e-windows] + needs: [build-linux, build-mac-arm64, build-mac-x64, build-windows] steps: - - name: Check success + - name: Check builds run: | for result in \ "${{ needs.build-linux.result }}" \ "${{ needs.build-mac-arm64.result }}" \ "${{ needs.build-mac-x64.result }}" \ - "${{ needs.build-windows.result }}" \ - "${{ needs.e2e-linux.result }}" \ - "${{ needs.e2e-mac-arm64.result }}" \ - "${{ needs.e2e-mac-x64.result }}" \ - "${{ needs.e2e-windows.result }}"; do + "${{ needs.build-windows.result }}"; do if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then - echo "Tests failed or were cancelled" + echo "Build failed or was cancelled" exit 1 fi done - echo "Tests passed or were skipped" - - # ============================================================ - # BENCH-PHASES JOBS - # Auto-trigger on `(pm` (loose) — covers `(pm)`, `(pm-bench-phases)`, - # `(pm-foo)`, etc. Posts a sticky PR comment per platform with the - # phase-isolated bench results across npmjs + npmmirror. - # ============================================================ - - bench-phases-linux: - needs: [build-linux] - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install bench tools - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - name: Install utoo@npm baseline - # Install latest published utoo to a temp dir (NOT the global prefix) - # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench - # script invokes it via $UTOO_NPM_BIN absolute path. Points directly - # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's - # node_modules/.bin/ symlink isn't always created for local installs. - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Verify tools - run: | - hyperfine --version - utoo --version - "$UTOO_NPM_BIN" --version - bun --version - - name: Define cache wipe function - run: | - cat > /tmp/wipe-bench-state.sh <<'WIPE' - #!/usr/bin/env bash - rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ - ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true - mkdir -p /tmp/pm-bench-output - WIPE - chmod +x /tmp/wipe-bench-state.sh - - name: Wipe bench state before npmjs - run: bash /tmp/wipe-bench-state.sh - - name: Run phase-isolated benchmark · npmjs - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,utoo-npm,bun' - run: | - chmod +x bench/pm-bench-phases.sh - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - name: Stash npmjs logs and wipe state before npmmirror - run: | - mkdir -p /tmp/bench-logs-stash - cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true - bash /tmp/wipe-bench-state.sh - mkdir -p /tmp/pm-bench-output - cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - - name: Run phase-isolated benchmark · npmmirror - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmmirror.com' - BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,utoo-npm,bun' - run: | - mkdir -p /tmp/pm-bench-output - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - - name: Upload bench logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: pm-bench-logs-linux - path: /tmp/pm-bench-output - retention-days: 7 - - name: Build PR comment body - if: always() - run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest - - name: Find Current PR - id: findPr - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - PR_NUMBER="" - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) - fi - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-linux - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - bench-phases-mac: - needs: [build-mac-arm64] - if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') - runs-on: macos-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install bench tools - # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses - # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin - # symlink earlier on PATH. - run: | - brew install hyperfine gnu-time - mkdir -p "$HOME/bin" - ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" - echo "$HOME/bin" >> $GITHUB_PATH - - name: Install utoo@npm baseline - # See linux job — same rationale. - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Verify tools - run: | - hyperfine --version - utoo --version - "$UTOO_NPM_BIN" --version - bun --version - - name: Define cache wipe function - run: | - cat > /tmp/wipe-bench-state.sh <<'WIPE' - #!/usr/bin/env bash - rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ - ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true - mkdir -p /tmp/pm-bench-output - WIPE - chmod +x /tmp/wipe-bench-state.sh - - name: Wipe bench state before npmjs - run: bash /tmp/wipe-bench-state.sh - - name: Run phase-isolated benchmark · npmjs - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,utoo-npm,bun' - run: | - chmod +x bench/pm-bench-phases.sh - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - name: Stash npmjs logs and wipe state before npmmirror - run: | - mkdir -p /tmp/bench-logs-stash - cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true - bash /tmp/wipe-bench-state.sh - mkdir -p /tmp/pm-bench-output - cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - - name: Run phase-isolated benchmark · npmmirror - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmmirror.com' - BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} - PM_LIST: 'utoo,utoo-npm,bun' - run: | - mkdir -p /tmp/pm-bench-output - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - - name: Upload bench logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: pm-bench-logs-mac - path: /tmp/pm-bench-output - retention-days: 7 - - name: Build PR comment body - if: always() - run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest - - name: Find Current PR - id: findPr - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - PR_NUMBER="" - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) - fi - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-mac - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # ============================================================ - # PM-BENCH JOBS (manual perf compare) - # workflow_dispatch with target=pm-bench, or PR labeled `benchmark`. - # Compares utoo against other PMs (pnpm/yarn/bun/aube) on cold + warm - # installs across configurable registries. - # ============================================================ - - pm-bench-linux: - needs: [build-linux] - if: > - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV - - name: Build next branch utoo - if: github.event.inputs.build_next == 'true' - run: | - # The PATH already points at dist/utoo (current branch) — build - # `next` separately to /tmp so it doesn't shadow. - git fetch origin next --depth=1 - WORKDIR=$(mktemp -d) - git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . - (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) - cp "$WORKDIR/target/release/utoo" /tmp/utoo-next - chmod +x /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - name: Install hyperfine and GNU time - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - name: Verify tools - run: | - hyperfine --version - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - name: Run benchmark - run: | - chmod +x bench/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} - - pm-bench-mac: - needs: [build-mac-arm64] - if: > - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV - - name: Build next branch utoo - if: github.event.inputs.build_next == 'true' - run: | - git fetch origin next --depth=1 - WORKDIR=$(mktemp -d) - git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . - (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) - cp "$WORKDIR/target/release/utoo" /tmp/utoo-next - chmod +x /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - name: Install hyperfine - run: brew install hyperfine - - name: Verify tools - run: | - hyperfine --version - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - name: Run benchmark - run: | - chmod +x bench/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} - - # ============================================================ - # PM-BENCH-PCAP (manual perf-debug only) - # workflow_dispatch with target=pm-bench-pcap. Captures tcpdump - # of the resolve phase per PM (linux only — sudo + raw sockets). - # ============================================================ - - pm-bench-pcap-linux: - needs: [build-linux] - if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-pcap' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install pcap tools - run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time - - name: Capture pcap - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - run: | - chmod +x bench/pm-bench-pcap.sh - bash bench/pm-bench-pcap.sh - - name: Upload pcap artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: pm-bench-pcap - path: /tmp/pm-bench-pcap - retention-days: 7 + echo "Builds passed or were skipped" diff --git a/.github/workflows/pm-e2e.yml b/.github/workflows/pm-e2e.yml new file mode 100644 index 0000000000..d18c52e125 --- /dev/null +++ b/.github/workflows/pm-e2e.yml @@ -0,0 +1,324 @@ +name: utoopm-e2e + +# E2E test workflow. Triggered by `utoopm-build` completing — downloads +# each platform's `utoo` artifact and runs the e2e suite on that +# platform's runner. No `cargo build` here; everything reuses +# pm-build's artifacts. +# +# Per-target jobs gate themselves on artifact existence (pm-build's +# build-mac-x64 / build-windows skip for non-`(pm)` PRs, so the matching +# e2e job no-ops in that case). The `utoopm-e2e-success` aggregator +# always runs and reports success unless any non-skipped job failed — +# preserving the default-trigger / always-report behaviour the old +# pm-e2e.yml relied on for branch protection. +on: + workflow_run: + workflows: ["utoopm-build"] + types: [completed] + +permissions: + contents: read + actions: read + +env: + # Repo + run id of the upstream pm-build run we're consuming. Used by + # `gh api` artifact lookups and `actions/download-artifact@v4`'s + # cross-run `run-id` parameter. + UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} + UPSTREAM_REPO: ${{ github.repository }} + +jobs: + e2e-linux: + name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Check artifact exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-linux-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping linux e2e." + fi + - name: Setup node + if: steps.check.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + if: steps.check.outputs.skip != 'true' + run: | + utoo --version + ut --version + - name: Make scripts executable + if: steps.check.outputs.skip != 'true' + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + if: steps.check.outputs.skip != 'true' + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + if: steps.check.outputs.skip != 'true' + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-arm64: + name: utoopm-e2e-macos-latest-aarch64-apple-darwin + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Check artifact exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-mac-arm64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-mac-arm64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac arm64 e2e." + fi + - name: Setup node + if: steps.check.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + if: steps.check.outputs.skip != 'true' + run: | + utoo --version + ut --version + - name: Make scripts executable + if: steps.check.outputs.skip != 'true' + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + if: steps.check.outputs.skip != 'true' + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + if: steps.check.outputs.skip != 'true' + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-aarch64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-x64: + name: utoopm-e2e-macos-latest-x86_64-apple-darwin + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Check artifact exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-mac-x64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-mac-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac x64 e2e." + fi + - name: Install x86_64 Node.js via nvm + if: steps.check.outputs.skip != 'true' + run: | + arch -x86_64 bash -c ' + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm install 22 + nvm use 22 + nvm alias default 22 + echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) + echo "$NODE_PATH/bin" >> $GITHUB_PATH + node -p process.arch + npm -v + ' + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-mac-x64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + if: steps.check.outputs.skip != 'true' + run: | + utoo --version + ut --version + - name: Make scripts executable + if: steps.check.outputs.skip != 'true' + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + if: steps.check.outputs.skip != 'true' + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/utoo-pm.sh + ' + - name: Run arborist e2e tests + if: steps.check.outputs.skip != 'true' + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/pm-arborist.sh + ' + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-x86_64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-windows: + name: utoopm-e2e-windows-latest-x86_64-pc-windows-msvc + if: github.event.workflow_run.conclusion != 'cancelled' + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - name: Disable Windows Defender + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $true + - name: Check artifact exists + id: check + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ + --jq '[.artifacts[] | select(.name == "utoo-windows-x64" and .expired == false)] | length') + if [ "$HAS" -eq 0 ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::notice::No utoo-windows-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping windows e2e." + fi + - name: Setup node + if: steps.check.outputs.skip != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: utoo-windows-x64 + path: dist + run-id: ${{ env.UPSTREAM_RUN_ID }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install utoo + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + # ut.exe is just a copy of utoo.exe (Windows has no symlinks). + Copy-Item "$binPath\utoo.exe" "$binPath\ut.exe" -Force + echo "$binPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Verify tools + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + Get-Command utoo | Select-Object -ExpandProperty Source + Get-Command ut | Select-Object -ExpandProperty Source + utoo --version + ut --version + - name: Run e2e tests + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + git config --system core.longpaths true + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + pwsh -File e2e/utoo-pm.ps1 --verbose + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-windows-latest-x86_64-pc-windows-msvc + path: ${{ runner.temp }}/utoo-* + if-no-files-found: ignore + + e2e-success: + name: utoopm-e2e-success + runs-on: ubuntu-latest + if: always() + needs: [e2e-linux, e2e-mac-arm64, e2e-mac-x64, e2e-windows] + steps: + - name: Check success + run: | + for result in \ + "${{ needs.e2e-linux.result }}" \ + "${{ needs.e2e-mac-arm64.result }}" \ + "${{ needs.e2e-mac-x64.result }}" \ + "${{ needs.e2e-windows.result }}"; do + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "Tests failed or were cancelled" + exit 1 + fi + done + echo "Tests passed or were skipped" From 469fb5d16c945edc3c7f0b609021ae8f303e72d6 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 22:29:41 +0800 Subject: [PATCH 14/19] Revert "ci(pm): split pm-build into build-only + per-consumer workflows" This reverts commit 70f8df5dfecfc9556f5180bef03d51681cb29388. --- .github/workflows/pm-bench-pcap.yml | 88 --- .github/workflows/pm-bench-phases.yml | 324 ----------- .github/workflows/pm-bench.yml | 278 --------- .github/workflows/pm-build.yml | 804 +++++++++++++++++++++++++- .github/workflows/pm-e2e.yml | 324 ----------- 5 files changed, 776 insertions(+), 1042 deletions(-) delete mode 100644 .github/workflows/pm-bench-pcap.yml delete mode 100644 .github/workflows/pm-bench-phases.yml delete mode 100644 .github/workflows/pm-bench.yml delete mode 100644 .github/workflows/pm-e2e.yml diff --git a/.github/workflows/pm-bench-pcap.yml b/.github/workflows/pm-bench-pcap.yml deleted file mode 100644 index b840f02f38..0000000000 --- a/.github/workflows/pm-bench-pcap.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: utoopm-bench-pcap - -# Manual perf-debug only: captures tcpdump of the resolve phase per PM -# (linux only — sudo + raw sockets aren't available on macos GH runners). -# Self-contained — builds utoo inline so the workflow doesn't depend on -# pm-build's state. -on: - workflow_dispatch: - inputs: - project: - description: 'Project to bench (under github.com/ant-design/)' - required: false - default: 'ant-design' - type: string - -permissions: - contents: read - -jobs: - build-linux: - runs-on: ubuntu-latest - container: - image: ubuntu:20.04 - steps: - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git - - uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory /__w/utoo/utoo - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-pcap-linux - - name: Build utoo - run: cargo build --release --bin utoo -p utoo-pm - - name: Upload utoo binary - uses: actions/upload-artifact@v4 - with: - name: utoo-pcap-linux - path: target/release/utoo - retention-days: 1 - - pcap-linux: - needs: [build-linux] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 - with: - name: utoo-pcap-linux - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install pcap tools - run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time - - name: Capture pcap - env: - PROJECT: ${{ github.event.inputs.project || 'ant-design' }} - REGISTRY: 'https://registry.npmjs.org' - run: | - chmod +x bench/pm-bench-pcap.sh - bash bench/pm-bench-pcap.sh - - name: Upload pcap artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: pm-bench-pcap - path: /tmp/pm-bench-pcap - retention-days: 7 diff --git a/.github/workflows/pm-bench-phases.yml b/.github/workflows/pm-bench-phases.yml deleted file mode 100644 index 91e2838803..0000000000 --- a/.github/workflows/pm-bench-phases.yml +++ /dev/null @@ -1,324 +0,0 @@ -name: utoopm-bench-phases - -# Phase-isolated benchmark. Triggered by `utoopm-build` completing — -# downloads each platform's `utoo` artifact, runs the bench across npmjs -# + npmmirror, and posts a sticky PR comment per platform with the -# results. Includes utoo@npm (latest published) as a baseline column -# alongside utoo (this branch) and bun (latest). -on: - workflow_run: - workflows: ["utoopm-build"] - types: [completed] - -permissions: - contents: read - actions: read - pull-requests: write - -env: - UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} - UPSTREAM_REPO: ${{ github.repository }} - -jobs: - bench-phases-linux: - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Check artifact exists - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-linux-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping linux bench-phases." - fi - - name: Setup Node.js - if: steps.check.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - if: steps.check.outputs.skip != 'true' - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install bench tools - if: steps.check.outputs.skip != 'true' - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - name: Install utoo@npm baseline - # Install latest published utoo to a temp dir (NOT the global prefix) - # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench - # script invokes it via $UTOO_NPM_BIN absolute path. Points directly - # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's - # node_modules/.bin/ symlink isn't always created for local installs. - if: steps.check.outputs.skip != 'true' - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Verify tools - if: steps.check.outputs.skip != 'true' - run: | - hyperfine --version - utoo --version - "$UTOO_NPM_BIN" --version - bun --version - - name: Define cache wipe function - if: steps.check.outputs.skip != 'true' - run: | - cat > /tmp/wipe-bench-state.sh <<'WIPE' - #!/usr/bin/env bash - rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ - ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true - mkdir -p /tmp/pm-bench-output - WIPE - chmod +x /tmp/wipe-bench-state.sh - - name: Wipe bench state before npmjs - if: steps.check.outputs.skip != 'true' - run: bash /tmp/wipe-bench-state.sh - - name: Run phase-isolated benchmark · npmjs - if: steps.check.outputs.skip != 'true' - env: - PROJECT: 'ant-design' - REGISTRY: 'https://registry.npmjs.org' - BENCH_RUNS: '3' - PM_LIST: 'utoo,utoo-npm,bun' - run: | - chmod +x bench/pm-bench-phases.sh - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - name: Stash npmjs logs and wipe state before npmmirror - if: steps.check.outputs.skip != 'true' - run: | - mkdir -p /tmp/bench-logs-stash - cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true - bash /tmp/wipe-bench-state.sh - mkdir -p /tmp/pm-bench-output - cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - - name: Run phase-isolated benchmark · npmmirror - if: steps.check.outputs.skip != 'true' - env: - PROJECT: 'ant-design' - REGISTRY: 'https://registry.npmmirror.com' - BENCH_RUNS: '3' - PM_LIST: 'utoo,utoo-npm,bun' - run: | - mkdir -p /tmp/pm-bench-output - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - - name: Upload bench logs - if: always() && steps.check.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: pm-bench-logs-linux - path: /tmp/pm-bench-output - retention-days: 7 - - name: Build PR comment body - if: always() && steps.check.outputs.skip != 'true' - env: - GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} - run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest - - name: Find Current PR - id: findPr - if: always() && steps.check.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - # Try the upstream workflow_run's pull_requests payload first - # (only populated for same-repo PRs). Fall back to PR list lookup. - PR_NUMBER="" - PRS_JSON='${{ toJson(github.event.workflow_run.pull_requests) }}' - PR_NUMBER=$(echo "$PRS_JSON" | jq -r '.[0].number // empty' 2>/dev/null || true) - if [ -z "$PR_NUMBER" ]; then - PR_NUMBER=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --state open --json number --jq '.[0].number' || true) - fi - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number' || true) - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.check.outputs.skip != 'true' && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-linux - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - bench-phases-mac: - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Check artifact exists - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-mac-arm64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-mac-arm64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac bench-phases." - fi - - name: Setup Node.js - if: steps.check.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-mac-arm64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install bun - if: steps.check.outputs.skip != 'true' - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install bench tools - # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses - # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin - # symlink earlier on PATH. - if: steps.check.outputs.skip != 'true' - run: | - brew install hyperfine gnu-time - mkdir -p "$HOME/bin" - ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" - echo "$HOME/bin" >> $GITHUB_PATH - - name: Install utoo@npm baseline - # See linux job — same rationale. - if: steps.check.outputs.skip != 'true' - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Verify tools - if: steps.check.outputs.skip != 'true' - run: | - hyperfine --version - utoo --version - "$UTOO_NPM_BIN" --version - bun --version - - name: Define cache wipe function - if: steps.check.outputs.skip != 'true' - run: | - cat > /tmp/wipe-bench-state.sh <<'WIPE' - #!/usr/bin/env bash - rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ - /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ - ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true - mkdir -p /tmp/pm-bench-output - WIPE - chmod +x /tmp/wipe-bench-state.sh - - name: Wipe bench state before npmjs - if: steps.check.outputs.skip != 'true' - run: bash /tmp/wipe-bench-state.sh - - name: Run phase-isolated benchmark · npmjs - if: steps.check.outputs.skip != 'true' - env: - PROJECT: 'ant-design' - REGISTRY: 'https://registry.npmjs.org' - BENCH_RUNS: '3' - PM_LIST: 'utoo,utoo-npm,bun' - run: | - chmod +x bench/pm-bench-phases.sh - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log - - name: Stash npmjs logs and wipe state before npmmirror - if: steps.check.outputs.skip != 'true' - run: | - mkdir -p /tmp/bench-logs-stash - cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true - bash /tmp/wipe-bench-state.sh - mkdir -p /tmp/pm-bench-output - cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true - - name: Run phase-isolated benchmark · npmmirror - if: steps.check.outputs.skip != 'true' - env: - PROJECT: 'ant-design' - REGISTRY: 'https://registry.npmmirror.com' - BENCH_RUNS: '3' - PM_LIST: 'utoo,utoo-npm,bun' - run: | - mkdir -p /tmp/pm-bench-output - bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log - - name: Upload bench logs - if: always() && steps.check.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: pm-bench-logs-mac - path: /tmp/pm-bench-output - retention-days: 7 - - name: Build PR comment body - if: always() && steps.check.outputs.skip != 'true' - env: - GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} - run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest - - name: Find Current PR - id: findPr - if: always() && steps.check.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - PR_NUMBER="" - PRS_JSON='${{ toJson(github.event.workflow_run.pull_requests) }}' - PR_NUMBER=$(echo "$PRS_JSON" | jq -r '.[0].number // empty' 2>/dev/null || true) - if [ -z "$PR_NUMBER" ]; then - PR_NUMBER=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --state open --json number --jq '.[0].number' || true) - fi - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number' || true) - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.check.outputs.skip != 'true' && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-mac - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pm-bench.yml b/.github/workflows/pm-bench.yml deleted file mode 100644 index fec5615459..0000000000 --- a/.github/workflows/pm-bench.yml +++ /dev/null @@ -1,278 +0,0 @@ -name: utoopm-bench - -# Manual perf comparison: utoo (current branch) vs published utoo from -# npm vs other PMs (pnpm/yarn/bun/aube) on cold + warm installs across -# configurable registries. -# -# Self-contained — builds utoo inline rather than reusing pm-build's -# artifact, so the workflow Just Works regardless of pm-build's state. -# Triggered manually via workflow_dispatch (or by labelling a PR with -# `benchmark`). -on: - workflow_dispatch: - inputs: - registry: - description: 'Registry to test' - required: true - default: 'both' - type: choice - options: [both, npmjs, npmmirror] - pm_list: - description: 'Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' - required: true - default: 'utoo,bun' - type: string - baseline: - description: 'Compare with published utoo from npm' - required: false - default: true - type: boolean - build_next: - description: 'Also compare with utoo built from `next` branch' - required: false - default: false - type: boolean - cold_runs: - description: 'Number of cold install runs' - required: false - default: '3' - type: string - warm_runs: - description: 'Number of warm install runs' - required: false - default: '3' - type: string - pull_request: - types: [labeled] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build-linux: - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: ubuntu-latest - container: - image: ubuntu:20.04 - steps: - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: apt-get update && apt-get install -y build-essential curl pkg-config libssl-dev git - - uses: actions/checkout@v4 - - name: Configure git safe directory - run: git config --global --add safe.directory /__w/utoo/utoo - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-linux - - name: Build current branch utoo - run: cargo build --release --bin utoo -p utoo-pm - - name: Upload current binary - uses: actions/upload-artifact@v4 - with: - name: utoo-bench-current-linux - path: target/release/utoo - retention-days: 1 - - name: Build next branch utoo - if: github.event.inputs.build_next == 'true' - run: | - git fetch origin next --depth=1 - git checkout origin/next - git submodule update --init --recursive --depth 1 - cargo build --release --bin utoo -p utoo-pm - - name: Upload next binary - if: github.event.inputs.build_next == 'true' - uses: actions/upload-artifact@v4 - with: - name: utoo-bench-next-linux - path: target/release/utoo - retention-days: 1 - - bench-linux: - needs: [build-linux] - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download current binary - uses: actions/download-artifact@v4 - with: - name: utoo-bench-current-linux - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Download next binary - if: github.event.inputs.build_next == 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-bench-next-linux - path: dist-next - - name: Install next binary - if: github.event.inputs.build_next == 'true' - run: | - chmod +x dist-next/utoo - cp dist-next/utoo /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - name: Install hyperfine and GNU time - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - name: Verify tools - run: | - hyperfine --version - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - name: Run benchmark - run: | - chmod +x bench/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} - - bench-mac: - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Init git submodules - run: git submodule update --init --recursive --depth 1 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: nightly-2026-04-02 - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - with: - shared-key: pm-bench-macos-latest - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV - - name: Build baseline utoo from next branch - if: github.event.inputs.build_next == 'true' - run: | - CURRENT_SHA=$(git rev-parse HEAD) - git fetch origin next --depth=1 - git checkout origin/next - git submodule update --init --recursive --depth 1 - cargo build --release --bin utoo -p utoo-pm - cp target/release/utoo /tmp/utoo-next - chmod +x /tmp/utoo-next - echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" - echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV - git checkout "$CURRENT_SHA" - git submodule update --init --recursive --depth 1 - - name: Build utoo-pm - run: cargo build --release --bin utoo -p utoo-pm - - name: Update PATH - run: echo "${{ github.workspace }}/target/release" >> $GITHUB_PATH - - name: Create ut symlink - run: ln -sf utoo $(dirname $(which utoo))/ut - - name: Resolve pm_list - id: pm - run: | - PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" - if [ "$PM_LIST" = "all" ]; then - PM_LIST="utoo,pnpm,yarn,bun,aube" - fi - echo "list=$PM_LIST" >> $GITHUB_OUTPUT - - name: Install pnpm - if: contains(steps.pm.outputs.list, 'pnpm') - uses: pnpm/action-setup@v4 - with: - version: latest - - name: Install yarn - if: contains(steps.pm.outputs.list, 'yarn') - run: npm install -g yarn - - name: Install bun - if: contains(steps.pm.outputs.list, 'bun') - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Install aube - if: contains(steps.pm.outputs.list, 'aube') - run: npm install -g @endevco/aube - - name: Install hyperfine - run: brew install hyperfine - - name: Verify tools - run: | - hyperfine --version - IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" - for pm in "${PMS[@]}"; do - echo "$pm: $($pm --version 2>&1 || echo 'not installed')" - done - - name: Run benchmark - run: | - chmod +x bench/pm-bench.sh - BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ - BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ - bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-build.yml index 465c610dc1..97e309a44a 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-build.yml @@ -1,22 +1,78 @@ name: utoopm-build -# Build-only workflow. Builds `utoo` once per target and uploads each as -# an artifact. Downstream workflows (pm-e2e, pm-bench-phases, pm-bench, -# pm-bench-pcap) trigger on this workflow's completion via `workflow_run` -# and download the artifacts they need — so each target's -# `cargo build --release` runs at most once per commit. +# Single workflow that builds utoo once per target, then fans out to +# every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Replaces +# the old pm-e2e.yml + pm-bench.yml + pm-bench-phases.yml trio so each +# target builds exactly once per commit and downstream jobs reuse the +# uploaded artifact. # -# Triggers: -# push to next/e2e-pm — only when commit msg contains `(pm` -# pull_request — only when title contains `(pm`, OR labeled `benchmark` -# workflow_dispatch — always builds linux + mac arm64 (skips mac x64 + -# windows since they only feed e2e and dispatch is for bench/pcap) +# Triggers (event → consumers): +# pull_request / push with `(pm)` in title/msg → e2e jobs +# pull_request / push with `(pm` (loose) in title/msg → bench-phases +# pull_request labeled `benchmark` → pm-bench (manual perf compare) +# workflow_dispatch with `target` choice → manual one-off runs +# +# Build job gates are the union of their downstream consumers' gates. +# Each downstream job is pure download-and-run, no `cargo build`. on: push: branches: ["next", "e2e-pm"] pull_request: types: [opened, synchronize, reopened, edited, labeled] workflow_dispatch: + inputs: + target: + description: 'Manual target to run' + required: true + type: choice + options: + - pm-bench + - pm-bench-phases + - pm-bench-pcap + default: pm-bench + # ----- pm-bench inputs ----- + registry: + description: '[pm-bench] Registry to test' + required: false + default: 'both' + type: choice + options: [both, npmjs, npmmirror] + pm_list: + description: '[pm-bench] Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' + required: false + default: 'utoo,bun' + type: string + baseline: + description: '[pm-bench] Compare with published utoo from npm' + required: false + default: true + type: boolean + build_next: + description: '[pm-bench] Also compare with utoo built from `next` branch' + required: false + default: false + type: boolean + cold_runs: + description: '[pm-bench] Number of cold install runs' + required: false + default: '3' + type: string + warm_runs: + description: '[pm-bench] Number of warm install runs' + required: false + default: '3' + type: string + # ----- pm-bench-phases / pcap inputs ----- + project: + description: '[phases/pcap] Project to bench (under github.com/ant-design/)' + required: false + default: 'ant-design' + type: string + bench_runs: + description: '[phases] Runs per phase' + required: false + default: '3' + type: string concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -27,13 +83,15 @@ permissions: jobs: # ============================================================ - # Per-target build jobs. Gates are unions of every downstream - # consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Linux + - # mac-arm64 have the broadest set of consumers; mac-x64 and - # windows only feed e2e (strict `(pm)` scope). + # BUILD JOBS + # One per target, runs whenever any downstream consumer needs it. + # Downstream jobs `needs:` the relevant build job and download + # the uploaded artifact. No downstream rebuilds. # ============================================================ build-linux: + # Linux serves: e2e-linux, bench-phases-linux, pm-bench-linux, pm-bench-pcap-linux. + # Gate is the union of all linux consumers' gates. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || @@ -85,11 +143,12 @@ jobs: retention-days: 1 build-mac-arm64: + # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-mac. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || - github.event_name == 'workflow_dispatch' + (github.event_name == 'workflow_dispatch' && inputs.target != 'pm-bench-pcap') runs-on: macos-latest steps: - uses: actions/checkout@v4 @@ -114,8 +173,8 @@ jobs: retention-days: 1 build-mac-x64: - # Only feeds e2e-mac-x64 — strict `(pm)` scope, no bench consumers. - # Cross-compiled on macos-latest (arm64 host). + # Mac x86_64 serves: e2e-mac-x64 only. Cross-compiled on macos-latest + # (arm64 host) via `rustup target add x86_64-apple-darwin`. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) @@ -145,7 +204,7 @@ jobs: retention-days: 1 build-windows: - # Only feeds e2e-windows — strict `(pm)` scope. + # Windows serves: e2e-windows only. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) @@ -175,25 +234,714 @@ jobs: path: target/x86_64-pc-windows-msvc/release/utoo.exe retention-days: 1 - # Always-runs aggregator so this workflow reports a definite check - # even on PRs where every build was gated out. Downstream workflows - # use `workflow_run` and react to this run's conclusion. - build-success: - name: utoopm-build-success + # ============================================================ + # E2E JOBS + # Auto-trigger on `(pm)` (strict) PR title / commit msg. + # ============================================================ + + e2e-linux: + needs: [build-linux] + name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-arm64: + needs: [build-mac-arm64] + name: utoopm-e2e-macos-latest-aarch64-apple-darwin + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + run: bash e2e/utoo-pm.sh + - name: Run arborist e2e tests + run: bash e2e/pm-arborist.sh + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-aarch64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-mac-x64: + needs: [build-mac-x64] + name: utoopm-e2e-macos-latest-x86_64-apple-darwin + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install x86_64 Node.js via nvm + run: | + arch -x86_64 bash -c ' + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm install 22 + nvm use 22 + nvm alias default 22 + echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) + echo "$NODE_PATH/bin" >> $GITHUB_PATH + node -p process.arch + npm -v + ' + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Verify tools + run: | + utoo --version + ut --version + - name: Make scripts executable + run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + - name: Run e2e tests + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/utoo-pm.sh + ' + - name: Run arborist e2e tests + shell: bash + run: | + arch -x86_64 bash -c ' + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 22 + bash e2e/pm-arborist.sh + ' + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-macos-latest-x86_64-apple-darwin + path: /tmp/utoo-* + if-no-files-found: ignore + + e2e-windows: + needs: [build-windows] + name: utoopm-e2e-windows-latest-x86_64-pc-windows-msvc + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Disable Windows Defender + shell: powershell + run: Set-MpPreference -DisableRealtimeMonitoring $true + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-windows-x64 + path: dist + - name: Install utoo + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + # ut.exe is just a copy of utoo.exe (Windows has no symlinks). + Copy-Item "$binPath\utoo.exe" "$binPath\ut.exe" -Force + echo "$binPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Verify tools + shell: pwsh + run: | + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + Get-Command utoo | Select-Object -ExpandProperty Source + Get-Command ut | Select-Object -ExpandProperty Source + utoo --version + ut --version + - name: Run e2e tests + shell: pwsh + run: | + git config --system core.longpaths true + $binPath = "${{ github.workspace }}\dist" + $env:PATH = "$binPath;$env:PATH" + pwsh -File e2e/utoo-pm.ps1 --verbose + - name: Upload e2e artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: e2e-artifacts-windows-latest-x86_64-pc-windows-msvc + path: ${{ runner.temp }}/utoo-* + if-no-files-found: ignore + + e2e-success: + name: utoopm-e2e-success runs-on: ubuntu-latest if: always() - needs: [build-linux, build-mac-arm64, build-mac-x64, build-windows] + needs: [build-linux, build-mac-arm64, build-mac-x64, build-windows, e2e-linux, e2e-mac-arm64, e2e-mac-x64, e2e-windows] steps: - - name: Check builds + - name: Check success run: | for result in \ "${{ needs.build-linux.result }}" \ "${{ needs.build-mac-arm64.result }}" \ "${{ needs.build-mac-x64.result }}" \ - "${{ needs.build-windows.result }}"; do + "${{ needs.build-windows.result }}" \ + "${{ needs.e2e-linux.result }}" \ + "${{ needs.e2e-mac-arm64.result }}" \ + "${{ needs.e2e-mac-x64.result }}" \ + "${{ needs.e2e-windows.result }}"; do if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then - echo "Build failed or was cancelled" + echo "Tests failed or were cancelled" exit 1 fi done - echo "Builds passed or were skipped" + echo "Tests passed or were skipped" + + # ============================================================ + # BENCH-PHASES JOBS + # Auto-trigger on `(pm` (loose) — covers `(pm)`, `(pm-bench-phases)`, + # `(pm-foo)`, etc. Posts a sticky PR comment per platform with the + # phase-isolated bench results across npmjs + npmmirror. + # ============================================================ + + bench-phases-linux: + needs: [build-linux] + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Install utoo@npm baseline + # Install latest published utoo to a temp dir (NOT the global prefix) + # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench + # script invokes it via $UTOO_NPM_BIN absolute path. Points directly + # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's + # node_modules/.bin/ symlink isn't always created for local installs. + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Verify tools + run: | + hyperfine --version + utoo --version + "$UTOO_NPM_BIN" --version + bun --version + - name: Define cache wipe function + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,utoo-npm,bun' + run: | + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,utoo-npm,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-linux + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() + run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest + - name: Find Current PR + id: findPr + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-linux + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + bench-phases-mac: + needs: [build-mac-arm64] + if: > + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') + runs-on: macos-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install bench tools + # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses + # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin + # symlink earlier on PATH. + run: | + brew install hyperfine gnu-time + mkdir -p "$HOME/bin" + ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" + echo "$HOME/bin" >> $GITHUB_PATH + - name: Install utoo@npm baseline + # See linux job — same rationale. + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - name: Verify tools + run: | + hyperfine --version + utoo --version + "$UTOO_NPM_BIN" --version + bun --version + - name: Define cache wipe function + run: | + cat > /tmp/wipe-bench-state.sh <<'WIPE' + #!/usr/bin/env bash + rm -rf /tmp/pm-bench /tmp/pm-bench-results /tmp/pm-bench-output \ + /tmp/utoo-bench-cache /tmp/utoo-npm-bench-cache /tmp/bun-bench-cache \ + ~/.bun/install/cache ~/.cache/nm 2>/dev/null || true + mkdir -p /tmp/pm-bench-output + WIPE + chmod +x /tmp/wipe-bench-state.sh + - name: Wipe bench state before npmjs + run: bash /tmp/wipe-bench-state.sh + - name: Run phase-isolated benchmark · npmjs + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,utoo-npm,bun' + run: | + chmod +x bench/pm-bench-phases.sh + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmjs.log + - name: Stash npmjs logs and wipe state before npmmirror + run: | + mkdir -p /tmp/bench-logs-stash + cp -r /tmp/pm-bench-output/. /tmp/bench-logs-stash/ 2>/dev/null || true + bash /tmp/wipe-bench-state.sh + mkdir -p /tmp/pm-bench-output + cp -r /tmp/bench-logs-stash/. /tmp/pm-bench-output/ 2>/dev/null || true + - name: Run phase-isolated benchmark · npmmirror + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmmirror.com' + BENCH_RUNS: ${{ github.event.inputs.bench_runs || '3' }} + PM_LIST: 'utoo,utoo-npm,bun' + run: | + mkdir -p /tmp/pm-bench-output + bash bench/pm-bench-phases.sh 2>&1 | tee /tmp/pm-bench-output/bench-phases-npmmirror.log + - name: Upload bench logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-logs-mac + path: /tmp/pm-bench-output + retention-days: 7 + - name: Build PR comment body + if: always() + run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest + - name: Find Current PR + id: findPr + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: always() && steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-mac + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ============================================================ + # PM-BENCH JOBS (manual perf compare) + # workflow_dispatch with target=pm-bench, or PR labeled `benchmark`. + # Compares utoo against other PMs (pnpm/yarn/bun/aube) on cold + warm + # installs across configurable registries. + # ============================================================ + + pm-bench-linux: + needs: [build-linux] + if: > + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + npm install -g utoo + BASELINE_PATH=$(which utoo) + cp "$BASELINE_PATH" /tmp/utoo-npm + chmod +x /tmp/utoo-npm + echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" + echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + - name: Build next branch utoo + if: github.event.inputs.build_next == 'true' + run: | + # The PATH already points at dist/utoo (current branch) — build + # `next` separately to /tmp so it doesn't shadow. + git fetch origin next --depth=1 + WORKDIR=$(mktemp -d) + git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . + (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) + cp "$WORKDIR/target/release/utoo" /tmp/utoo-next + chmod +x /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine and GNU time + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x bench/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + + pm-bench-mac: + needs: [build-mac-arm64] + if: > + (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-mac-arm64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + ln -sf utoo dist/ut + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install baseline utoo from npm + if: github.event.inputs.baseline != 'false' + run: | + npm install -g utoo + BASELINE_PATH=$(which utoo) + cp "$BASELINE_PATH" /tmp/utoo-npm + chmod +x /tmp/utoo-npm + echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" + echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + - name: Build next branch utoo + if: github.event.inputs.build_next == 'true' + run: | + git fetch origin next --depth=1 + WORKDIR=$(mktemp -d) + git --work-tree="$WORKDIR" --git-dir="$(pwd)/.git" checkout origin/next -- . + (cd "$WORKDIR" && git submodule update --init --recursive --depth 1 && cargo build --release --bin utoo -p utoo-pm) + cp "$WORKDIR/target/release/utoo" /tmp/utoo-next + chmod +x /tmp/utoo-next + echo "Baseline utoo (next) version: $(/tmp/utoo-next --version)" + echo "UTOO_NEXT_BIN=/tmp/utoo-next" >> $GITHUB_ENV + - name: Resolve pm_list + id: pm + run: | + PM_LIST="${{ github.event.inputs.pm_list || 'utoo,bun' }}" + if [ "$PM_LIST" = "all" ]; then + PM_LIST="utoo,pnpm,yarn,bun,aube" + fi + echo "list=$PM_LIST" >> $GITHUB_OUTPUT + - name: Install pnpm + if: contains(steps.pm.outputs.list, 'pnpm') + uses: pnpm/action-setup@v4 + with: + version: latest + - name: Install yarn + if: contains(steps.pm.outputs.list, 'yarn') + run: npm install -g yarn + - name: Install bun + if: contains(steps.pm.outputs.list, 'bun') + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install aube + if: contains(steps.pm.outputs.list, 'aube') + run: npm install -g @endevco/aube + - name: Install hyperfine + run: brew install hyperfine + - name: Verify tools + run: | + hyperfine --version + IFS=',' read -ra PMS <<< "${{ steps.pm.outputs.list }}" + for pm in "${PMS[@]}"; do + echo "$pm: $($pm --version 2>&1 || echo 'not installed')" + done + - name: Run benchmark + run: | + chmod +x bench/pm-bench.sh + BENCH_COLD_RUNS=${{ github.event.inputs.cold_runs || '3' }} \ + BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ + bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} + + # ============================================================ + # PM-BENCH-PCAP (manual perf-debug only) + # workflow_dispatch with target=pm-bench-pcap. Captures tcpdump + # of the resolve phase per PM (linux only — sudo + raw sockets). + # ============================================================ + + pm-bench-pcap-linux: + needs: [build-linux] + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-pcap' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: utoo-linux-x64 + path: dist + - name: Install utoo + run: | + chmod +x dist/utoo + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install pcap tools + run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time + - name: Capture pcap + env: + PROJECT: ${{ github.event.inputs.project || 'ant-design' }} + REGISTRY: 'https://registry.npmjs.org' + run: | + chmod +x bench/pm-bench-pcap.sh + bash bench/pm-bench-pcap.sh + - name: Upload pcap artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: pm-bench-pcap + path: /tmp/pm-bench-pcap + retention-days: 7 diff --git a/.github/workflows/pm-e2e.yml b/.github/workflows/pm-e2e.yml deleted file mode 100644 index d18c52e125..0000000000 --- a/.github/workflows/pm-e2e.yml +++ /dev/null @@ -1,324 +0,0 @@ -name: utoopm-e2e - -# E2E test workflow. Triggered by `utoopm-build` completing — downloads -# each platform's `utoo` artifact and runs the e2e suite on that -# platform's runner. No `cargo build` here; everything reuses -# pm-build's artifacts. -# -# Per-target jobs gate themselves on artifact existence (pm-build's -# build-mac-x64 / build-windows skip for non-`(pm)` PRs, so the matching -# e2e job no-ops in that case). The `utoopm-e2e-success` aggregator -# always runs and reports success unless any non-skipped job failed — -# preserving the default-trigger / always-report behaviour the old -# pm-e2e.yml relied on for branch protection. -on: - workflow_run: - workflows: ["utoopm-build"] - types: [completed] - -permissions: - contents: read - actions: read - -env: - # Repo + run id of the upstream pm-build run we're consuming. Used by - # `gh api` artifact lookups and `actions/download-artifact@v4`'s - # cross-run `run-id` parameter. - UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} - UPSTREAM_REPO: ${{ github.repository }} - -jobs: - e2e-linux: - name: utoopm-e2e-ubuntu-latest-x86_64-unknown-linux-gnu - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Check artifact exists - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-linux-x64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-linux-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping linux e2e." - fi - - name: Setup node - if: steps.check.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-linux-x64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - if: steps.check.outputs.skip != 'true' - run: | - utoo --version - ut --version - - name: Make scripts executable - if: steps.check.outputs.skip != 'true' - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - if: steps.check.outputs.skip != 'true' - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - if: steps.check.outputs.skip != 'true' - run: bash e2e/pm-arborist.sh - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-ubuntu-latest-x86_64-unknown-linux-gnu - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-mac-arm64: - name: utoopm-e2e-macos-latest-aarch64-apple-darwin - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Check artifact exists - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-mac-arm64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-mac-arm64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac arm64 e2e." - fi - - name: Setup node - if: steps.check.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-mac-arm64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - if: steps.check.outputs.skip != 'true' - run: | - utoo --version - ut --version - - name: Make scripts executable - if: steps.check.outputs.skip != 'true' - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - if: steps.check.outputs.skip != 'true' - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - if: steps.check.outputs.skip != 'true' - run: bash e2e/pm-arborist.sh - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-macos-latest-aarch64-apple-darwin - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-mac-x64: - name: utoopm-e2e-macos-latest-x86_64-apple-darwin - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Check artifact exists - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-mac-x64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-mac-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping mac x64 e2e." - fi - - name: Install x86_64 Node.js via nvm - if: steps.check.outputs.skip != 'true' - run: | - arch -x86_64 bash -c ' - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 22 - nvm use 22 - nvm alias default 22 - echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV - NODE_PATH=$(find $HOME/.nvm/versions/node -name "v22.*" -type d | head -1) - echo "$NODE_PATH/bin" >> $GITHUB_PATH - node -p process.arch - npm -v - ' - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-mac-x64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Verify tools - if: steps.check.outputs.skip != 'true' - run: | - utoo --version - ut --version - - name: Make scripts executable - if: steps.check.outputs.skip != 'true' - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests - if: steps.check.outputs.skip != 'true' - shell: bash - run: | - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/utoo-pm.sh - ' - - name: Run arborist e2e tests - if: steps.check.outputs.skip != 'true' - shell: bash - run: | - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 - bash e2e/pm-arborist.sh - ' - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-macos-latest-x86_64-apple-darwin - path: /tmp/utoo-* - if-no-files-found: ignore - - e2e-windows: - name: utoopm-e2e-windows-latest-x86_64-pc-windows-msvc - if: github.event.workflow_run.conclusion != 'cancelled' - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - name: Disable Windows Defender - shell: powershell - run: Set-MpPreference -DisableRealtimeMonitoring $true - - name: Check artifact exists - id: check - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - HAS=$(gh api "repos/${UPSTREAM_REPO}/actions/runs/${UPSTREAM_RUN_ID}/artifacts" \ - --jq '[.artifacts[] | select(.name == "utoo-windows-x64" and .expired == false)] | length') - if [ "$HAS" -eq 0 ]; then - echo "skip=true" >> $GITHUB_OUTPUT - echo "::notice::No utoo-windows-x64 artifact in pm-build run ${UPSTREAM_RUN_ID} — skipping windows e2e." - fi - - name: Setup node - if: steps.check.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Download utoo binary - if: steps.check.outputs.skip != 'true' - uses: actions/download-artifact@v4 - with: - name: utoo-windows-x64 - path: dist - run-id: ${{ env.UPSTREAM_RUN_ID }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install utoo - if: steps.check.outputs.skip != 'true' - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\dist" - # ut.exe is just a copy of utoo.exe (Windows has no symlinks). - Copy-Item "$binPath\utoo.exe" "$binPath\ut.exe" -Force - echo "$binPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - name: Verify tools - if: steps.check.outputs.skip != 'true' - shell: pwsh - run: | - $binPath = "${{ github.workspace }}\dist" - $env:PATH = "$binPath;$env:PATH" - Get-Command utoo | Select-Object -ExpandProperty Source - Get-Command ut | Select-Object -ExpandProperty Source - utoo --version - ut --version - - name: Run e2e tests - if: steps.check.outputs.skip != 'true' - shell: pwsh - run: | - git config --system core.longpaths true - $binPath = "${{ github.workspace }}\dist" - $env:PATH = "$binPath;$env:PATH" - pwsh -File e2e/utoo-pm.ps1 --verbose - - name: Upload e2e artifacts on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: e2e-artifacts-windows-latest-x86_64-pc-windows-msvc - path: ${{ runner.temp }}/utoo-* - if-no-files-found: ignore - - e2e-success: - name: utoopm-e2e-success - runs-on: ubuntu-latest - if: always() - needs: [e2e-linux, e2e-mac-arm64, e2e-mac-x64, e2e-windows] - steps: - - name: Check success - run: | - for result in \ - "${{ needs.e2e-linux.result }}" \ - "${{ needs.e2e-mac-arm64.result }}" \ - "${{ needs.e2e-mac-x64.result }}" \ - "${{ needs.e2e-windows.result }}"; do - if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then - echo "Tests failed or were cancelled" - exit 1 - fi - done - echo "Tests passed or were skipped" From a8c1e015eca2b61d21fb86c2dc1744da3118bb39 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 22:46:44 +0800 Subject: [PATCH 15/19] =?UTF-8?q?ci(pm):=20rename=20pm-build.yml=20?= =?UTF-8?q?=E2=86=92=20pm-e2e-bench.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow does e2e + bench (in addition to building utoo) — the old `pm-build` name only described the first stage. `pm-e2e-bench` explicitly lists what the consolidated workflow covers and reads better alongside `pm-ci` (unit tests) and `pm-release` (publishing). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/render-bench-phases-comment.sh | 2 +- .github/workflows/{pm-build.yml => pm-e2e-bench.yml} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{pm-build.yml => pm-e2e-bench.yml} (99%) diff --git a/.github/scripts/render-bench-phases-comment.sh b/.github/scripts/render-bench-phases-comment.sh index 9fd8531ab7..7cc2340dad 100755 --- a/.github/scripts/render-bench-phases-comment.sh +++ b/.github/scripts/render-bench-phases-comment.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Render the pm-bench-phases sticky PR comment from per-registry log files -# in /tmp/pm-bench-output/. Called by pm-build.yml's bench-phases-* jobs. +# in /tmp/pm-bench-output/. Called by pm-e2e-bench.yml's bench-phases-* jobs. # # Output: /tmp/pm-bench-output/pr_comment.md # diff --git a/.github/workflows/pm-build.yml b/.github/workflows/pm-e2e-bench.yml similarity index 99% rename from .github/workflows/pm-build.yml rename to .github/workflows/pm-e2e-bench.yml index 97e309a44a..c3229f66cd 100644 --- a/.github/workflows/pm-build.yml +++ b/.github/workflows/pm-e2e-bench.yml @@ -1,4 +1,4 @@ -name: utoopm-build +name: utoopm-e2e-bench # Single workflow that builds utoo once per target, then fans out to # every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Replaces From f13a7a0d0f481a56eb536d349676a6ca6af8c627 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 23:18:24 +0800 Subject: [PATCH 16/19] ci(pm): bench-phases now requires the `bench` PR label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-isolated bench takes ~25 min per platform — too expensive to fire on every (pm-scoped PR. Switch the gate to require an explicit `bench` label (the PR author / reviewer adds it when they actually want bench numbers in the comment). Build jobs' gate union updated to include the `bench` and `benchmark` labels so the build artifact still gets produced when either trigger fires. Push to next/e2e-pm with `(pm)` in commit msg keeps building + running e2e (no bench), so trunk doesn't lose its smoke test. Also tightened the `(pm` loose match to `(pm)` strict everywhere — `(pm-foo)` scopes shouldn't auto-trigger e2e. They still trigger manual paths (label / dispatch) on demand. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-e2e-bench.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pm-e2e-bench.yml b/.github/workflows/pm-e2e-bench.yml index c3229f66cd..79a0c161c1 100644 --- a/.github/workflows/pm-e2e-bench.yml +++ b/.github/workflows/pm-e2e-bench.yml @@ -93,9 +93,10 @@ jobs: # Linux serves: e2e-linux, bench-phases-linux, pm-bench-linux, pm-bench-pcap-linux. # Gate is the union of all linux consumers' gates. if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest container: @@ -145,9 +146,10 @@ jobs: build-mac-arm64: # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-mac. if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') || + (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || (github.event_name == 'workflow_dispatch' && inputs.target != 'pm-bench-pcap') runs-on: macos-latest steps: @@ -473,8 +475,7 @@ jobs: bench-phases-linux: needs: [build-linux] if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') runs-on: ubuntu-latest permissions: @@ -600,8 +601,7 @@ jobs: bench-phases-mac: needs: [build-mac-arm64] if: > - (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') runs-on: macos-latest permissions: From 386a7b434560d17bf4a3c9a2169de4beac1fd36f Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 23:41:56 +0800 Subject: [PATCH 17/19] =?UTF-8?q?ci(pm):=20swap=20bench=20triggers=20?= =?UTF-8?q?=E2=80=94=20phases=20on=20`benchmark`=20label,=20pm-bench=20dis?= =?UTF-8?q?patch-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bench-phases now fires on the `benchmark` label (was `bench`) - pm-bench (full PM shootout — utoo + bun + pnpm + yarn + aube) is workflow_dispatch only. Comparing every PM is slow enough that even a label trigger isn't worth the runner time; user must explicitly dispatch when they want it. - build gates trimmed to drop the unused `bench` label entry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-e2e-bench.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pm-e2e-bench.yml b/.github/workflows/pm-e2e-bench.yml index 79a0c161c1..cafbe1b423 100644 --- a/.github/workflows/pm-e2e-bench.yml +++ b/.github/workflows/pm-e2e-bench.yml @@ -95,7 +95,6 @@ jobs: if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest @@ -148,7 +147,6 @@ jobs: if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) || - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || (github.event_name == 'workflow_dispatch' && inputs.target != 'pm-bench-pcap') runs-on: macos-latest @@ -475,7 +473,7 @@ jobs: bench-phases-linux: needs: [build-linux] if: > - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') runs-on: ubuntu-latest permissions: @@ -601,7 +599,7 @@ jobs: bench-phases-mac: needs: [build-mac-arm64] if: > - (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'bench')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) || (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-phases') runs-on: macos-latest permissions: @@ -736,9 +734,7 @@ jobs: pm-bench-linux: needs: [build-linux] - if: > - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -820,9 +816,7 @@ jobs: pm-bench-mac: needs: [build-mac-arm64] - if: > - (github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench') || - (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench' runs-on: macos-latest steps: - uses: actions/checkout@v4 From 19fa69ade07775c95f1093324d9f980ab162150f Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 23:45:23 +0800 Subject: [PATCH 18/19] =?UTF-8?q?ci(pm):=20rename=20pm-bench=20=E2=86=92?= =?UTF-8?q?=20pm-bench-all=20(full=20PM=20shootout)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Job names `pm-bench-{linux,mac}` → `pm-bench-all-{linux,mac}` and the workflow_dispatch target value `pm-bench` → `pm-bench-all`. Makes it explicit that this is the all-PMs comparison (utoo + pnpm + yarn + bun + aube), not the lighter phase-isolated bench. The shell script filename (bench/pm-bench.sh) is unchanged — it's a stable on-disk asset. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pm-e2e-bench.yml | 52 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/.github/workflows/pm-e2e-bench.yml b/.github/workflows/pm-e2e-bench.yml index cafbe1b423..5816d5d486 100644 --- a/.github/workflows/pm-e2e-bench.yml +++ b/.github/workflows/pm-e2e-bench.yml @@ -1,16 +1,18 @@ name: utoopm-e2e-bench # Single workflow that builds utoo once per target, then fans out to -# every consumer (e2e, bench-phases, pm-bench, pm-bench-pcap). Replaces -# the old pm-e2e.yml + pm-bench.yml + pm-bench-phases.yml trio so each -# target builds exactly once per commit and downstream jobs reuse the -# uploaded artifact. +# every consumer (e2e, bench-phases, pm-bench-all, pm-bench-pcap). +# Replaces the old pm-e2e.yml + pm-bench.yml + pm-bench-phases.yml +# trio so each target builds exactly once per commit and downstream +# jobs reuse the uploaded artifact. # # Triggers (event → consumers): # pull_request / push with `(pm)` in title/msg → e2e jobs -# pull_request / push with `(pm` (loose) in title/msg → bench-phases -# pull_request labeled `benchmark` → pm-bench (manual perf compare) +# pull_request labeled `benchmark` → bench-phases (utoo / utoo-npm / bun) # workflow_dispatch with `target` choice → manual one-off runs +# (pm-bench-all = full PM shootout vs pnpm/yarn/bun/aube; +# pm-bench-phases = same as the labeled trigger but ad-hoc; +# pm-bench-pcap = tcpdump capture for resolve phase) # # Build job gates are the union of their downstream consumers' gates. # Each downstream job is pure download-and-run, no `cargo build`. @@ -26,39 +28,39 @@ on: required: true type: choice options: - - pm-bench + - pm-bench-all - pm-bench-phases - pm-bench-pcap - default: pm-bench - # ----- pm-bench inputs ----- + default: pm-bench-all + # ----- pm-bench-all inputs ----- registry: - description: '[pm-bench] Registry to test' + description: '[pm-bench-all] Registry to test' required: false default: 'both' type: choice options: [both, npmjs, npmmirror] pm_list: - description: '[pm-bench] Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' + description: '[pm-bench-all] Comma-separated PMs (e.g. utoo,bun); "all" → utoo,pnpm,yarn,bun,aube' required: false default: 'utoo,bun' type: string baseline: - description: '[pm-bench] Compare with published utoo from npm' + description: '[pm-bench-all] Compare with published utoo from npm' required: false default: true type: boolean build_next: - description: '[pm-bench] Also compare with utoo built from `next` branch' + description: '[pm-bench-all] Also compare with utoo built from `next` branch' required: false default: false type: boolean cold_runs: - description: '[pm-bench] Number of cold install runs' + description: '[pm-bench-all] Number of cold install runs' required: false default: '3' type: string warm_runs: - description: '[pm-bench] Number of warm install runs' + description: '[pm-bench-all] Number of warm install runs' required: false default: '3' type: string @@ -90,7 +92,7 @@ jobs: # ============================================================ build-linux: - # Linux serves: e2e-linux, bench-phases-linux, pm-bench-linux, pm-bench-pcap-linux. + # Linux serves: e2e-linux, bench-phases-linux, pm-bench-all-linux, pm-bench-pcap-linux. # Gate is the union of all linux consumers' gates. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || @@ -143,7 +145,7 @@ jobs: retention-days: 1 build-mac-arm64: - # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-mac. + # Mac arm64 serves: e2e-mac-arm64, bench-phases-mac, pm-bench-all-mac. if: > (github.event_name == 'push' && contains(github.event.head_commit.message, '(pm)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pm)')) || @@ -726,15 +728,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ============================================================ - # PM-BENCH JOBS (manual perf compare) - # workflow_dispatch with target=pm-bench, or PR labeled `benchmark`. - # Compares utoo against other PMs (pnpm/yarn/bun/aube) on cold + warm - # installs across configurable registries. + # PM-BENCH-ALL (manual full PM shootout) + # workflow_dispatch with target=pm-bench-all only — comparing utoo + # against pnpm/yarn/bun/aube on cold + warm installs across + # configurable registries is slow, so it's never auto-triggered. # ============================================================ - pm-bench-linux: + pm-bench-all-linux: needs: [build-linux] - if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench' + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-all' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -814,9 +816,9 @@ jobs: BENCH_WARM_RUNS=${{ github.event.inputs.warm_runs || '3' }} \ bash bench/pm-bench.sh ${{ github.event.inputs.registry || 'both' }} ${{ steps.pm.outputs.list }} - pm-bench-mac: + pm-bench-all-mac: needs: [build-mac-arm64] - if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench' + if: github.event_name == 'workflow_dispatch' && inputs.target == 'pm-bench-all' runs-on: macos-latest steps: - uses: actions/checkout@v4 From 6f2bf0e9c36edd4392de39cb211cd047c908974e Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Sun, 26 Apr 2026 23:54:23 +0800 Subject: [PATCH 19/19] ci(pm): extract composite actions to dedup workflow steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull repeated step sequences out of pm-e2e-bench.yml into composite actions under .github/actions/. The workflow shrinks from ~947 lines to ~792 (~16%) and each shared concern now lives in one place instead of being copy-pasted across linux + mac variants. New actions: - setup-utoo-bin download artifact + chmod + (optional ut symlink) + PATH - install-utoo-npm-baseline npm install utoo + set UTOO_NPM_BIN - install-bench-tools per-platform hyperfine + GNU time install - post-bench-phases-comment render markdown + find PR + post sticky comment Behaviour unchanged — only structural refactor. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../actions/install-bench-tools/action.yml | 23 ++ .../install-utoo-npm-baseline/action.yml | 16 ++ .../post-bench-phases-comment/action.yml | 48 ++++ .github/actions/setup-utoo-bin/action.yml | 28 ++ .github/workflows/pm-e2e-bench.yml | 259 ++++-------------- 5 files changed, 169 insertions(+), 205 deletions(-) create mode 100644 .github/actions/install-bench-tools/action.yml create mode 100644 .github/actions/install-utoo-npm-baseline/action.yml create mode 100644 .github/actions/post-bench-phases-comment/action.yml create mode 100644 .github/actions/setup-utoo-bin/action.yml diff --git a/.github/actions/install-bench-tools/action.yml b/.github/actions/install-bench-tools/action.yml new file mode 100644 index 0000000000..5a1b129c35 --- /dev/null +++ b/.github/actions/install-bench-tools/action.yml @@ -0,0 +1,23 @@ +name: install-bench-tools +description: Install hyperfine + GNU time on the bench runner. macOS /usr/bin/time isn't GNU; install gnu-time and shadow `time` via $HOME/bin so bench scripts can use `time -v` regardless of platform. + +inputs: + platform: + description: linux or mac + required: true + +runs: + using: composite + steps: + - name: Install bench tools (linux) + if: inputs.platform == 'linux' + shell: bash + run: sudo apt-get update && sudo apt-get install -y hyperfine time + - name: Install bench tools (mac) + if: inputs.platform == 'mac' + shell: bash + run: | + brew install hyperfine gnu-time + mkdir -p "$HOME/bin" + ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" + echo "$HOME/bin" >> $GITHUB_PATH diff --git a/.github/actions/install-utoo-npm-baseline/action.yml b/.github/actions/install-utoo-npm-baseline/action.yml new file mode 100644 index 0000000000..6f5a1738a9 --- /dev/null +++ b/.github/actions/install-utoo-npm-baseline/action.yml @@ -0,0 +1,16 @@ +name: install-utoo-npm-baseline +description: Install latest published `utoo` from npm into a temp directory and set $UTOO_NPM_BIN. Used by bench-phases jobs to add a published-version baseline column alongside the current-branch utoo. Installed locally (not -g) so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH; npm's node_modules/.bin/ symlink isn't always populated for local installs, so we point at node_modules/utoo/bin/utoo (the package's `bin` field) directly. + +runs: + using: composite + steps: + - name: Install utoo@npm baseline + shell: bash + run: | + mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg + npm init -y >/dev/null + npm install utoo --no-audit --no-fund --no-progress + UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" + chmod +x "$UTOO_NPM_BIN" + "$UTOO_NPM_BIN" --version + echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV diff --git a/.github/actions/post-bench-phases-comment/action.yml b/.github/actions/post-bench-phases-comment/action.yml new file mode 100644 index 0000000000..a4d9e5d3c9 --- /dev/null +++ b/.github/actions/post-bench-phases-comment/action.yml @@ -0,0 +1,48 @@ +name: post-bench-phases-comment +description: Render bench-phases output as markdown tables and upsert a sticky PR comment. The comment is keyed by header `pm-bench-phases-` so each platform's results live as a separate sticky comment, idempotent across commits. + +inputs: + platform: + description: Platform name (linux / mac) — used in the comment header marker + required: true + os: + description: Runner OS string (ubuntu-latest / macos-latest) — shown in the comment title + required: true + github-token: + description: GitHub token for `gh` CLI lookups + sticky-comment posting + required: true + +runs: + using: composite + steps: + - name: Build PR comment body + shell: bash + run: bash .github/scripts/render-bench-phases-comment.sh ${{ inputs.platform }} ${{ inputs.os }} + - name: Find Current PR + id: findPr + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + GH_REPO: ${{ github.repository }} + run: | + PR_NUMBER="" + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + PR_NUMBER="${{ github.event.pull_request.number }}" + else + PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) + fi + fi + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT + fi + - name: Post PR Comment + if: steps.findPr.outputs.number + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pm-bench-phases-${{ inputs.platform }} + path: /tmp/pm-bench-output/pr_comment.md + number: ${{ steps.findPr.outputs.number }} + recreate: true + GITHUB_TOKEN: ${{ inputs.github-token }} diff --git a/.github/actions/setup-utoo-bin/action.yml b/.github/actions/setup-utoo-bin/action.yml new file mode 100644 index 0000000000..a59fe6454b --- /dev/null +++ b/.github/actions/setup-utoo-bin/action.yml @@ -0,0 +1,28 @@ +name: setup-utoo-bin +description: Download a utoo artifact built by pm-e2e-bench's build-* job, install it on PATH, optionally create the `ut` symlink. Unix only — Windows e2e handles its own .exe layout inline. + +inputs: + artifact: + description: Artifact name to download (e.g. utoo-linux-x64, utoo-mac-arm64) + required: true + with-ut-symlink: + description: Create `ut → utoo` symlink alongside the binary (e2e tests use both names; bench scripts don't). + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Download utoo binary + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact }} + path: dist + - name: Install utoo + shell: bash + run: | + chmod +x dist/utoo + if [ "${{ inputs.with-ut-symlink }}" = "true" ]; then + ln -sf utoo dist/ut + fi + echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH diff --git a/.github/workflows/pm-e2e-bench.yml b/.github/workflows/pm-e2e-bench.yml index 5816d5d486..a2a3f9c348 100644 --- a/.github/workflows/pm-e2e-bench.yml +++ b/.github/workflows/pm-e2e-bench.yml @@ -254,26 +254,18 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-linux-x64 - name: Verify tools run: | utoo --version ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - name: Run e2e tests - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - run: bash e2e/pm-arborist.sh + run: | + chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + bash e2e/utoo-pm.sh + bash e2e/pm-arborist.sh - name: Upload e2e artifacts on failure if: failure() uses: actions/upload-artifact@v4 @@ -295,26 +287,18 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-mac-arm64 - name: Verify tools run: | utoo --version ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - name: Run e2e tests - run: bash e2e/utoo-pm.sh - - name: Run arborist e2e tests - run: bash e2e/pm-arborist.sh + run: | + chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh + bash e2e/utoo-pm.sh + bash e2e/pm-arborist.sh - name: Upload e2e artifacts on failure if: failure() uses: actions/upload-artifact@v4 @@ -347,38 +331,22 @@ jobs: node -p process.arch npm -v ' - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-mac-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-mac-x64 - name: Verify tools run: | utoo --version ut --version - - name: Make scripts executable - run: chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh - - name: Run e2e tests + - name: Run e2e tests (under arch -x86_64) shell: bash run: | + chmod +x e2e/utoo-pm.sh e2e/pm-arborist.sh arch -x86_64 bash -c ' export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm use 22 bash e2e/utoo-pm.sh - ' - - name: Run arborist e2e tests - shell: bash - run: | - arch -x86_64 bash -c ' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use 22 bash e2e/pm-arborist.sh ' - name: Upload e2e artifacts on failure @@ -487,35 +455,18 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-linux-x64 + with-ut-symlink: 'false' - name: Install bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - - name: Install bench tools - run: sudo apt-get update && sudo apt-get install -y hyperfine time - - name: Install utoo@npm baseline - # Install latest published utoo to a temp dir (NOT the global prefix) - # so it doesn't shadow $GITHUB_WORKSPACE/dist/utoo on PATH. The bench - # script invokes it via $UTOO_NPM_BIN absolute path. Points directly - # at node_modules/utoo/bin/utoo (the package's `bin` field) — npm's - # node_modules/.bin/ symlink isn't always created for local installs. - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - uses: ./.github/actions/install-bench-tools + with: + platform: linux + - uses: ./.github/actions/install-utoo-npm-baseline - name: Verify tools run: | hyperfine --version @@ -566,37 +517,12 @@ jobs: name: pm-bench-logs-linux path: /tmp/pm-bench-output retention-days: 7 - - name: Build PR comment body - if: always() - run: bash .github/scripts/render-bench-phases-comment.sh linux ubuntu-latest - - name: Find Current PR - id: findPr - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - PR_NUMBER="" - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) - fi - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-linux - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - if: always() + uses: ./.github/actions/post-bench-phases-comment + with: + platform: linux + os: ubuntu-latest + github-token: ${{ secrets.GITHUB_TOKEN }} bench-phases-mac: needs: [build-mac-arm64] @@ -613,38 +539,18 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-mac-arm64 + with-ut-symlink: 'false' - name: Install bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - - name: Install bench tools - # macOS /usr/bin/time isn't GNU; bench/pm-bench-phases.sh uses - # `time -v`. Install gnu-time and shadow `time` with a $HOME/bin - # symlink earlier on PATH. - run: | - brew install hyperfine gnu-time - mkdir -p "$HOME/bin" - ln -sf "$(brew --prefix gnu-time)/bin/gtime" "$HOME/bin/time" - echo "$HOME/bin" >> $GITHUB_PATH - - name: Install utoo@npm baseline - # See linux job — same rationale. - run: | - mkdir -p /tmp/utoo-npm-pkg && cd /tmp/utoo-npm-pkg - npm init -y >/dev/null - npm install utoo --no-audit --no-fund --no-progress - UTOO_NPM_BIN="$PWD/node_modules/utoo/bin/utoo" - chmod +x "$UTOO_NPM_BIN" - "$UTOO_NPM_BIN" --version - echo "UTOO_NPM_BIN=$UTOO_NPM_BIN" >> $GITHUB_ENV + - uses: ./.github/actions/install-bench-tools + with: + platform: mac + - uses: ./.github/actions/install-utoo-npm-baseline - name: Verify tools run: | hyperfine --version @@ -695,37 +601,12 @@ jobs: name: pm-bench-logs-mac path: /tmp/pm-bench-output retention-days: 7 - - name: Build PR comment body - if: always() - run: bash .github/scripts/render-bench-phases-comment.sh mac macos-latest - - name: Find Current PR - id: findPr - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - PR_NUMBER="" - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - PR_NUMBER=$(gh pr list --head "${{ github.ref_name }}" --state open --json number --jq '.[0].number' || true) - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" == "null" ]; then - PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state open --json number --jq '.[0].number' || true) - fi - fi - if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then - echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT - fi - - name: Post PR Comment - if: always() && steps.findPr.outputs.number - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pm-bench-phases-mac - path: /tmp/pm-bench-output/pr_comment.md - number: ${{ steps.findPr.outputs.number }} - recreate: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - if: always() + uses: ./.github/actions/post-bench-phases-comment + with: + platform: mac + os: macos-latest + github-token: ${{ secrets.GITHUB_TOKEN }} # ============================================================ # PM-BENCH-ALL (manual full PM shootout) @@ -744,25 +625,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + artifact: utoo-linux-x64 + - if: github.event.inputs.baseline != 'false' + uses: ./.github/actions/install-utoo-npm-baseline - name: Build next branch utoo if: github.event.inputs.build_next == 'true' run: | @@ -826,25 +693,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-mac-arm64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - ln -sf utoo dist/ut - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH - - name: Install baseline utoo from npm - if: github.event.inputs.baseline != 'false' - run: | - npm install -g utoo - BASELINE_PATH=$(which utoo) - cp "$BASELINE_PATH" /tmp/utoo-npm - chmod +x /tmp/utoo-npm - echo "Baseline utoo (npm) version: $(/tmp/utoo-npm --version)" - echo "UTOO_NPM_BIN=/tmp/utoo-npm" >> $GITHUB_ENV + artifact: utoo-mac-arm64 + - if: github.event.inputs.baseline != 'false' + uses: ./.github/actions/install-utoo-npm-baseline - name: Build next branch utoo if: github.event.inputs.build_next == 'true' run: | @@ -912,20 +765,16 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - - name: Download utoo binary - uses: actions/download-artifact@v4 + - uses: ./.github/actions/setup-utoo-bin with: - name: utoo-linux-x64 - path: dist - - name: Install utoo - run: | - chmod +x dist/utoo - echo "$GITHUB_WORKSPACE/dist" >> $GITHUB_PATH + artifact: utoo-linux-x64 + with-ut-symlink: 'false' - name: Install bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install pcap tools + # tcpdump + dnsutils on top of the standard bench tooling. run: sudo apt-get update && sudo apt-get install -y tcpdump dnsutils hyperfine time - name: Capture pcap env: