name: Performance Testing
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 1' # Weekly on Monday at 2 AM UTC
workflow_dispatch:
jobs:
# Job 1: Benchmark Tests
benchmark:
name: Performance Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Run startup benchmark
run: |
echo "## ⚡ Performance Benchmarks" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Startup Time" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Run multiple iterations
TIMES=""
for i in {1..10}; do
START=$(date +%s%N)
timeout 1s node dist/server.js > /dev/null 2>&1 || true
END=$(date +%s%N)
TIME=$((($END - $START)/1000000))
TIMES="$TIMES $TIME"
done
# Calculate average
AVG=$(echo $TIMES | awk '{sum=0; for(i=1;i<=NF;i++)sum+=$i; print sum/NF}')
echo "Average startup time: ${AVG}ms" >> $GITHUB_STEP_SUMMARY
# Check threshold
if (( $(echo "$AVG > 500" | bc -l) )); then
echo "⚠️ Warning: Startup time exceeds 500ms threshold" >> $GITHUB_STEP_SUMMARY
exit 1
fi
env:
MCP_TRANSPORT: stdio
LOG_LEVEL: error
- name: Memory usage benchmark
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Memory Usage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
node --expose-gc -e "
const { performance } = require('perf_hooks');
async function measureMemory() {
// Force garbage collection
global.gc();
const before = process.memoryUsage();
// Import the server
await import('./dist/server.js');
// Force garbage collection again
global.gc();
const after = process.memoryUsage();
const used = {
rss: ((after.rss - before.rss) / 1024 / 1024).toFixed(2),
heapTotal: ((after.heapTotal - before.heapTotal) / 1024 / 1024).toFixed(2),
heapUsed: ((after.heapUsed - before.heapUsed) / 1024 / 1024).toFixed(2),
external: ((after.external - before.external) / 1024 / 1024).toFixed(2)
};
console.log('| Metric | Usage (MB) |');
console.log('|--------|------------|');
console.log(\`| RSS | \${used.rss} |\`);
console.log(\`| Heap Total | \${used.heapTotal} |\`);
console.log(\`| Heap Used | \${used.heapUsed} |\`);
console.log(\`| External | \${used.external} |\`);
// Check threshold
if (parseFloat(used.heapUsed) > 50) {
console.error('Warning: Heap usage exceeds 50MB');
process.exit(1);
}
}
measureMemory().catch(console.error);
" >> $GITHUB_STEP_SUMMARY
- name: Tool execution benchmark
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Tool Execution Performance" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Tool | Avg Time (ms) | Max Time (ms) |" >> $GITHUB_STEP_SUMMARY
echo "|------|---------------|---------------|" >> $GITHUB_STEP_SUMMARY
# Start server in background
npm start &
SERVER_PID=$!
sleep 5
# Test each tool
for tool in get_active_storms get_local_hurricane_alerts; do
TIMES=""
MAX=0
for i in {1..5}; do
START=$(date +%s%N)
if [ "$tool" = "get_active_storms" ]; then
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_active_storms"},"id":1}' > /dev/null
else
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_local_hurricane_alerts","arguments":{"lat":25.76,"lon":-80.19}},"id":1}' > /dev/null
fi
END=$(date +%s%N)
TIME=$((($END - $START)/1000000))
TIMES="$TIMES $TIME"
if [ $TIME -gt $MAX ]; then
MAX=$TIME
fi
done
AVG=$(echo $TIMES | awk '{sum=0; for(i=1;i<=NF;i++)sum+=$i; print sum/NF}')
echo "| $tool | $AVG | $MAX |" >> $GITHUB_STEP_SUMMARY
done
# Stop server
kill $SERVER_PID || true
env:
MCP_TRANSPORT: http
LOG_LEVEL: error
# Job 2: Load Testing
load-test:
name: Load Testing
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies
run: |
npm ci
npm install -g artillery
- name: Build and start server
run: |
npm run build
npm start &
echo "SERVER_PID=$!" >> $GITHUB_ENV
sleep 5
env:
MCP_TRANSPORT: http
LOG_LEVEL: error
- name: Create Artillery config
run: |
cat > artillery.yml << 'EOF'
config:
target: "http://localhost:8080"
phases:
- duration: 30
arrivalRate: 5
name: "Warm-up"
- duration: 60
arrivalRate: 10
name: "Sustained load"
- duration: 30
arrivalRate: 20
name: "Peak load"
processor: "./artillery-processor.js"
scenarios:
- name: "Health Check"
weight: 2
flow:
- get:
url: "/health"
expect:
- statusCode: 200
- name: "MCP Initialize"
weight: 1
flow:
- post:
url: "/mcp"
json:
jsonrpc: "2.0"
method: "initialize"
params:
protocolVersion: "2024-11-05"
capabilities: {}
clientInfo:
name: "artillery-test"
version: "1.0.0"
id: 1
capture:
- json: "$.result.sessionId"
as: "sessionId"
EOF
- name: Run load test
run: |
artillery run artillery.yml --output report.json || true
echo "## 🎯 Load Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -f report.json ]; then
artillery report report.json --output report.html
# Extract key metrics
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
SCENARIOS=$(cat report.json | jq '.aggregate.scenariosCompleted // 0')
REQUESTS=$(cat report.json | jq '.aggregate.requestsCompleted // 0')
RPS=$(cat report.json | jq '.aggregate.rps.mean // 0')
LATENCY_P50=$(cat report.json | jq '.aggregate.latency.p50 // 0')
LATENCY_P95=$(cat report.json | jq '.aggregate.latency.p95 // 0')
LATENCY_P99=$(cat report.json | jq '.aggregate.latency.p99 // 0')
ERRORS=$(cat report.json | jq '.aggregate.errors // 0')
echo "| Completed Scenarios | $SCENARIOS |" >> $GITHUB_STEP_SUMMARY
echo "| Total Requests | $REQUESTS |" >> $GITHUB_STEP_SUMMARY
echo "| Mean RPS | $RPS |" >> $GITHUB_STEP_SUMMARY
echo "| P50 Latency (ms) | $LATENCY_P50 |" >> $GITHUB_STEP_SUMMARY
echo "| P95 Latency (ms) | $LATENCY_P95 |" >> $GITHUB_STEP_SUMMARY
echo "| P99 Latency (ms) | $LATENCY_P99 |" >> $GITHUB_STEP_SUMMARY
echo "| Errors | $ERRORS |" >> $GITHUB_STEP_SUMMARY
fi
- name: Upload load test report
if: always()
uses: actions/upload-artifact@v3
with:
name: load-test-report
path: |
report.json
report.html
- name: Stop server
if: always()
run: |
kill ${{ env.SERVER_PID }} || true
# Job 3: Bundle Size Analysis
bundle-analysis:
name: Bundle Size Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Analyze bundle size
run: |
echo "## 📦 Bundle Size Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Get dist folder size
DIST_SIZE=$(du -sh dist | cut -f1)
echo "### Total dist size: $DIST_SIZE" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# List largest files
echo "### Largest Files" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Size |" >> $GITHUB_STEP_SUMMARY
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
du -h dist/* | sort -rh | head -10 | while read size file; do
filename=$(basename "$file")
echo "| $filename | $size |" >> $GITHUB_STEP_SUMMARY
done
# Check for source maps
echo "" >> $GITHUB_STEP_SUMMARY
if ls dist/*.map 1> /dev/null 2>&1; then
MAP_SIZE=$(du -ch dist/*.map | tail -1 | cut -f1)
echo "⚠️ Source maps found: $MAP_SIZE (consider excluding from production)" >> $GITHUB_STEP_SUMMARY
fi
- name: Dependency analysis
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Dependency Analysis" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Count dependencies
PROD_DEPS=$(cat package.json | jq '.dependencies | length')
DEV_DEPS=$(cat package.json | jq '.devDependencies | length')
echo "| Type | Count |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Production | $PROD_DEPS |" >> $GITHUB_STEP_SUMMARY
echo "| Development | $DEV_DEPS |" >> $GITHUB_STEP_SUMMARY
# Find largest dependencies
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Largest Dependencies" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
npx du -d 1 node_modules | sort -rn | head -5 >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY