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
60 changes: 41 additions & 19 deletions packages/next/src/build/duration-to-string.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,54 @@
// Time thresholds in seconds
const SECONDS_IN_MINUTE = 60
const MINUTES_THRESHOLD_SECONDS = 120 // 2 minutes
const SECONDS_THRESHOLD_HIGH = 40
const SECONDS_THRESHOLD_LOW = 2
const MILLISECONDS_PER_SECOND = 1000

// Time thresholds and conversion factors for nanoseconds
const NANOSECONDS_PER_SECOND = 1_000_000_000
const NANOSECONDS_PER_MILLISECOND = 1_000_000
const NANOSECONDS_PER_MICROSECOND = 1_000
const NANOSECONDS_IN_MINUTE = 60_000_000_000 // 60 * 1_000_000_000
const MINUTES_THRESHOLD_NANOSECONDS = 120_000_000_000 // 2 minutes in nanoseconds
const SECONDS_THRESHOLD_HIGH_NANOSECONDS = 40_000_000_000 // 40 seconds in nanoseconds
const SECONDS_THRESHOLD_LOW_NANOSECONDS = 2_000_000_000 // 2 seconds in nanoseconds
const MILLISECONDS_THRESHOLD_NANOSECONDS = 1_000_000 // 1 millisecond in nanoseconds

