Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -3609,10 +3609,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3995,10 +3995,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3566,10 +3566,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3952,10 +3952,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3609,10 +3609,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3995,10 +3995,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3566,10 +3566,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3952,10 +3952,8 @@
}
function markOperationEndTime(request, task, timestamp) {
(request.status === ABORTING && timestamp > request.abortTime) ||
(timestamp > task.time
? (emitTimingChunk(request, task.id, timestamp),
(task.time = timestamp))
: emitTimingChunk(request, task.id, task.time));
(emitTimingChunk(request, task.id, timestamp),
timestamp > task.time && (task.time = timestamp));
}
function emitChunk(request, task, value) {
var id = task.id;
Expand Down
24 changes: 17 additions & 7 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ import { ResponseCookies } from '../web/spec-extension/cookies'
import { isInstantValidationError } from './instant-validation/instant-validation-error'
import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers'
import { RENDER_STAGES_BY_DATA_KIND } from '../dynamic-rendering-utils'
import type { StageEndTimes } from './instant-validation/instant-validation'

export type GetDynamicParamFromSegment = (
// The LoaderTree to extract the dynamic param from
Expand Down Expand Up @@ -5560,6 +5561,10 @@ async function spawnStaticShellValidationInDevImpl(
accumulatedChunks,
debugChunks,
startTime,
{
[RenderStage.Static]: staticStageEndTime,
[RenderStage.Runtime]: runtimeStageEndTime,
},
rootParams,
fallbackRouteParams,
ctx,
Expand Down Expand Up @@ -5920,6 +5925,7 @@ async function validateInstantConfigs(
accumulatedChunks: AccumulatedStreamChunks,
debugChunks: null | Array<Uint8Array>,
startTime: number,
endTimes: StageEndTimes,
rootParams: Params,
fallbackRouteParams: OpaqueFallbackRouteParams | null,
ctx: AppRenderContext,
Expand Down Expand Up @@ -5956,11 +5962,7 @@ async function validateInstantConfigs(
? createNodeDebugChannel
: createWebDebugChannel

const {
cache,
payload: initialRscPayload,
stageEndTimes,
} = await collectStagedSegmentData(
const { cache, payload: initialRscPayload } = await collectStagedSegmentData(
ctx.componentMod,
renderFlightStream,
{
Expand Down Expand Up @@ -6013,7 +6015,7 @@ async function validateInstantConfigs(
extraChunksController.signal,
boundaryState,
clientReferenceManifest,
stageEndTimes,
endTimes,
useRuntimeStageForPartialSegments
)

Expand Down Expand Up @@ -6081,7 +6083,7 @@ async function validateInstantConfigs(
<App
reactServerStream={serverStream}
reactDebugStream={debugStream ?? undefined}
debugEndTime={undefined}
debugEndTime={payloadResult.endTime}
preinitScripts={preinitScripts}
ServerInsertedHTMLProvider={ServerInsertedHTMLProvider}
nonce={nonce}
Expand Down Expand Up @@ -6963,6 +6965,14 @@ async function validateInstantConfigInBuildWithSample(
accumulatedChunks,
debugChunks,
startTime,
{
[RenderStage.Static]: stageController.getStageEndTime(
RenderStage.Static
),
[RenderStage.Runtime]: stageController.getStageEndTime(
RenderStage.Runtime
),
},
sampleRootParams,
fallbackRouteParams,
validationCtx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,6 @@ export async function collectStagedSegmentData(
const cache = createSegmentCache()
const pendingTasks: Promise<void>[] = []

/** Track when we advance stages so we can pass them as `endTime` later. */
const stageEndTimes: StageEndTimes = {
[RenderStage.Static]: Infinity,
[RenderStage.Runtime]: Infinity,
}

const renderIntoCacheItem = async (
data: HeadData | SegmentData,
cacheEntry: SegmentCacheItem
Expand Down Expand Up @@ -370,16 +364,6 @@ export async function collectStagedSegmentData(
])
}

const advanceStage = (
targetStage: Exclude<SegmentStage, RenderStage.Static>
) => {
const { currentStage } = controller
if (currentStage !== RenderStage.Dynamic) {
stageEndTimes[currentStage] = performance.now() + performance.timeOrigin
}
controller.advanceStage(targetStage)
}

await runInSequentialTasks(
() => {
{
Expand All @@ -394,12 +378,12 @@ export async function collectStagedSegmentData(
pendingTasks.push(renderIntoCacheItem(segmentData, segmentCacheItem))
}
},
() => advanceStage(RenderStage.Runtime),
() => advanceStage(RenderStage.Dynamic)
() => controller.advanceStage(RenderStage.Runtime),
() => controller.advanceStage(RenderStage.Dynamic)
)
await Promise.all(pendingTasks)

return { cache, payload, stageEndTimes }
return { cache, payload }
}

/**
Expand Down Expand Up @@ -894,6 +878,7 @@ export function discoverValidationDepths(loaderTree: LoaderTree): number[] {
*/
export type ValidationPayloadResult = {
payload: InitialRSCPayload
endTime: number
/** Whether errors from this payload could be ambiguous between runtime
* API access (cookies, headers) and uncached IO (connection, fetch).
* True when some segments used Static stage. False when all segments
Expand Down Expand Up @@ -1343,8 +1328,13 @@ export async function createCombinedPayloadAtDepth(
f: [[flightRouterState, seedData, head]],
}

const combinedEndTime = hasRuntimeSegments
? stageEndTimes[RenderStage.Runtime]
: stageEndTimes[RenderStage.Static]

return {
payload,
endTime: combinedEndTime,
hasAmbiguousErrors: hasStaticSegments,
slotStacks,
}
Expand Down
15 changes: 7 additions & 8 deletions packages/next/src/server/app-render/staged-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,17 @@ export class StagedRenderingController {
return this.syncInterruptReason
}

getStageEndTime(stage: Exclude<AdvanceableRenderStage, RenderStage.Dynamic>) {
// Stage N ends when stage N+1 begins.
return this.triggers[getNextStage(stage)].triggeredAt ?? Infinity
}

getStaticStageEndTime() {
// The Static stage ends when the stage after it began.
return (
this.triggers[getNextStage(RenderStage.Static)].triggeredAt ?? Infinity
)
return this.getStageEndTime(RenderStage.Static)
}

getRuntimeStageEndTime() {
// The Runtime stage ended when the stage after it began.
return (
this.triggers[getNextStage(RenderStage.Runtime)].triggeredAt ?? Infinity
)
return this.getStageEndTime(RenderStage.Runtime)
}

private abandonRender() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const instant = { level: 'experimental-error' }

export default async function Page() {
await loadOuter()
return (
<main>
<p>
This page awaits a couple blocking (dynamic) things in sequence. We
should point to the first one as the cause.
</p>
</main>
)
}

async function loadOuter() {
await new Promise((resolve) => setTimeout(resolve)) // 1 (correct)
await loadInner()
}

async function loadInner() {
await new Promise((resolve) => setTimeout(resolve)) // 2 (incorrect)
await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { cookies } from 'next/headers'

export const instant = { level: 'experimental-error' }

export default async function Layout({ children, params }) {
await params // 1 (correct)
await cookies() // 2 (incorrect)
return (
<div>
<p>This layout is blocked on params.</p>
{children}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Instant } from 'next'
import { cookies } from 'next/headers'

export const prefetch = 'allow-runtime'

export const instant: Instant = {
level: 'experimental-error',
unstable_samples: [{ params: { slug: '123' } }],
}

export default async function Page() {
await cookies() // Not blocking due to allow-runtime
return (
<main>
<p>
This page component is valid because it's allow-runtime, but its parent
layout is static and blocked on runtime data.
</p>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Instant } from 'next'
import { cookies } from 'next/headers'

export const instant: Instant = { level: 'experimental-error' }

export default async function Page() {
await loadOuter()
return (
<main>
<p>
This page awaits a couple blocking (runtime, then dynamic) things in
sequence. We should point to the first one as the cause.
</p>
</main>
)
}

async function loadOuter() {
await cookies() // 1 (correct)
await loadInner()
}

async function loadInner() {
await new Promise((resolve) => setTimeout(resolve)) // 2 (not correct, but expected)
await new Promise((resolve) => setTimeout(resolve)) // 3 (incorrect)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/instant-validation/app/suspense-in-root/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,19 @@ export default async function Page() {
<DebugLinks href="/suspense-in-root/disable-validation/disable-build" />
</li>
</ul>

<h2>Blocking await attribution</h2>
<ul>
<li>
<DebugLinks href="/suspense-in-root/blocking-attribution/dynamic-then-dynamic" />
</li>
<li>
<DebugLinks href="/suspense-in-root/blocking-attribution/runtime-in-static/123" />
</li>
<li>
<DebugLinks href="/suspense-in-root/blocking-attribution/runtime-then-dynamic" />
</li>
</ul>
</main>
)
}
Loading
Loading