diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js index 5d22f1a4c94..82510b94ca1 100644 --- a/packages/react-markup/src/ReactMarkupServer.js +++ b/packages/react-markup/src/ReactMarkupServer.js @@ -11,6 +11,8 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {ErrorInfo} from 'react-server/src/ReactFizzServer'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import ReactVersion from 'shared/ReactVersion'; import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer'; @@ -68,6 +70,7 @@ type MarkupOptions = { identifierPrefix?: string, signal?: AbortSignal, onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, + startTime?: number, }; function noServerCallOrFormAction() { @@ -184,6 +187,7 @@ export function experimental_renderToHTML( handleFlightError, options ? options.identifierPrefix : undefined, undefined, + enableProfilerTimer && options ? options.startTime : undefined, 'Markup', undefined, false, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 469e5465f30..52c3507644d 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -73,6 +73,7 @@ type Options = { signal?: AbortSignal, debugChannel?: {onMessage?: (message: string) => void}, onError?: (error: mixed) => void, + startTime?: number, }; function render(model: ReactClientValue, options?: Options): Destination { @@ -84,6 +85,7 @@ function render(model: ReactClientValue, options?: Options): Destination { options ? options.onError : undefined, options ? options.identifierPrefix : undefined, undefined, + __PROFILE__ && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options && options.debugChannel !== undefined, diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index 8e0799fb020..85e0417b510 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -17,6 +17,7 @@ import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {Duplex} from 'stream'; @@ -146,6 +147,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -183,6 +185,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -272,6 +275,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -303,6 +307,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js index bdaadd66684..3a145af2d75 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js @@ -12,6 +12,9 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {ReactFormState, Thenable} from 'shared/ReactTypes'; + +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import { preloadModule, requireModule, @@ -67,6 +70,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -128,6 +132,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -215,6 +220,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index 83150996ae6..c7c69d090b4 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,8 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {ReactFormState, Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import { preloadModule, requireModule, @@ -72,6 +74,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -133,6 +136,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -220,6 +224,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js index c5903c41ed4..9681c4f80bb 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js @@ -15,6 +15,7 @@ import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {ReactFormState, Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type { ServerManifest, ServerReferenceId, @@ -159,6 +160,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -195,6 +197,7 @@ export function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -352,6 +355,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -434,6 +438,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -464,6 +469,7 @@ export function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -526,6 +532,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 9388e5790f1..c6e19991899 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -12,6 +12,7 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; @@ -64,6 +65,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -126,6 +128,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -214,6 +217,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index f6a8fcc9abc..039809b2da5 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,7 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {ClientManifest} from './ReactFlightServerConfigTurbopackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; @@ -69,6 +70,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -131,6 +133,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -219,6 +222,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index 74d379f53a0..e489e48a90a 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -17,6 +17,7 @@ import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {Duplex} from 'stream'; @@ -152,6 +153,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +191,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -347,6 +350,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +433,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +465,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +529,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js index 9a75c20395b..7b32cefd0a4 100644 --- a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js @@ -17,6 +17,7 @@ import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {Duplex} from 'stream'; @@ -152,6 +153,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +191,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -347,6 +350,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +433,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +465,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +529,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index bd2f96609af..ad3ff1b4004 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -101,7 +101,7 @@ describe('ReactFlightDOMNode', () => { return ( ' in ' + name + - (/\d/.test(m) + (/:\d+:\d+/.test(m) ? preserveLocation ? ' ' + location.replace(__filename, relativeFilename) : ' (at **)' @@ -1589,6 +1589,296 @@ describe('ReactFlightDOMNode', () => { expect(ownerStack).toBeNull(); } }); + + function createReadableWithLateRelease(initialChunks, lateChunks, signal) { + // Create a new Readable and push all initial chunks immediately. + const readable = new Stream.Readable({...streamOptions, read() {}}); + for (let i = 0; i < initialChunks.length; i++) { + readable.push(initialChunks[i]); + } + + // When prerendering is aborted, push all dynamic chunks. They won't be + // considered for rendering, but they include debug info we want to use. + signal.addEventListener( + 'abort', + () => { + for (let i = 0; i < lateChunks.length; i++) { + readable.push(lateChunks[i]); + } + setImmediate(() => { + readable.push(null); + }); + }, + {once: true}, + ); + + return readable; + } + + async function reencodeFlightStream( + staticChunks, + dynamicChunks, + startTime, + serverConsumerManifest, + ) { + let staticEndTime = -1; + const chunks = { + static: [], + dynamic: [], + }; + await new Promise(async resolve => { + const renderStageController = new AbortController(); + + const serverStream = createReadableWithLateRelease( + staticChunks, + dynamicChunks, + renderStageController.signal, + ); + const decoded = await ReactServerDOMClient.createFromNodeStream( + serverStream, + serverConsumerManifest, + { + // We're re-encoding the whole stream, so we don't want to filter out any debug info. + endTime: undefined, + }, + ); + + setTimeout(async () => { + const stream = ReactServerDOMServer.renderToPipeableStream( + decoded, + webpackMap, + { + filterStackFrame, + // Pass in the original render's startTime to avoid omitting its IO info. + startTime, + }, + ); + + const passThrough = new Stream.PassThrough(streamOptions); + + passThrough.on('data', chunk => { + if (!renderStageController.signal.aborted) { + chunks.static.push(chunk); + } else { + chunks.dynamic.push(chunk); + } + }); + passThrough.on('end', resolve); + + stream.pipe(passThrough); + }); + + setTimeout(() => { + staticEndTime = performance.now() + performance.timeOrigin; + renderStageController.abort(); + }); + }); + + return {chunks, staticEndTime}; + } + + // @gate __DEV__ + it('can preserve old IO info when decoding and re-encoding a stream with options.startTime', async () => { + let resolveDynamicData; + + function getDynamicData() { + return new Promise(resolve => { + resolveDynamicData = resolve; + }); + } + + async function Dynamic() { + const data = await getDynamicData(); + return ReactServer.createElement('p', null, data); + } + + function App() { + return ReactServer.createElement( + 'html', + null, + ReactServer.createElement( + 'body', + null, + ReactServer.createElement( + ReactServer.Suspense, + {fallback: 'Loading...'}, + // TODO: having a wrapper
here seems load-bearing. + // ReactServer.createElement(ReactServer.createElement(Dynamic)), + ReactServer.createElement( + 'section', + null, + ReactServer.createElement(Dynamic), + ), + ), + ), + ); + } + + const resolveDynamic = () => { + resolveDynamicData('Hi Janka'); + }; + + // 1. Render , dividing the output into static and dynamic content. + + let startTime = -1; + + let isStatic = true; + const chunks1 = { + static: [], + dynamic: [], + }; + + await new Promise(resolve => { + setTimeout(async () => { + startTime = performance.now() + performance.timeOrigin; + + const stream = ReactServerDOMServer.renderToPipeableStream( + ReactServer.createElement(App), + webpackMap, + { + filterStackFrame, + startTime, + environmentName() { + return isStatic ? 'Prerender' : 'Server'; + }, + }, + ); + + const passThrough = new Stream.PassThrough(streamOptions); + + passThrough.on('data', chunk => { + if (isStatic) { + chunks1.static.push(chunk); + } else { + chunks1.dynamic.push(chunk); + } + }); + passThrough.on('end', resolve); + + stream.pipe(passThrough); + }); + setTimeout(() => { + isStatic = false; + resolveDynamic(); + }); + }); + + //=============================================== + // 2. Decode the stream from the previous step and render it again. + // This should preserve existing debug info. + + const serverConsumerManifest = { + moduleMap: null, + moduleLoading: null, + }; + + const {chunks: chunks2, staticEndTime: reencodeStaticEndTime} = + await reencodeFlightStream( + chunks1.static, + chunks1.dynamic, + // This is load-bearing. If we don't pass a startTime, IO info + // from the initial render will be skipped (because it finished in the past) + // and we won't get the precise location of the blocking await in the owner stack. + startTime, + serverConsumerManifest, + ); + + //=============================================== + // 3. SSR the stream from the previous step and abort it after the static stage + // (which should trigger `onError` for each "hole" that hasn't resolved yet) + + function ClientRoot({response}) { + return use(response); + } + + let ssrStream; + let ownerStack; + let componentStack; + + await new Promise(async (resolve, reject) => { + const renderController = new AbortController(); + + const serverStream = createReadableWithLateRelease( + chunks2.static, + chunks2.dynamic, + renderController.signal, + ); + + const decodedPromise = ReactServerDOMClient.createFromNodeStream( + serverStream, + serverConsumerManifest, + { + endTime: reencodeStaticEndTime, + }, + ); + + setTimeout(() => { + ssrStream = ReactDOMServer.renderToPipeableStream( + React.createElement(ClientRoot, { + response: decodedPromise, + }), + { + onError(err, errorInfo) { + componentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + return null; + }, + }, + ); + + renderController.signal.addEventListener( + 'abort', + () => { + const {reason} = renderController.signal; + ssrStream.abort(reason); + }, + { + once: true, + }, + ); + }); + + setTimeout(() => { + renderController.abort(new Error('ssr-abort')); + resolve(); + }); + }); + + const result = await readResult(ssrStream); + + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n' + + // TODO: + // when we reencode a stream, the component stack doesn't have server frames for the dynamic content + // (which is what causes the dynamic hole here) + // because Flight delays forwarding debug info for lazies until they resolve. + // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine) + // + // ' in Dynamic (at **)\n' + ' in section\n' + + ' in Suspense\n' + + ' in body\n' + + ' in html\n' + + ' in App (at **)\n' + + ' in ClientRoot (at **)', + ); + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n' + + gate(flags => + flags.enableAsyncDebugInfo + ? ' in Dynamic (at **)\n' + : ' in section\n', + ) + + ' in App (at **)', + ); + + expect(result).toContain( + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'ssr-abort', + ); + }); }); it('warns with a tailored message if eval is not available in dev', async () => { diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index 1c417ff6bda..2d81fb374bf 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -15,6 +15,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import { createRequest, createPrerenderRequest, @@ -64,6 +66,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -126,6 +129,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -214,6 +218,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 77067754bc5..7b7ffe2c48d 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -15,6 +15,8 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ClientManifest} from './ReactFlightServerConfigWebpackBundler'; import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import {ASYNC_ITERATOR} from 'shared/ReactSymbols'; import { @@ -69,6 +71,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -131,6 +134,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -219,6 +223,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 888d0139144..1bae54ab339 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -17,6 +17,7 @@ import type {ServerManifest} from 'react-client/src/ReactFlightClientConfig'; import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; import type {Thenable} from 'shared/ReactTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; import type {Duplex} from 'stream'; @@ -152,6 +153,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +191,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -347,6 +350,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +433,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +465,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +529,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + enableProfilerTimer && options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f31fa45f7a7..6edfbaa4df3 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -660,6 +660,7 @@ function RequestInstance( onFatalError: (error: mixed) => void, identifierPrefix?: string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -751,7 +752,15 @@ function RequestInstance( // This avoids leaking unnecessary information like how long the server has // been running and allows for more compact representation of each timestamp. // The time origin is stored as an offset in the time space of this environment. - timeOrigin = this.timeOrigin = performance.now(); + if (typeof debugStartTime === 'number') { + // We expect `startTime` to be an absolute timestamp, so relativize it to match the other case. + timeOrigin = this.timeOrigin = + debugStartTime - + // $FlowFixMe[prop-missing] + performance.timeOrigin; + } else { + timeOrigin = this.timeOrigin = performance.now(); + } emitTimeOriginChunk( this, timeOrigin + @@ -784,6 +793,7 @@ export function createRequest( onError: void | ((error: mixed) => ?string), identifierPrefix: void | string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -802,6 +812,7 @@ export function createRequest( noop, identifierPrefix, temporaryReferences, + debugStartTime, environmentName, filterStackFrame, keepDebugAlive, @@ -816,6 +827,7 @@ export function createPrerenderRequest( onError: void | ((error: mixed) => ?string), identifierPrefix: void | string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -834,6 +846,7 @@ export function createPrerenderRequest( onFatalError, identifierPrefix, temporaryReferences, + debugStartTime, environmentName, filterStackFrame, keepDebugAlive,