Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 64 additions & 7 deletions .github/workflows/pm-bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,81 @@ name: utoopm-benchmark

on:
workflow_dispatch:
inputs:
registry:
description: 'Registry to test'
required: true
default: 'both'
type: choice
options:
- both
- npmjs
- npmmirror
pull_request:
types: [labeled]

jobs:
build:
runs-on: macos-latest

benchmark:
# Run on manual trigger or when PR is labeled with 'benchmark'
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.label.name == 'benchmark')
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
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-2025-10-27
components: rustfmt, clippy

- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: pm-bench
shared-key: pm-bench-${{ matrix.os }}

- 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: Install yarn
run: npm install -g yarn

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest

- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Verify package managers
run: |
echo "utoo: $(utoo --version)"
echo "yarn: $(yarn --version)"
echo "pnpm: $(pnpm --version)"
echo "bun: $(bun --version)"

- name: Run Benchmark
run: cargo bench -p utoo-pm
- name: Run benchmark
run: |
chmod +x e2e/pm-bench.sh
bash e2e/pm-bench.sh ${{ github.event.inputs.registry || 'both' }}
275 changes: 275 additions & 0 deletions e2e/pm-bench.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#!/bin/bash
set -e

Comment thread
elrrrrrrr marked this conversation as resolved.
# Check for required commands
for cmd in git node bc utoo pnpm bun; do
if ! command -v "$cmd" &> /dev/null; then
echo "Error: Required command '$cmd' not found. Please install it." >&2
exit 1
fi
done

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# Argument: both | npmjs | npmmirror
REGISTRY_MODE=${1:-both}

# Configuration
PROJECTS=("ant-design" "ant-design-x")
PACKAGE_MANAGERS=("utoo" "pnpm" "bun")
# PACKAGE_MANAGERS=("utoo" "yarn" "pnpm" "bun") # yarn is too slow, disabled for now
REGISTRIES=()

if [ "$REGISTRY_MODE" = "both" ] || [ "$REGISTRY_MODE" = "npmjs" ]; then
REGISTRIES+=("https://registry.npmjs.org")
fi
if [ "$REGISTRY_MODE" = "both" ] || [ "$REGISTRY_MODE" = "npmmirror" ]; then
REGISTRIES+=("https://registry.npmmirror.com")
fi

RESULTS_DIR="/tmp/pm-bench-results"
BENCH_DIR="/tmp/pm-bench"
mkdir -p "$RESULTS_DIR"

echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} PM Benchmark Starting...${NC}"
echo -e "${YELLOW}========================================${NC}"
echo -e "Registry mode: ${CYAN}$REGISTRY_MODE${NC}"
echo -e "Projects: ${CYAN}${PROJECTS[*]}${NC}"
echo -e "Package managers: ${CYAN}${PACKAGE_MANAGERS[*]}${NC}"
echo ""

# Create pnpm workspace config from package.json
setup_pnpm_workspace() {
local project_dir=$1
cd "$project_dir"

# Skip if pnpm-workspace.yaml already exists
if [ -f "pnpm-workspace.yaml" ]; then
return
fi

# Read workspaces from package.json and generate pnpm-workspace.yaml
if [ -f "package.json" ] && grep -q '"workspaces"' package.json; then
echo -e "Creating pnpm-workspace.yaml for $(basename $project_dir)..."
# Parse workspaces from package.json using node
node -e "
const pkg = require('./package.json');
const workspaces = pkg.workspaces || [];
const patterns = Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
if (patterns.length > 0) {
console.log('packages:');
patterns.forEach(p => console.log(' - \"' + p + '\"'));
}
" > pnpm-workspace.yaml 2>/dev/null || true
fi
}

# Clone test projects
clone_projects() {
mkdir -p "$BENCH_DIR"
cd "$BENCH_DIR"

echo -e "${YELLOW}Cloning projects...${NC}"

if [ ! -d "ant-design" ]; then
echo -e "Cloning ant-design..."
git clone --depth=1 https://github.com/ant-design/ant-design.git
else
echo -e "ant-design already exists, skipping clone"
fi

if [ ! -d "ant-design-x" ]; then
echo -e "Cloning ant-design-x (next branch)..."
git clone --branch next --depth=1 https://github.com/ant-design/x.git ant-design-x
else
echo -e "ant-design-x already exists, skipping clone"
fi

# Setup pnpm workspace configs
echo -e "${YELLOW}Setting up pnpm workspace configs...${NC}"
setup_pnpm_workspace "$BENCH_DIR/ant-design"
setup_pnpm_workspace "$BENCH_DIR/ant-design-x"

echo -e "${GREEN}Projects ready${NC}"
echo ""
}

# Clean lock files and node_modules using git clean
clean_local() {
git clean -dfx
}

# Cache path configuration
UTOO_CACHE_DIR="${UTOO_CACHE_DIR:-$HOME/.cache/nm}"
PNPM_STORE_DIR="${PNPM_STORE_DIR:-$(pnpm store path 2>/dev/null || echo "$HOME/.pnpm-store")}"
BUN_INSTALL_DIR="${BUN_INSTALL_DIR:-$HOME/.bun/install}"

# Clean global cache for each package manager (including manifest and tgz)
clean_pm_cache() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function, as well as clean_manifest_cache, uses hardcoded paths to user-specific directories (e.g., ~/.cache/nm, ~/.pnpm-store). This makes the script less portable and might fail if a user has a non-standard configuration. Whenever possible, it's better to retrieve these paths from the package manager itself (e.g., pnpm store path). If that's not possible, consider defining these paths as variables at the top of the script for easier configuration. Additionally, some paths have comments like (to be confirmed) (line 128), which indicates uncertainty and could lead to incorrect benchmark behavior. This should be verified or the code removed.

