Skip to content

Commit 748e836

Browse files
feedthejimclaude
andcommitted
chore: add dev boot profiling and benchmark scripts
- benchmark-boot-time.sh: Wall-clock benchmark with two metrics - benchmark-next-dev-boot.js: Multi-iteration benchmark with stats - profile-next-dev-boot.js: CPU profiling infrastructure - analyze-*.js: Profile and bundle analysis tools - trace-cli-startup.js: Module loading trace 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4fa7d80 commit 748e836

File tree

7 files changed

+1351
-0
lines changed

7 files changed

+1351
-0
lines changed
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
name: Dev Server Boot Time
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
9+
cancel-in-progress: true
10+
11+
env:
12+
NEXT_TELEMETRY_DISABLED: 1
13+
NODE_LTS_VERSION: 22
14+
15+
jobs:
16+
benchmark-boot-time:
17+
name: Benchmark Dev Server Boot Time
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 30
20+
21+
steps:
22+
- name: Checkout PR branch
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Setup Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: ${{ env.NODE_LTS_VERSION }}
31+
32+
- name: Setup pnpm
33+
run: |
34+
npm i -g corepack@0.31
35+
corepack enable
36+
corepack prepare pnpm@9.15.1 --activate
37+
38+
- name: Get pnpm store directory
39+
id: pnpm-cache
40+
run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
41+
42+
- name: Cache pnpm
43+
uses: actions/cache@v4
44+
with:
45+
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
46+
key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
47+
restore-keys: pnpm-store-
48+
49+
- name: Create test app
50+
run: |
51+
mkdir -p /tmp/boot-test-app/app
52+
cat > /tmp/boot-test-app/package.json << 'EOF'
53+
{
54+
"name": "boot-test-app",
55+
"private": true,
56+
"dependencies": {
57+
"react": "19.0.0",
58+
"react-dom": "19.0.0"
59+
}
60+
}
61+
EOF
62+
cat > /tmp/boot-test-app/app/layout.tsx << 'EOF'
63+
export default function RootLayout({ children }: { children: React.ReactNode }) {
64+
return <html><body>{children}</body></html>
65+
}
66+
EOF
67+
cat > /tmp/boot-test-app/app/page.tsx << 'EOF'
68+
export default function Home() { return <h1>Hello</h1> }
69+
EOF
70+
cd /tmp/boot-test-app && npm install
71+
72+
# ===== BENCHMARK FUNCTION =====
73+
# Measures two metrics:
74+
# - listen_time: when port starts accepting TCP connections
75+
# - ready_time: when first HTTP request succeeds
76+
- name: Create benchmark script
77+
run: |
78+
cat > /tmp/benchmark.sh << 'SCRIPT'
79+
#!/bin/bash
80+
set -e
81+
NEXT_BIN=$1
82+
TEST_DIR=$2
83+
RUNS=${3:-5}
84+
PORT=3456
85+
86+
benchmark_run() {
87+
local clean=$1
88+
[ "$clean" = "true" ] && rm -rf "$TEST_DIR/.next"
89+
90+
local start_ms=$(date +%s%3N)
91+
"$NEXT_BIN" dev --turbopack --port $PORT "$TEST_DIR" > /dev/null 2>&1 &
92+
local pid=$!
93+
94+
local listen_ms=""
95+
local ready_ms=""
96+
97+
# Wait for port to listen
98+
for i in $(seq 1 600); do
99+
if nc -z localhost $PORT 2>/dev/null; then
100+
listen_ms=$(date +%s%3N)
101+
break
102+
fi
103+
sleep 0.05
104+
done
105+
106+
# Wait for HTTP response
107+
if [ -n "$listen_ms" ]; then
108+
for i in $(seq 1 600); do
109+
if curl -s "http://localhost:$PORT" > /dev/null 2>&1; then
110+
ready_ms=$(date +%s%3N)
111+
break
112+
fi
113+
sleep 0.05
114+
done
115+
fi
116+
117+
kill $pid 2>/dev/null || true
118+
wait $pid 2>/dev/null || true
119+
120+
if [ -n "$listen_ms" ] && [ -n "$ready_ms" ]; then
121+
echo "$((listen_ms - start_ms)),$((ready_ms - start_ms))"
122+
else
123+
echo "TIMEOUT,TIMEOUT"
124+
fi
125+
}
126+
127+
run_series() {
128+
local name=$1
129+
local clean=$2
130+
local listen_sum=0
131+
local ready_sum=0
132+
local count=0
133+
134+
for i in $(seq 1 $RUNS); do
135+
result=$(benchmark_run "$clean")
136+
listen=$(echo "$result" | cut -d',' -f1)
137+
ready=$(echo "$result" | cut -d',' -f2)
138+
if [ "$listen" != "TIMEOUT" ]; then
139+
listen_sum=$((listen_sum + listen))
140+
ready_sum=$((ready_sum + ready))
141+
count=$((count + 1))
142+
echo "${name} run $i: listen=${listen}ms ready=${ready}ms" >&2
143+
fi
144+
done
145+
146+
if [ $count -gt 0 ]; then
147+
echo "$((listen_sum / count)),$((ready_sum / count))"
148+
else
149+
echo "0,0"
150+
fi
151+
}
152+
153+
# Run cold benchmark
154+
cold=$(run_series "cold" "true")
155+
echo "COLD_LISTEN=$(echo $cold | cut -d',' -f1)"
156+
echo "COLD_READY=$(echo $cold | cut -d',' -f2)"
157+
158+
# Warmup for bytecode cache
159+
rm -rf "$TEST_DIR/.next"
160+
"$NEXT_BIN" dev --turbopack --port $PORT "$TEST_DIR" > /dev/null 2>&1 &
161+
pid=$!
162+
for i in $(seq 1 200); do
163+
curl -s "http://localhost:$PORT" > /dev/null 2>&1 && break
164+
sleep 0.05
165+
done
166+
sleep 10
167+
kill $pid 2>/dev/null || true
168+
wait $pid 2>/dev/null || true
169+
170+
# Run warm benchmark
171+
warm=$(run_series "warm" "false")
172+
echo "WARM_LISTEN=$(echo $warm | cut -d',' -f1)"
173+
echo "WARM_READY=$(echo $warm | cut -d',' -f2)"
174+
SCRIPT
175+
chmod +x /tmp/benchmark.sh
176+
177+
# ===== BASELINE (canary) =====
178+
- name: Checkout canary
179+
run: git checkout origin/canary
180+
181+
- name: Install dependencies (canary)
182+
run: pnpm install --frozen-lockfile
183+
184+
- name: Build Next.js (canary)
185+
run: pnpm --filter=next build
186+
187+
- name: Link Next.js (canary)
188+
run: |
189+
cd /tmp/boot-test-app
190+
npm link ${{ github.workspace }}/packages/next
191+
192+
- name: Benchmark canary
193+
id: canary
194+
run: |
195+
cd /tmp/boot-test-app
196+
/tmp/benchmark.sh "${{ github.workspace }}/packages/next/dist/bin/next" /tmp/boot-test-app 5 >> $GITHUB_OUTPUT
197+
198+
# ===== PR BRANCH =====
199+
- name: Checkout PR
200+
run: git checkout ${{ github.sha }}
201+
202+
- name: Install dependencies (PR)
203+
run: pnpm install --frozen-lockfile
204+
205+
- name: Build Next.js (PR)
206+
run: pnpm --filter=next build
207+
208+
- name: Link Next.js (PR)
209+
run: |
210+
cd /tmp/boot-test-app
211+
rm -rf node_modules/next node_modules/.next
212+
npm link ${{ github.workspace }}/packages/next
213+
214+
- name: Benchmark PR
215+
id: pr
216+
run: |
217+
cd /tmp/boot-test-app
218+
/tmp/benchmark.sh "${{ github.workspace }}/packages/next/dist/bin/next" /tmp/boot-test-app 5 >> $GITHUB_OUTPUT
219+
220+
# ===== RESULTS =====
221+
- name: Calculate and report results
222+
run: |
223+
# Canary results
224+
CANARY_LISTEN=${{ steps.canary.outputs.COLD_LISTEN }}
225+
CANARY_READY=${{ steps.canary.outputs.COLD_READY }}
226+
CANARY_WARM_LISTEN=${{ steps.canary.outputs.WARM_LISTEN }}
227+
CANARY_WARM_READY=${{ steps.canary.outputs.WARM_READY }}
228+
229+
# PR results
230+
PR_LISTEN=${{ steps.pr.outputs.COLD_LISTEN }}
231+
PR_READY=${{ steps.pr.outputs.COLD_READY }}
232+
PR_WARM_LISTEN=${{ steps.pr.outputs.WARM_LISTEN }}
233+
PR_WARM_READY=${{ steps.pr.outputs.WARM_READY }}
234+
235+
# Calculate differences
236+
COLD_LISTEN_DIFF=$((PR_LISTEN - CANARY_LISTEN))
237+
COLD_READY_DIFF=$((PR_READY - CANARY_READY))
238+
WARM_LISTEN_DIFF=$((PR_WARM_LISTEN - CANARY_WARM_LISTEN))
239+
WARM_READY_DIFF=$((PR_WARM_READY - CANARY_WARM_READY))
240+
241+
# Status indicators
242+
status() {
243+
if [ $1 -lt -20 ]; then echo "🟢";
244+
elif [ $1 -gt 50 ]; then echo "🔴";
245+
else echo "🟡"; fi
246+
}
247+
248+
COLD_LISTEN_STATUS=$(status $COLD_LISTEN_DIFF)
249+
COLD_READY_STATUS=$(status $COLD_READY_DIFF)
250+
WARM_LISTEN_STATUS=$(status $WARM_LISTEN_DIFF)
251+
WARM_READY_STATUS=$(status $WARM_READY_DIFF)
252+
253+
# Generate summary
254+
cat >> $GITHUB_STEP_SUMMARY << EOF
255+
## Dev Server Boot Time Benchmark
256+
257+
### Cold Start (fresh .next)
258+
| Metric | Canary | PR | Diff |
259+
|--------|--------|-----|------|
260+
| Port listening | ${CANARY_LISTEN}ms | ${PR_LISTEN}ms | ${COLD_LISTEN_STATUS} ${COLD_LISTEN_DIFF}ms |
261+
| First request | ${CANARY_READY}ms | ${PR_READY}ms | ${COLD_READY_STATUS} ${COLD_READY_DIFF}ms |
262+
263+
### Warm Start (with cache)
264+
| Metric | Canary | PR | Diff |
265+
|--------|--------|-----|------|
266+
| Port listening | ${CANARY_WARM_LISTEN}ms | ${PR_WARM_LISTEN}ms | ${WARM_LISTEN_STATUS} ${WARM_LISTEN_DIFF}ms |
267+
| First request | ${CANARY_WARM_READY}ms | ${PR_WARM_READY}ms | ${WARM_READY_STATUS} ${WARM_READY_DIFF}ms |
268+
269+
**Legend:** 🟢 Faster (>20ms improvement) | 🟡 Similar | 🔴 Slower (>50ms regression)
270+
271+
**Metrics:**
272+
- **Port listening**: When server starts accepting TCP connections
273+
- **First request**: When server handles first HTTP request (actual readiness)
274+
EOF
275+
276+
# Log to console
277+
echo "=== RESULTS ==="
278+
echo "Cold: listen ${CANARY_LISTEN}ms→${PR_LISTEN}ms (${COLD_LISTEN_DIFF}ms), ready ${CANARY_READY}ms→${PR_READY}ms (${COLD_READY_DIFF}ms)"
279+
echo "Warm: listen ${CANARY_WARM_LISTEN}ms→${PR_WARM_LISTEN}ms (${WARM_LISTEN_DIFF}ms), ready ${CANARY_WARM_READY}ms→${PR_WARM_READY}ms (${WARM_READY_DIFF}ms)"
280+
281+
- name: Check for regression
282+
run: |
283+
COLD_READY_DIFF=$((${{ steps.pr.outputs.COLD_READY }} - ${{ steps.canary.outputs.COLD_READY }}))
284+
285+
if [ "$COLD_READY_DIFF" -gt 200 ]; then
286+
echo "::error::Cold start regression: ${COLD_READY_DIFF}ms slower (threshold: 200ms)"
287+
exit 1
288+
fi
289+
290+
echo "✅ No significant regression"

0 commit comments

Comments
 (0)