11const path = require ( 'path' )
2+ const net = require ( 'net' )
23const fs = require ( 'fs/promises' )
34const getPort = require ( 'get-port' )
45const fetch = require ( 'node-fetch' )
@@ -10,6 +11,108 @@ const { parse: urlParse } = require('url')
1011const benchmarkUrl = require ( './benchmark-url' )
1112const { statsAppDir, diffingDir, benchTitle } = require ( '../constants' )
1213
14+ // Check if a port is accepting TCP connections
15+ function checkPort ( port , timeout = 100 ) {
16+ return new Promise ( ( resolve ) => {
17+ const socket = new net . Socket ( )
18+ socket . setTimeout ( timeout )
19+ socket . once ( 'connect' , ( ) => {
20+ socket . destroy ( )
21+ resolve ( true )
22+ } )
23+ socket . once ( 'timeout' , ( ) => {
24+ socket . destroy ( )
25+ resolve ( false )
26+ } )
27+ socket . once ( 'error' , ( ) => {
28+ socket . destroy ( )
29+ resolve ( false )
30+ } )
31+ socket . connect ( port , 'localhost' )
32+ } )
33+ }
34+
35+ // Wait for port to start accepting TCP connections
36+ async function waitForPort ( port , timeoutMs = 60000 ) {
37+ const start = Date . now ( )
38+ while ( Date . now ( ) - start < timeoutMs ) {
39+ if ( await checkPort ( port ) ) {
40+ return Date . now ( ) - start
41+ }
42+ await new Promise ( ( r ) => setTimeout ( r , 50 ) )
43+ }
44+ return null
45+ }
46+
47+ // Wait for HTTP server to respond
48+ async function waitForHttp ( port , timeoutMs = 60000 ) {
49+ const start = Date . now ( )
50+ while ( Date . now ( ) - start < timeoutMs ) {
51+ try {
52+ const res = await fetch ( `http://localhost:${ port } /` , { timeout : 2000 } )
53+ if ( res . ok ) {
54+ return Date . now ( ) - start
55+ }
56+ } catch ( e ) {
57+ // Server not ready yet
58+ }
59+ await new Promise ( ( r ) => setTimeout ( r , 50 ) )
60+ }
61+ return null
62+ }
63+
64+ // Run a single dev server boot benchmark
65+ async function benchmarkDevBoot ( appDevCommand , curDir , port , cleanBuild ) {
66+ // Clean .next directory for cold start
67+ if ( cleanBuild ) {
68+ const nextDir = path . join ( curDir , '.next' )
69+ await fs . rm ( nextDir , { recursive : true , force : true } )
70+ }
71+
72+ const startTime = Date . now ( )
73+ const devChild = spawn ( appDevCommand , {
74+ cwd : curDir ,
75+ env : {
76+ PORT : port ,
77+ } ,
78+ stdio : 'pipe' ,
79+ } )
80+
81+ let exited = false
82+ devChild . on ( 'exit' , ( ) => {
83+ exited = true
84+ } )
85+
86+ // Capture output for debugging
87+ devChild . stdout . on ( 'data' , ( data ) => process . stdout . write ( data ) )
88+ devChild . stderr . on ( 'data' , ( data ) => process . stderr . write ( data ) )
89+
90+ // Measure time to port listening (TCP level)
91+ const listenTime = await waitForPort ( port , 60000 )
92+
93+ // Measure time to HTTP ready
94+ let readyTime = null
95+ if ( listenTime !== null && ! exited ) {
96+ readyTime = await waitForHttp ( port , 60000 )
97+ }
98+
99+ devChild . kill ( )
100+
101+ // Wait for process to fully exit to avoid port conflicts on subsequent runs
102+ if ( ! exited ) {
103+ await new Promise ( ( resolve ) => {
104+ devChild . on ( 'exit' , resolve )
105+ // Timeout after 5 seconds in case process doesn't exit cleanly
106+ setTimeout ( resolve , 5000 )
107+ } )
108+ }
109+
110+ return {
111+ listenTime,
112+ readyTime,
113+ }
114+ }
115+
13116async function defaultGetRequiredFiles ( nextAppDir , fileName ) {
14117 return [ fileName ]
15118}
@@ -33,6 +136,7 @@ module.exports = async function collectStats(
33136 const hasPagesToBench =
34137 Array . isArray ( runConfig . pagesToBench ) && runConfig . pagesToBench . length > 0
35138
139+ // Run production start benchmark FIRST (before dev benchmark which cleans .next)
36140 if (
37141 ! fromDiff &&
38142 statsConfig . appStartCommand &&
@@ -139,6 +243,81 @@ module.exports = async function collectStats(
139243 child . kill ( )
140244 }
141245
246+ // Measure dev server boot time if configured (full matrix: cold/warm x listen/ready)
247+ // NOTE: This runs AFTER the production start benchmark because it cleans the .next directory
248+ if ( ! fromDiff && statsConfig . appDevCommand && statsConfig . measureDevBoot ) {
249+ const devPort = await getPort ( )
250+
251+ if ( ! orderedStats [ 'General' ] ) {
252+ orderedStats [ 'General' ] = { }
253+ }
254+
255+ // 1. Cold start benchmark (clean .next directory)
256+ logger ( '=== Cold Start Benchmark ===' )
257+ const coldResult = await benchmarkDevBoot (
258+ statsConfig . appDevCommand ,
259+ curDir ,
260+ devPort ,
261+ true // clean build
262+ )
263+
264+ if ( coldResult . listenTime !== null ) {
265+ orderedStats [ 'General' ] [ 'nextDevColdListenDuration (ms)' ] =
266+ coldResult . listenTime
267+ }
268+ if ( coldResult . readyTime !== null ) {
269+ orderedStats [ 'General' ] [ 'nextDevColdReadyDuration (ms)' ] =
270+ coldResult . readyTime
271+ }
272+
273+ // 2. Warm up bytecode cache by running server for ~10 seconds
274+ if ( coldResult . readyTime !== null ) {
275+ logger ( '=== Warming up bytecode cache (10s) ===' )
276+ const warmupChild = spawn ( statsConfig . appDevCommand , {
277+ cwd : curDir ,
278+ env : {
279+ PORT : devPort ,
280+ } ,
281+ stdio : 'pipe' ,
282+ } )
283+
284+ // Wait for server to be ready
285+ await waitForHttp ( devPort , 60000 )
286+
287+ // Let it run for 10 seconds to warm bytecode cache
288+ await new Promise ( ( r ) => setTimeout ( r , 10000 ) )
289+
290+ warmupChild . kill ( )
291+
292+ // Wait for warmup server to fully exit to avoid port conflicts
293+ await new Promise ( ( resolve ) => {
294+ warmupChild . on ( 'exit' , resolve )
295+ // Timeout after 5 seconds in case process doesn't exit cleanly
296+ setTimeout ( resolve , 5000 )
297+ } )
298+
299+ // 3. Warm start benchmark (keep .next directory)
300+ logger ( '=== Warm Start Benchmark ===' )
301+ const warmResult = await benchmarkDevBoot (
302+ statsConfig . appDevCommand ,
303+ curDir ,
304+ devPort ,
305+ false // keep build
306+ )
307+
308+ if ( warmResult . listenTime !== null ) {
309+ orderedStats [ 'General' ] [ 'nextDevWarmListenDuration (ms)' ] =
310+ warmResult . listenTime
311+ }
312+ if ( warmResult . readyTime !== null ) {
313+ orderedStats [ 'General' ] [ 'nextDevWarmReadyDuration (ms)' ] =
314+ warmResult . readyTime
315+ }
316+ }
317+
318+ logger ( '=== Dev Boot Benchmark Complete ===' )
319+ }
320+
142321 for ( const fileGroup of runConfig . filesToTrack ) {
143322 const {
144323 getRequiredFiles = defaultGetRequiredFiles ,
0 commit comments