local pm=$1
echo -e " ${YELLOW}Cleaning $pm global cache...${NC}"
case $pm in
utoo)
rm -rf "$UTOO_CACHE_DIR"
;;
yarn)
yarn cache clean 2>/dev/null || rm -rf ~/.yarn/cache "$(yarn cache dir 2>/dev/null)"
;;
pnpm)
pnpm store prune 2>/dev/null || rm -rf "$PNPM_STORE_DIR"
;;
bun)
rm -rf "$BUN_INSTALL_DIR"
bun pm cache rm 2>/dev/null || true
;;
esac
}

# Clean only manifest cache, keep tgz package cache
clean_manifest_cache() {
local pm=$1
echo -e " ${YELLOW}Cleaning $pm manifest cache only...${NC}"
case $pm in
utoo)
rm -rf "$UTOO_CACHE_DIR/manifests" 2>/dev/null || true
;;
yarn)
# yarn v1 manifest cache
rm -rf ~/.yarn/cache/.npm-*.json 2>/dev/null || true
;;
pnpm)
# pnpm metadata cache
rm -rf "$PNPM_STORE_DIR/v3/metadata" 2>/dev/null || true
;;
bun)
# bun manifest cache are .npm files
rm -f "$BUN_INSTALL_DIR/cache/*.npm" 2>/dev/null || true
;;
esac
}

# Full cleanup (called before cold install)
clean_all() {
local pm=$1
clean_local
clean_pm_cache "$pm"
}

# Run single benchmark
run_benchmark() {
local project=$1
local pm=$2
local registry=$3
local install_type=$4 # cold | warm | hot

cd "$BENCH_DIR/$project"

# hot type: no cleanup here (manifest cache cleaned by caller)
if [ "$install_type" = "cold" ]; then
# Cold install: clean local files + all global cache
clean_all "$pm"
elif [ "$install_type" = "warm" ] || [ "$install_type" = "hot" ]; then
# warm/hot: only clean local files
clean_local
fi

# pnpm needs workspace config recreated (git clean deletes it)
if [ "$pm" = "pnpm" ]; then
setup_pnpm_workspace "$BENCH_DIR/$project"
fi

local cmd=()
# bun cold install needs --no-cache to disable HTTP 304 caching
local bun_cache_flag=""
if [ "$install_type" = "cold" ]; then
bun_cache_flag="--no-cache"
fi

local env_prefix=()
case $pm in
utoo) cmd=(utoo install --ignore-scripts "--registry=$registry") ;;
yarn) cmd=(yarn install --ignore-scripts --registry "$registry") ;;
pnpm) env_prefix=(env npm_config_package_manager_strict=false); cmd=(pnpm install --ignore-scripts --registry "$registry") ;;
bun) cmd=(bun install --ignore-scripts --registry "$registry" $bun_cache_flag) ;;
esac

local start=$(date +%s.%N)
local install_failed=0
if ! "${env_prefix[@]}" "${cmd[@]}" >/dev/null 2>&1; then
install_failed=1
fi
local end=$(date +%s.%N)

if [ "$install_failed" -eq 1 ]; then
echo -e " ${RED}$pm${NC} @ $registry ($install_type): ${RED}FAILED${NC}"
echo "$project,$pm,$registry,$install_type,FAILED" >> "$RESULTS_DIR/results.csv"
return
fi

local duration=$(echo "$end - $start" | bc)

# Output result
echo "$project,$pm,$registry,$install_type,$duration" >> "$RESULTS_DIR/results.csv"

local registry_short=$(echo "$registry" | sed 's|https://||' | cut -d'.' -f2)
echo -e " ${CYAN}$pm${NC} @ $registry_short ($install_type): ${GREEN}${duration}s${NC}"
}

# Print results table
print_results() {
echo ""
echo -e "${YELLOW}========================================${NC}"
echo -e "${YELLOW} Benchmark Results${NC}"
echo -e "${YELLOW}========================================${NC}"
echo ""

if command -v column &> /dev/null; then
cat "$RESULTS_DIR/results.csv" | column -t -s,
else
cat "$RESULTS_DIR/results.csv"
fi

echo ""
echo -e "${GREEN}Benchmark completed!${NC}"
}

# Main flow
main() {
echo "project,pm,registry,type,duration" > "$RESULTS_DIR/results.csv"

clone_projects

for project in "${PROJECTS[@]}"; do
for registry in "${REGISTRIES[@]}"; do
local registry_short=$(echo "$registry" | sed 's|https://||')
echo -e "${YELLOW}----------------------------------------${NC}"
echo -e "${YELLOW}Project: ${CYAN}$project${NC}"
echo -e "${YELLOW}Registry: ${CYAN}$registry_short${NC}"
echo -e "${YELLOW}----------------------------------------${NC}"

for pm in "${PACKAGE_MANAGERS[@]}"; do
# Cold install - no cache at all
run_benchmark "$project" "$pm" "$registry" "cold"
# Warm install - with all cache
run_benchmark "$project" "$pm" "$registry" "warm"

# bun specific test: delete manifest cache and run again
if [ "$pm" = "bun" ]; then
clean_manifest_cache "bun"
run_benchmark "$project" "$pm" "$registry" "hot"
fi
done
echo ""
done
done

print_results
}

main