Skip to content

Commit 24489f4

Browse files
committed
Transfer Flight debug info to derived promises
The `_debugInfo` field contains profiling information. Promises that are created by Flight already have this info added by React; for any derived promise created by the router, we need to transfer the Flight debug info onto the derived promise. The debug info represents the latency between the start of the navigation and the start of rendering. (It does not represent the time it takes for whole stream to finish.) Concretely, we create a derived promise for each route segment contained in a response. Any nested promises contained with that segment are passed directly from Flight to React and don't require special processing.
1 parent e7fa1a9 commit 24489f4

File tree

4 files changed

+80
-33
lines changed

4 files changed

+80
-33
lines changed

packages/next/src/client/components/router-reducer/create-initial-router-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export function createInitialRouterState({
145145
prerendered && !process.env.__NEXT_CLIENT_SEGMENT_CACHE
146146
? STATIC_STALETIME_MS
147147
: -1,
148+
debugInfo: null,
148149
},
149150
tree: initialState.tree,
150151
prefetchCache: initialState.prefetchCache,

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export type FetchServerResponseResult = {
6666
prerendered: boolean
6767
postponed: boolean
6868
staleTime: number
69+
debugInfo: Array<any> | null
6970
}
7071

7172
export type RequestHeaders = {
@@ -91,6 +92,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
9192
prerendered: false,
9293
postponed: false,
9394
staleTime: -1,
95+
debugInfo: null,
9496
}
9597
}
9698

@@ -263,6 +265,7 @@ export async function fetchServerResponse(
263265
prerendered: flightResponse.S,
264266
postponed,
265267
staleTime,
268+
debugInfo: flightResponsePromise._debugInfo ?? null,
266269
}
267270
} catch (err) {
268271
if (!abortController.signal.aborted) {
@@ -282,6 +285,7 @@ export async function fetchServerResponse(
282285
prerendered: false,
283286
postponed: false,
284287
staleTime: -1,
288+
debugInfo: null,
285289
}
286290
}
287291
}