export function durationToString(compilerDuration: number) {
let durationString
if (compilerDuration > 120) {
durationString = `${(compilerDuration / 60).toFixed(1)}min`
} else if (compilerDuration > 40) {
durationString = `${compilerDuration.toFixed(0)}s`
} else if (compilerDuration > 2) {
durationString = `${compilerDuration.toFixed(1)}s`
if (compilerDuration > MINUTES_THRESHOLD_SECONDS) {
return `${(compilerDuration / SECONDS_IN_MINUTE).toFixed(1)}min`
} else if (compilerDuration > SECONDS_THRESHOLD_HIGH) {
return `${compilerDuration.toFixed(0)}s`
} else if (compilerDuration > SECONDS_THRESHOLD_LOW) {
return `${compilerDuration.toFixed(1)}s`
} else {
durationString = `${(compilerDuration * 1000).toFixed(0)}ms`
return `${(compilerDuration * MILLISECONDS_PER_SECOND).toFixed(1)}ms`
Copy link
Member

@sokra sokra Oct 16, 2025

Choose a reason for hiding this comment

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

This needs another MILLISECONDS_THRESHOLD_LOW and HIGH and use toFixed(0) and toFixed(1).

We don't want to see 1784.4ms that's a bit too exact.

MILLISECONDS_THRESHOLD_HIGH = 0.040
MILLISECONDS_THRESHOLD_LOW = 0.002

1784ms
784ms
84ms
4.4ms
412µs
12µs
2µs

}
return durationString
}

export function hrtimeToSeconds(hrtime: [number, number]): number {
// hrtime is a tuple of [seconds, nanoseconds]
return hrtime[0] + hrtime[1] / 1e9
}

function nanosecondsBigIntToSeconds(nanoseconds: bigint): number {
return Number(nanoseconds) / 1000000000
function durationToStringWithNanoseconds(durationBigInt: bigint): string {
const duration = Number(durationBigInt)
if (duration > MINUTES_THRESHOLD_NANOSECONDS) {
return `${(duration / NANOSECONDS_IN_MINUTE).toFixed(1)}min`
} else if (duration > SECONDS_THRESHOLD_HIGH_NANOSECONDS) {
return `${(duration / NANOSECONDS_PER_SECOND).toFixed(0)}s`
} else if (duration > SECONDS_THRESHOLD_LOW_NANOSECONDS) {
return `${(duration / NANOSECONDS_PER_SECOND).toFixed(1)}s`
} else if (duration > MILLISECONDS_THRESHOLD_NANOSECONDS) {
return `${(duration / NANOSECONDS_PER_MILLISECOND).toFixed(0)}ms`
} else {
return `${(duration / NANOSECONDS_PER_MICROSECOND).toFixed(0)}µs`
}
Comment on lines +32 to +42
Copy link
Member

Choose a reason for hiding this comment

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

I think this could call durationToString and durationToString could be extended to µs

}

function hrtimeBigIntToSeconds(hrtime: bigint): number {
return nanosecondsBigIntToSeconds(hrtime)
export function hrtimeToSeconds(hrtime: [number, number]): number {
// hrtime is a tuple of [seconds, nanoseconds]
return hrtime[0] + hrtime[1] / NANOSECONDS_PER_SECOND
}

export function hrtimeBigIntDurationToString(hrtime: bigint) {
return durationToString(hrtimeBigIntToSeconds(hrtime))
return durationToStringWithNanoseconds(hrtime)
}

export function hrtimeDurationToString(hrtime: [number, number]): string {
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/build/templates/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RouteKind } from '../../server/route-kind' with { 'turbopack-transition

import { getRevalidateReason } from '../../server/instrumentation/utils'
import { getTracer, SpanKind, type Span } from '../../server/lib/trace/tracer'
import { getRequestMeta } from '../../server/request-meta'
import { addRequestMeta, getRequestMeta } from '../../server/request-meta'
import { BaseServerSpan } from '../../server/lib/trace/constants'
import { interopDefault } from '../../server/app-render/interop-default'
import { stripFlightHeaders } from '../../server/app-render/strip-flight-headers'
Expand Down Expand Up @@ -118,6 +118,9 @@ export async function handler(
waitUntil: (prom: Promise<void>) => void
}
) {
if (routeModule.isDev) {
addRequestMeta(req, 'devRequestTimingInternalsEnd', process.hrtime.bigint())
}
let srcPage = 'VAR_DEFINITION_PAGE'

// turbopack doesn't normalize `/index` in the page name
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/build/templates/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { RouteKind } from '../../server/route-kind'
import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { getRequestMeta } from '../../server/request-meta'
import { addRequestMeta, getRequestMeta } from '../../server/request-meta'
import { getTracer, type Span, SpanKind } from '../../server/lib/trace/tracer'
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
import { createServerModuleMap } from '../../server/app-render/action-utils'
Expand Down Expand Up @@ -85,6 +85,9 @@ export async function handler(
waitUntil: (prom: Promise<void>) => void
}
) {
if (routeModule.isDev) {
addRequestMeta(req, 'devRequestTimingInternalsEnd', process.hrtime.bigint())
}
let srcPage = 'VAR_DEFINITION_PAGE'

// turbopack doesn't normalize `/index` in the page name
Expand Down
42 changes: 38 additions & 4 deletions packages/next/src/server/dev/log-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
red,
white,
yellow,
dim,
} from '../../lib/picocolors'
import { stripNextRscUnionQuery } from '../../lib/url'
import type { FetchMetric } from '../base-http'
Expand Down Expand Up @@ -43,14 +44,18 @@ export function logRequests(
response: NodeNextResponse,
loggingConfig: LoggingConfig,
requestStartTime: bigint,
requestEndTime: bigint
requestEndTime: bigint,
devRequestTimingMiddlewareStart: bigint | undefined,
devRequestTimingMiddlewareEnd: bigint | undefined
): void {
if (!ignoreLoggingIncomingRequests(request, loggingConfig)) {
logIncomingRequests(
request,
requestStartTime,
requestEndTime,
response.statusCode
response.statusCode,
devRequestTimingMiddlewareStart,
devRequestTimingMiddlewareEnd
)
}

Expand All @@ -65,9 +70,15 @@ function logIncomingRequests(
request: NodeNextRequest,
requestStartTime: bigint,
requestEndTime: bigint,
statusCode: number
statusCode: number,
devRequestTimingMiddlewareStart: bigint | undefined,
devRequestTimingMiddlewareEnd: bigint | undefined
): void {
const isRSC = getRequestMeta(request, 'isRSCRequest')
const devRequestTimingInternalsEnd = getRequestMeta(
request,
'devRequestTimingInternalsEnd'
)
Comment on lines +74 to +81
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit inconsistent that devRequestTimingInternalsEnd is grabbed from the request meta, but devRequestTimingMiddlewareStart/End are passed as argument. They could also be grabbed from the request meta...

const url = isRSC ? stripNextRscUnionQuery(request.url) : request.url

const statusCodeColor =
Expand All @@ -83,8 +94,31 @@ function logIncomingRequests(

const coloredStatus = statusCodeColor(statusCode.toString())

const totalRequestTime = requestEndTime - requestStartTime

const times: Array<[label: string, time: bigint]> = []

let middlewareTime: bigint | undefined
if (devRequestTimingMiddlewareStart && devRequestTimingMiddlewareEnd) {
middlewareTime =
devRequestTimingMiddlewareEnd - devRequestTimingMiddlewareStart
times.push(['proxy.ts', middlewareTime])
Copy link
Member

Choose a reason for hiding this comment

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

It could be a proxy.js file too, couldn't it? Maybe only use proxy instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

We discussed this with @feedthejim but it's a bit confusing until the middleware -> proxy rename is a bit further along.

}

if (devRequestTimingInternalsEnd) {
let frameworkTime = devRequestTimingInternalsEnd - requestStartTime

/* Middleware runs during the internals so we have to subtract it from the framework time */
if (middlewareTime) {
frameworkTime -= middlewareTime
}
// Insert as the first item to be rendered in the list
times.unshift(['compile', frameworkTime])
times.push(['render', requestEndTime - devRequestTimingInternalsEnd])
}

return writeLine(
`${request.method} ${url} ${coloredStatus} in ${hrtimeBigIntDurationToString(requestEndTime - requestStartTime)}`
`${request.method} ${url} ${coloredStatus} in ${hrtimeBigIntDurationToString(totalRequestTime)}${times.length > 0 ? dim(` (${times.map(([label, time]) => `${label}: ${hrtimeBigIntDurationToString(time)}`).join(', ')})`) : ''}`
)
}

Expand Down
20 changes: 17 additions & 3 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,14 @@ export default class DevServer extends Server {
const loggingConfig = this.nextConfig.logging

if (loggingConfig !== false) {
const requestStart = process.hrtime.bigint()
const isMiddlewareRequest = getRequestMeta(req, 'middlewareInvoke')
// The closure variable is not used here because the request handler may be invoked twice for one request when middleware is added in the application.
// By setting the start time we can ensure that the middleware timing is correctly included.
if (!getRequestMeta(req, 'devRequestTimingStart')) {
const requestStart = process.hrtime.bigint()
addRequestMeta(req, 'devRequestTimingStart', requestStart)
}
const isMiddlewareRequest =
getRequestMeta(req, 'middlewareInvoke') ?? false

if (!isMiddlewareRequest) {
response.originalResponse.once('close', () => {
Expand All @@ -480,13 +486,21 @@ export default class DevServer extends Server {
return
}

// The closure variable is not used here because the request handler may be invoked twice for one request when middleware is added in the application.
// By setting the start time we can ensure that the middleware timing is correctly included.
const requestStart = getRequestMeta(req, 'devRequestTimingStart')
if (!requestStart) {
return
}
const requestEnd = process.hrtime.bigint()
logRequests(
request,
response,
loggingConfig,
requestStart,
requestEnd
requestEnd,
getRequestMeta(req, 'devRequestTimingMiddlewareStart'),
getRequestMeta(req, 'devRequestTimingMiddlewareEnd')
)
})
}
Expand Down
15 changes: 15 additions & 0 deletions packages/next/src/server/lib/router-utils/resolve-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,13 @@ export function getResolveRoutes(
addRequestMeta(req, 'invokeOutput', '')
addRequestMeta(req, 'invokeQuery', {})
addRequestMeta(req, 'middlewareInvoke', true)
if (opts.dev) {
addRequestMeta(
req,
'devRequestTimingMiddlewareStart',
process.hrtime.bigint()
)
}
debug('invoking middleware', req.url, req.headers)

let middlewareRes: Response | undefined = undefined
Expand All @@ -543,6 +550,14 @@ export function getResolveRoutes(
},
})
}
} finally {
if (opts.dev) {
addRequestMeta(
req,
'devRequestTimingMiddlewareEnd',
process.hrtime.bigint()
)
}
}
} catch (e) {
// If the client aborts before we can receive a response object
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/request-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ export interface RequestMeta {
* DEV only: The fallback params that should be used when validating prerenders during dev
*/
devValidatingFallbackParams?: OpaqueFallbackRouteParams

/**
* DEV only: Request timings in process.hrtime.bigint()
*/
devRequestTimingStart?: bigint
devRequestTimingMiddlewareStart?: bigint
devRequestTimingMiddlewareEnd?: bigint
devRequestTimingInternalsEnd?: bigint
}

/**
Expand Down
Loading