packages/next/src/client/components/router-reducer/ppr-navigations.ts

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ export function listenForDynamicRequest(
757757
responsePromise: Promise<FetchServerResponseResult>
758758
) {
759759
responsePromise.then(
760-
({ flightData }: FetchServerResponseResult) => {
760+
({ flightData, debugInfo }: FetchServerResponseResult) => {
761761
if (typeof flightData === 'string') {
762762
// Happens when navigating to page in `pages` from `app`. We shouldn't
763763
// get here because should have already handled this during
@@ -784,18 +784,19 @@ export function listenForDynamicRequest(
784784
segmentPath,
785785
serverRouterState,
786786
dynamicData,
787-
dynamicHead
787+
dynamicHead,
788+
debugInfo
788789
)
789790
}
790791

791792
// Now that we've exhausted all the data we received from the server, if
792793
// there are any remaining pending tasks in the tree, abort them now.
793794
// If there's any missing data, it will trigger a lazy fetch.
794-
abortTask(task, null)
795+
abortTask(task, null, debugInfo)
795796
},
796797
(error: any) => {
797798
// This will trigger an error during render
798-
abortTask(task, error)
799+
abortTask(task, error, null)
799800
}
800801
)
801802
}
@@ -805,7 +806,8 @@ function writeDynamicDataIntoPendingTask(
805806
segmentPath: FlightSegmentPath,
806807
serverRouterState: FlightRouterState,
807808
dynamicData: CacheNodeSeedData,
808-
dynamicHead: HeadData
809+
dynamicHead: HeadData,
810+
debugInfo: Array<any> | null
809811
) {
810812
// The data sent by the server represents only a subtree of the app. We need
811813
// to find the part of the task tree that matches the server response, and
@@ -844,15 +846,17 @@ function writeDynamicDataIntoPendingTask(
844846
task,
845847
serverRouterState,
846848
dynamicData,
847-
dynamicHead
849+
dynamicHead,
850+
debugInfo
848851
)
849852
}
850853

851854
function finishTaskUsingDynamicDataPayload(
852855
task: SPANavigationTask,
853856
serverRouterState: FlightRouterState,
854857
dynamicData: CacheNodeSeedData,
855-
dynamicHead: HeadData
858+
dynamicHead: HeadData,
859+
debugInfo: Array<any> | null
856860
) {
857861
if (task.dynamicRequestTree === null) {
858862
// Everything in this subtree is already complete. Bail out.
@@ -873,7 +877,8 @@ function finishTaskUsingDynamicDataPayload(
873877
task.route,
874878
serverRouterState,
875879
dynamicData,
876-
dynamicHead
880+
dynamicHead,
881+
debugInfo
877882
)
878883
// Set this to null to indicate that this task is now complete.
879884
task.dynamicRequestTree = null
@@ -904,7 +909,8 @@ function finishTaskUsingDynamicDataPayload(
904909
taskChild,
905910
serverRouterStateChild,
906911
dynamicDataChild,
907-
dynamicHead
912+
dynamicHead,
913+
debugInfo
908914
)
909915
}
910916
}
@@ -1004,7 +1010,8 @@ function finishPendingCacheNode(
10041010
taskState: FlightRouterState,
10051011
serverState: FlightRouterState,
10061012
dynamicData: CacheNodeSeedData,
1007-
dynamicHead: HeadData
1013+
dynamicHead: HeadData,
1014+
debugInfo: Array<any> | null
10081015
): void {
10091016
// Writes a dynamic response into an existing Cache Node tree. This does _not_
10101017
// create a new tree, it updates the existing tree in-place. So it must follow
@@ -1053,19 +1060,20 @@ function finishPendingCacheNode(
10531060
taskStateChild,
10541061
serverStateChild,
10551062
dataChild,
1056-
dynamicHead
1063+
dynamicHead,
1064+
debugInfo
10571065
)
10581066
} else {
10591067
// The server never returned data for this segment. Trigger a lazy
10601068
// fetch during render. This shouldn't happen because the Route Tree
10611069
// and the Seed Data tree sent by the server should always be the same
10621070
// shape when part of the same server response.
1063-
abortPendingCacheNode(taskStateChild, cacheNodeChild, null)
1071+
abortPendingCacheNode(taskStateChild, cacheNodeChild, null, debugInfo)
10641072
}
10651073
} else {
10661074
// The server never returned data for this segment. Trigger a lazy
10671075
// fetch during render.
1068-
abortPendingCacheNode(taskStateChild, cacheNodeChild, null)
1076+
abortPendingCacheNode(taskStateChild, cacheNodeChild, null, debugInfo)
10691077
}
10701078
} else {
10711079
// The server response matches what was expected to receive, but there's
@@ -1087,7 +1095,7 @@ function finishPendingCacheNode(
10871095
// This is a deferred RSC promise. We can fulfill it with the data we just
10881096
// received from the server. If it was already resolved by a different
10891097
// navigation, then this does nothing because we can't overwrite data.
1090-
rsc.resolve(dynamicSegmentData)
1098+
rsc.resolve(dynamicSegmentData, debugInfo)
10911099
} else {
10921100
// This is not a deferred RSC promise, nor is it empty, so it must have
10931101
// been populated by a different navigation. We must not overwrite it.
@@ -1098,19 +1106,23 @@ function finishPendingCacheNode(
10981106
const loading = cacheNode.loading
10991107
if (isDeferredRsc(loading)) {
11001108
const dynamicLoading = dynamicData[3]
1101-
loading.resolve(dynamicLoading)
1109+
loading.resolve(dynamicLoading, debugInfo)
11021110
}
11031111

11041112
// Check if this is a leaf segment. If so, it will have a `head` property with
11051113
// a pending promise that needs to be resolved with the dynamic head from
11061114
// the server.
11071115
const head = cacheNode.head
11081116
if (isDeferredRsc(head)) {
1109-
head.resolve(dynamicHead)
1117+
head.resolve(dynamicHead, debugInfo)
11101118
}
11111119
}
11121120

1113-
export function abortTask(task: SPANavigationTask, error: any): void {
1121+
export function abortTask(
1122+
task: SPANavigationTask,
1123+
error: any,
1124+
debugInfo: Array<any> | null
1125+
): void {
11141126
const cacheNode = task.node
11151127
if (cacheNode === null) {
11161128
// This indicates the task is already complete.
@@ -1121,13 +1133,13 @@ export function abortTask(task: SPANavigationTask, error: any): void {
11211133
if (taskChildren === null) {
11221134
// Reached the leaf task node. This is the root of a pending cache
11231135
// node tree.
1124-
abortPendingCacheNode(task.route, cacheNode, error)
1136+
abortPendingCacheNode(task.route, cacheNode, error, debugInfo)
11251137
} else {
11261138
// This is an intermediate task node. Keep traversing until we reach a
11271139
// task node with no children. That will be the root of the cache node tree
11281140
// that needs to be resolved.
11291141
for (const taskChild of taskChildren.values()) {
1130-
abortTask(taskChild, error)
1142+
abortTask(taskChild, error, debugInfo)
11311143
}
11321144
}
11331145

@@ -1138,7 +1150,8 @@ export function abortTask(task: SPANavigationTask, error: any): void {
11381150
function abortPendingCacheNode(
11391151
routerState: FlightRouterState,
11401152
cacheNode: CacheNode,
1141-
error: any
1153+
error: any,
1154+
debugInfo: Array<any> | null
11421155
): void {
11431156
// For every pending segment in the tree, resolve its `rsc` promise to `null`
11441157
// to trigger a lazy fetch during render.
@@ -1159,7 +1172,7 @@ function abortPendingCacheNode(
11591172
const segmentKeyChild = createRouterCacheKey(segmentChild)
11601173
const cacheNodeChild = segmentMapChild.get(segmentKeyChild)
11611174
if (cacheNodeChild !== undefined) {
1162-
abortPendingCacheNode(routerStateChild, cacheNodeChild, error)
1175+
abortPendingCacheNode(routerStateChild, cacheNodeChild, error, debugInfo)
11631176
} else {
11641177
// This shouldn't happen because we're traversing the same tree that was
11651178
// used to construct the cache nodes in the first place.
@@ -1170,16 +1183,16 @@ function abortPendingCacheNode(
11701183
if (isDeferredRsc(rsc)) {
11711184
if (error === null) {
11721185
// This will trigger a lazy fetch during render.
1173-
rsc.resolve(null)
1186+
rsc.resolve(null, debugInfo)
11741187
} else {
11751188
// This will trigger an error during rendering.
1176-
rsc.reject(error)
1189+
rsc.reject(error, debugInfo)
11771190
}
11781191
}
11791192

11801193
const loading = cacheNode.loading
11811194
if (isDeferredRsc(loading)) {
1182-
loading.resolve(null)
1195+
loading.resolve(null, debugInfo)
11831196
}
11841197

11851198
// Check if this is a leaf segment. If so, it will have a `head` property with
@@ -1188,7 +1201,7 @@ function abortPendingCacheNode(
11881201
// the app. We want the segment to error, not the entire app.
11891202
const head = cacheNode.head
11901203
if (isDeferredRsc(head)) {
1191-
head.resolve(null)
1204+
head.resolve(null, debugInfo)
11921205
}
11931206
}
11941207

@@ -1260,25 +1273,28 @@ const DEFERRED = Symbol()
12601273

12611274
type PendingDeferredRsc<T> = Promise<T> & {
12621275
status: 'pending'
1263-
resolve: (value: T) => void
1264-
reject: (error: any) => void
1276+
resolve: (value: T, debugInfo: Array<any> | null) => void
1277+
reject: (error: any, debugInfo: Array<any> | null) => void
12651278
tag: Symbol
1279+
_debugInfo: Array<any>
12661280
}
12671281

12681282
type FulfilledDeferredRsc<T> = Promise<T> & {
12691283
status: 'fulfilled'
12701284
value: T
1271-
resolve: (value: T) => void
1272-
reject: (error: any) => void
1285+
resolve: (value: T, debugInfo: Array<any> | null) => void
1286+
reject: (error: any, debugInfo: Array<any> | null) => void
12731287
tag: Symbol
1288+
_debugInfo: Array<any>
12741289
}
12751290

12761291
type RejectedDeferredRsc<T> = Promise<T> & {
12771292
status: 'rejected'
12781293
reason: any
1279-
resolve: (value: T) => void
1280-
reject: (error: any) => void
1294+
resolve: (value: T, debugInfo: Array<any> | null) => void
1295+
reject: (error: any, debugInfo: Array<any> | null) => void
12811296
tag: Symbol
1297+
_debugInfo: Array<any>
12821298
}
12831299

12841300
type DeferredRsc<T extends React.ReactNode = React.ReactNode> =
@@ -1297,29 +1313,54 @@ function isDeferredRsc(value: any): value is DeferredRsc {
12971313
function createDeferredRsc<
12981314
T extends React.ReactNode = React.ReactNode,
12991315
>(): PendingDeferredRsc<T> {
1316+
// Create an unresolved promise that represents data derived from a Flight
1317+
// response. The promise will be resolved later as soon as we start receiving
1318+
// data from the server, i.e. as soon as the Flight client decodes and returns
1319+
// the top-level response object.
1320+
1321+
// The `_debugInfo` field contains profiling information. Promises that are
1322+
// created by Flight already have this info added by React; for any derived
1323+
// promise created by the router, we need to transfer the Flight debug info
1324+
// onto the derived promise.
1325+
//
1326+
// The debug info represents the latency between the start of the navigation
1327+
// and the start of rendering. (It does not represent the time it takes for
1328+
// whole stream to finish.)
1329+
const debugInfo: Array<any> = []
1330+
13001331
let resolve: any
13011332
let reject: any
13021333
const pendingRsc = new Promise<T>((res, rej) => {
13031334
resolve = res
13041335
reject = rej
13051336
}) as PendingDeferredRsc<T>
13061337
pendingRsc.status = 'pending'
1307-
pendingRsc.resolve = (value: T) => {
1338+
pendingRsc.resolve = (value: T, responseDebugInfo: Array<any> | null) => {
13081339
if (pendingRsc.status === 'pending') {
13091340
const fulfilledRsc: FulfilledDeferredRsc<T> = pendingRsc as any
13101341
fulfilledRsc.status = 'fulfilled'
13111342
fulfilledRsc.value = value
1343+
if (responseDebugInfo !== null) {
1344+
// Transfer the debug info to the derived promise.
1345+
debugInfo.push.apply(debugInfo, responseDebugInfo)
1346+
}
13121347
resolve(value)
13131348
}
13141349
}
1315-
pendingRsc.reject = (error: any) => {
1350+
pendingRsc.reject = (error: any, responseDebugInfo: Array<any> | null) => {
13161351
if (pendingRsc.status === 'pending') {
13171352
const rejectedRsc: RejectedDeferredRsc<T> = pendingRsc as any
13181353
rejectedRsc.status = 'rejected'
13191354
rejectedRsc.reason = error
1355+
if (responseDebugInfo !== null) {
1356+
// Transfer the debug info to the derived promise.
1357+
debugInfo.push.apply(debugInfo, responseDebugInfo)
1358+
}
13201359
reject(error)
13211360
}
13221361
}
13231362
pendingRsc.tag = DEFERRED
1363+
pendingRsc._debugInfo = debugInfo
1364+
13241365
return pendingRsc
13251366
}

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ export function serverActionReducer(
420420
// TODO: We should be able to set this if the server action
421421
// returned a fully static response.
422422
staleTime: -1,
423+
debugInfo: null,
423424
},
424425
tree: state.tree,
425426
prefetchCache: state.prefetchCache,

0 commit comments

Comments
 (0)