@samitouri / QOS-React / commits / b1533b034e

[Flight] Allow overriding `request.timeOrigin` via `options.startTime` (#35598)

Currently, IO that finished before the request started is not considered IO: https://github.com/facebook/react/blob/6a0ab4d2dd62dfdf8881ba1c3443c6d5acedc871/packages/react-server/src/ReactFlightServer.js#L5338-L5343 This leads to loss of debug info when a flight stream is deserialized and serialized again. We can solve this by allowing "when the the request started" to be set to a point in the past, when the original stream started by doing ```js const startTime = performance.now() + performance.timeOrigin // ... stuff happens and time passes... ReactServer.renderToReadableStream(..., { startTime }) ```

Janka Uryga committed Feb 3, 2026 at 15:29 UTC b1533b034ee5e38e825fe20d789f2e83ede163ad
15 files changed +356 -2
packages/react-markup/src/ReactMarkupServer.js
+1
@@ -184,6 +184,7 @@ export function experimental_renderToHTML(
184 handleFlightError,
185 options ? options.identifierPrefix : undefined,
186 undefined,
187 + undefined,
188 'Markup',
189 undefined,
190 false,
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+2
@@ -73,6 +73,7 @@ type Options = {
73 signal?: AbortSignal,
74 debugChannel?: {onMessage?: (message: string) => void},
75 onError?: (error: mixed) => void,
76 + startTime?: number,
77 };
78
79 function render(model: ReactClientValue, options?: Options): Destination {
@@ -84,6 +85,7 @@ function render(model: ReactClientValue, options?: Options): Destination {
85 options ? options.onError : undefined,
86 options ? options.identifierPrefix : undefined,
87 undefined,
88 + options ? options.startTime : undefined,
89 __DEV__ && options ? options.environmentName : undefined,
90 __DEV__ && options ? options.filterStackFrame : undefined,
91 __DEV__ && options && options.debugChannel !== undefined,
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js
+4
@@ -146,6 +146,7 @@ type Options = {
146 onError?: (error: mixed) => void,
147 identifierPrefix?: string,
148 temporaryReferences?: TemporaryReferenceSet,
149 + startTime?: number,
150 };
151
152 type PipeableStream = {
@@ -183,6 +184,7 @@ function renderToPipeableStream(
184 options ? options.onError : undefined,
185 options ? options.identifierPrefix : undefined,
186 options ? options.temporaryReferences : undefined,
187 + options ? options.startTime : undefined,
188 __DEV__ && options ? options.environmentName : undefined,
189 __DEV__ && options ? options.filterStackFrame : undefined,
190 debugChannel !== undefined,
@@ -272,6 +274,7 @@ type PrerenderOptions = {
274 identifierPrefix?: string,
275 temporaryReferences?: TemporaryReferenceSet,
276 signal?: AbortSignal,
277 + startTime?: number,
278 };
279
280 type StaticResult = {
@@ -303,6 +306,7 @@ function prerenderToNodeStream(
306 options ? options.onError : undefined,
307 options ? options.identifierPrefix : undefined,
308 options ? options.temporaryReferences : undefined,
309 + options ? options.startTime : undefined,
310 __DEV__ && options ? options.environmentName : undefined,
311 __DEV__ && options ? options.filterStackFrame : undefined,
312 false,
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js
+4
@@ -12,6 +12,7 @@ import type {
12 ReactClientValue,
13 } from 'react-server/src/ReactFlightServer';
14 import type {ReactFormState, Thenable} from 'shared/ReactTypes';
15 +
16 import {
17 preloadModule,
18 requireModule,
@@ -67,6 +68,7 @@ type Options = {
68 signal?: AbortSignal,
69 temporaryReferences?: TemporaryReferenceSet,
70 onError?: (error: mixed) => void,
71 + startTime?: number,
72 };
73
74 function startReadingFromDebugChannelReadableStream(
@@ -128,6 +130,7 @@ export function renderToReadableStream(
130 options ? options.onError : undefined,
131 options ? options.identifierPrefix : undefined,
132 options ? options.temporaryReferences : undefined,
133 + options ? options.startTime : undefined,
134 __DEV__ && options ? options.environmentName : undefined,
135 __DEV__ && options ? options.filterStackFrame : undefined,
136 debugChannelReadable !== undefined,
@@ -215,6 +218,7 @@ export function prerender(
218 options ? options.onError : undefined,
219 options ? options.identifierPrefix : undefined,
220 options ? options.temporaryReferences : undefined,
221 + options ? options.startTime : undefined,
222 __DEV__ && options ? options.environmentName : undefined,
223 __DEV__ && options ? options.filterStackFrame : undefined,
224 false,
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js
+4
@@ -12,6 +12,7 @@ import type {
12 ReactClientValue,
13 } from 'react-server/src/ReactFlightServer';
14 import type {ReactFormState, Thenable} from 'shared/ReactTypes';
15 +
16 import {
17 preloadModule,
18 requireModule,
@@ -72,6 +73,7 @@ type Options = {
73 signal?: AbortSignal,
74 temporaryReferences?: TemporaryReferenceSet,
75 onError?: (error: mixed) => void,
76 + startTime?: number,
77 };
78
79 function startReadingFromDebugChannelReadableStream(
@@ -133,6 +135,7 @@ export function renderToReadableStream(
135 options ? options.onError : undefined,
136 options ? options.identifierPrefix : undefined,
137 options ? options.temporaryReferences : undefined,
138 + options ? options.startTime : undefined,
139 __DEV__ && options ? options.environmentName : undefined,
140 __DEV__ && options ? options.filterStackFrame : undefined,
141 debugChannelReadable !== undefined,
@@ -220,6 +223,7 @@ export function prerender(
223 options ? options.onError : undefined,
224 options ? options.identifierPrefix : undefined,
225 options ? options.temporaryReferences : undefined,
226 + options ? options.startTime : undefined,
227 __DEV__ && options ? options.environmentName : undefined,
228 __DEV__ && options ? options.filterStackFrame : undefined,
229 false,
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js
+6
@@ -159,6 +159,7 @@ type Options = {
159 onError?: (error: mixed) => void,
160 identifierPrefix?: string,
161 temporaryReferences?: TemporaryReferenceSet,
162 + startTime?: number,
163 };
164
165 type PipeableStream = {
@@ -195,6 +196,7 @@ export function renderToPipeableStream(
196 options ? options.onError : undefined,
197 options ? options.identifierPrefix : undefined,
198 options ? options.temporaryReferences : undefined,
199 + options ? options.startTime : undefined,
200 __DEV__ && options ? options.environmentName : undefined,
201 __DEV__ && options ? options.filterStackFrame : undefined,
202 debugChannel !== undefined,
@@ -352,6 +354,7 @@ export function renderToReadableStream(
354 options ? options.onError : undefined,
355 options ? options.identifierPrefix : undefined,
356 options ? options.temporaryReferences : undefined,
357 + options ? options.startTime : undefined,
358 __DEV__ && options ? options.environmentName : undefined,
359 __DEV__ && options ? options.filterStackFrame : undefined,
360 debugChannelReadable !== undefined,
@@ -434,6 +437,7 @@ type PrerenderOptions = {
437 identifierPrefix?: string,
438 temporaryReferences?: TemporaryReferenceSet,
439 signal?: AbortSignal,
440 + startTime?: number,
441 };
442
443 type StaticResult = {
@@ -464,6 +468,7 @@ export function prerenderToNodeStream(
468 options ? options.onError : undefined,
469 options ? options.identifierPrefix : undefined,
470 options ? options.temporaryReferences : undefined,
471 + options ? options.startTime : undefined,
472 __DEV__ && options ? options.environmentName : undefined,
473 __DEV__ && options ? options.filterStackFrame : undefined,
474 false,
@@ -526,6 +531,7 @@ export function prerender(
531 options ? options.onError : undefined,
532 options ? options.identifierPrefix : undefined,
533 options ? options.temporaryReferences : undefined,
534 + options ? options.startTime : undefined,
535 __DEV__ && options ? options.environmentName : undefined,
536 __DEV__ && options ? options.filterStackFrame : undefined,
537 false,
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js
+3
@@ -64,6 +64,7 @@ type Options = {
64 signal?: AbortSignal,
65 temporaryReferences?: TemporaryReferenceSet,
66 onError?: (error: mixed) => void,
67 + startTime?: number,
68 };
69
70 function startReadingFromDebugChannelReadableStream(
@@ -126,6 +127,7 @@ function renderToReadableStream(
127 options ? options.onError : undefined,
128 options ? options.identifierPrefix : undefined,
129 options ? options.temporaryReferences : undefined,
130 + options ? options.startTime : undefined,
131 __DEV__ && options ? options.environmentName : undefined,
132 __DEV__ && options ? options.filterStackFrame : undefined,
133 debugChannelReadable !== undefined,
@@ -214,6 +216,7 @@ function prerender(
216 options ? options.onError : undefined,
217 options ? options.identifierPrefix : undefined,
218 options ? options.temporaryReferences : undefined,
219 + options ? options.startTime : undefined,
220 __DEV__ && options ? options.environmentName : undefined,
221 __DEV__ && options ? options.filterStackFrame : undefined,
222 false,
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js
+3
@@ -69,6 +69,7 @@ type Options = {
69 signal?: AbortSignal,
70 temporaryReferences?: TemporaryReferenceSet,
71 onError?: (error: mixed) => void,
72 + startTime?: number,
73 };
74
75 function startReadingFromDebugChannelReadableStream(
@@ -131,6 +132,7 @@ function renderToReadableStream(
132 options ? options.onError : undefined,
133 options ? options.identifierPrefix : undefined,
134 options ? options.temporaryReferences : undefined,
135 + options ? options.startTime : undefined,
136 __DEV__ && options ? options.environmentName : undefined,
137 __DEV__ && options ? options.filterStackFrame : undefined,
138 debugChannelReadable !== undefined,
@@ -219,6 +221,7 @@ function prerender(
221 options ? options.onError : undefined,
222 options ? options.identifierPrefix : undefined,
223 options ? options.temporaryReferences : undefined,
224 + options ? options.startTime : undefined,
225 __DEV__ && options ? options.environmentName : undefined,
226 __DEV__ && options ? options.filterStackFrame : undefined,
227 false,
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js
+6
@@ -152,6 +152,7 @@ type Options = {
152 onError?: (error: mixed) => void,
153 identifierPrefix?: string,
154 temporaryReferences?: TemporaryReferenceSet,
155 + startTime?: number,
156 };
157
158 type PipeableStream = {
@@ -189,6 +190,7 @@ function renderToPipeableStream(
190 options ? options.onError : undefined,
191 options ? options.identifierPrefix : undefined,
192 options ? options.temporaryReferences : undefined,
193 + options ? options.startTime : undefined,
194 __DEV__ && options ? options.environmentName : undefined,
195 __DEV__ && options ? options.filterStackFrame : undefined,
196 debugChannel !== undefined,
@@ -347,6 +349,7 @@ function renderToReadableStream(
349 options ? options.onError : undefined,
350 options ? options.identifierPrefix : undefined,
351 options ? options.temporaryReferences : undefined,
352 + options ? options.startTime : undefined,
353 __DEV__ && options ? options.environmentName : undefined,
354 __DEV__ && options ? options.filterStackFrame : undefined,
355 debugChannelReadable !== undefined,
@@ -429,6 +432,7 @@ type PrerenderOptions = {
432 identifierPrefix?: string,
433 temporaryReferences?: TemporaryReferenceSet,
434 signal?: AbortSignal,
435 + startTime?: number,
436 };
437
438 type StaticResult = {
@@ -460,6 +464,7 @@ function prerenderToNodeStream(
464 options ? options.onError : undefined,
465 options ? options.identifierPrefix : undefined,
466 options ? options.temporaryReferences : undefined,
467 + options ? options.startTime : undefined,
468 __DEV__ && options ? options.environmentName : undefined,
469 __DEV__ && options ? options.filterStackFrame : undefined,
470 false,
@@ -523,6 +528,7 @@ function prerender(
528 options ? options.onError : undefined,
529 options ? options.identifierPrefix : undefined,
530 options ? options.temporaryReferences : undefined,
531 + options ? options.startTime : undefined,
532 __DEV__ && options ? options.environmentName : undefined,
533 __DEV__ && options ? options.filterStackFrame : undefined,
534 false,
packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js
+6
@@ -152,6 +152,7 @@ type Options = {
152 onError?: (error: mixed) => void,
153 identifierPrefix?: string,
154 temporaryReferences?: TemporaryReferenceSet,
155 + startTime?: number,
156 };
157
158 type PipeableStream = {
@@ -189,6 +190,7 @@ function renderToPipeableStream(
190 options ? options.onError : undefined,
191 options ? options.identifierPrefix : undefined,
192 options ? options.temporaryReferences : undefined,
193 + options ? options.startTime : undefined,
194 __DEV__ && options ? options.environmentName : undefined,
195 __DEV__ && options ? options.filterStackFrame : undefined,
196 debugChannelReadable !== undefined,
@@ -347,6 +349,7 @@ function renderToReadableStream(
349 options ? options.onError : undefined,
350 options ? options.identifierPrefix : undefined,
351 options ? options.temporaryReferences : undefined,
352 + options ? options.startTime : undefined,
353 __DEV__ && options ? options.environmentName : undefined,
354 __DEV__ && options ? options.filterStackFrame : undefined,
355 debugChannelReadable !== undefined,
@@ -429,6 +432,7 @@ type PrerenderOptions = {
432 identifierPrefix?: string,
433 temporaryReferences?: TemporaryReferenceSet,
434 signal?: AbortSignal,
435 + startTime?: number,
436 };
437
438 type StaticResult = {
@@ -460,6 +464,7 @@ function prerenderToNodeStream(
464 options ? options.onError : undefined,
465 options ? options.identifierPrefix : undefined,
466 options ? options.temporaryReferences : undefined,
467 + options ? options.startTime : undefined,
468 __DEV__ && options ? options.environmentName : undefined,
469 __DEV__ && options ? options.filterStackFrame : undefined,
470 false,
@@ -523,6 +528,7 @@ function prerender(
528 options ? options.onError : undefined,
529 options ? options.identifierPrefix : undefined,
530 options ? options.temporaryReferences : undefined,
531 + options ? options.startTime : undefined,
532 __DEV__ && options ? options.environmentName : undefined,
533 __DEV__ && options ? options.filterStackFrame : undefined,
534 false,
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+291 -1
@@ -101,7 +101,7 @@ describe('ReactFlightDOMNode', () => {
101 return (
102 ' in ' +
103 name +
104 - (/\d/.test(m)
104 + (/:\d+:\d+/.test(m)
105 ? preserveLocation
106 ? ' ' + location.replace(__filename, relativeFilename)
107 : ' (at **)'
@@ -1589,6 +1589,296 @@ describe('ReactFlightDOMNode', () => {
1589 expect(ownerStack).toBeNull();
1590 }
1591 });
1592 +
1593 + function createReadableWithLateRelease(initialChunks, lateChunks, signal) {
1594 + // Create a new Readable and push all initial chunks immediately.
1595 + const readable = new Stream.Readable({...streamOptions, read() {}});
1596 + for (let i = 0; i < initialChunks.length; i++) {
1597 + readable.push(initialChunks[i]);
1598 + }
1599 +
1600 + // When prerendering is aborted, push all dynamic chunks. They won't be
1601 + // considered for rendering, but they include debug info we want to use.
1602 + signal.addEventListener(
1603 + 'abort',
1604 + () => {
1605 + for (let i = 0; i < lateChunks.length; i++) {
1606 + readable.push(lateChunks[i]);
1607 + }
1608 + setImmediate(() => {
1609 + readable.push(null);
1610 + });
1611 + },
1612 + {once: true},
1613 + );
1614 +
1615 + return readable;
1616 + }
1617 +
1618 + async function reencodeFlightStream(
1619 + staticChunks,
1620 + dynamicChunks,
1621 + startTime,
1622 + serverConsumerManifest,
1623 + ) {
1624 + let staticEndTime = -1;
1625 + const chunks = {
1626 + static: [],
1627 + dynamic: [],
1628 + };
1629 + await new Promise(async resolve => {
1630 + const renderStageController = new AbortController();
1631 +
1632 + const serverStream = createReadableWithLateRelease(
1633 + staticChunks,
1634 + dynamicChunks,
1635 + renderStageController.signal,
1636 + );
1637 + const decoded = await ReactServerDOMClient.createFromNodeStream(
1638 + serverStream,
1639 + serverConsumerManifest,
1640 + {
1641 + // We're re-encoding the whole stream, so we don't want to filter out any debug info.
1642 + endTime: undefined,
1643 + },
1644 + );
1645 +
1646 + setTimeout(async () => {
1647 + const stream = ReactServerDOMServer.renderToPipeableStream(
1648 + decoded,
1649 + webpackMap,
1650 + {
1651 + filterStackFrame,
1652 + // Pass in the original render's startTime to avoid omitting its IO info.
1653 + startTime,
1654 + },
1655 + );
1656 +
1657 + const passThrough = new Stream.PassThrough(streamOptions);
1658 +
1659 + passThrough.on('data', chunk => {
1660 + if (!renderStageController.signal.aborted) {
1661 + chunks.static.push(chunk);
1662 + } else {
1663 + chunks.dynamic.push(chunk);
1664 + }
1665 + });
1666 + passThrough.on('end', resolve);
1667 +
1668 + stream.pipe(passThrough);
1669 + });
1670 +
1671 + setTimeout(() => {
1672 + staticEndTime = performance.now() + performance.timeOrigin;
1673 + renderStageController.abort();
1674 + });
1675 + });
1676 +
1677 + return {chunks, staticEndTime};
1678 + }
1679 +
1680 + // @gate __DEV__
1681 + it('can preserve old IO info when decoding and re-encoding a stream with options.startTime', async () => {
1682 + let resolveDynamicData;
1683 +
1684 + function getDynamicData() {
1685 + return new Promise(resolve => {
1686 + resolveDynamicData = resolve;
1687 + });
1688 + }
1689 +
1690 + async function Dynamic() {
1691 + const data = await getDynamicData();
1692 + return ReactServer.createElement('p', null, data);
1693 + }
1694 +
1695 + function App() {
1696 + return ReactServer.createElement(
1697 + 'html',
1698 + null,
1699 + ReactServer.createElement(
1700 + 'body',
1701 + null,
1702 + ReactServer.createElement(
1703 + ReactServer.Suspense,
1704 + {fallback: 'Loading...'},
1705 + // TODO: having a wrapper <section> here seems load-bearing.
1706 + // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1707 + ReactServer.createElement(
1708 + 'section',
1709 + null,
1710 + ReactServer.createElement(Dynamic),
1711 + ),
1712 + ),
1713 + ),
1714 + );
1715 + }
1716 +
1717 + const resolveDynamic = () => {
1718 + resolveDynamicData('Hi Janka');
1719 + };
1720 +
1721 + // 1. Render <App />, dividing the output into static and dynamic content.
1722 +
1723 + let startTime = -1;
1724 +
1725 + let isStatic = true;
1726 + const chunks1 = {
1727 + static: [],
1728 + dynamic: [],
1729 + };
1730 +
1731 + await new Promise(resolve => {
1732 + setTimeout(async () => {
1733 + startTime = performance.now() + performance.timeOrigin;
1734 +
1735 + const stream = ReactServerDOMServer.renderToPipeableStream(
1736 + ReactServer.createElement(App),
1737 + webpackMap,
1738 + {
1739 + filterStackFrame,
1740 + startTime,
1741 + environmentName() {
1742 + return isStatic ? 'Prerender' : 'Server';
1743 + },
1744 + },
1745 + );
1746 +
1747 + const passThrough = new Stream.PassThrough(streamOptions);
1748 +
1749 + passThrough.on('data', chunk => {
1750 + if (isStatic) {
1751 + chunks1.static.push(chunk);
1752 + } else {
1753 + chunks1.dynamic.push(chunk);
1754 + }
1755 + });
1756 + passThrough.on('end', resolve);
1757 +
1758 + stream.pipe(passThrough);
1759 + });
1760 + setTimeout(() => {
1761 + isStatic = false;
1762 + resolveDynamic();
1763 + });
1764 + });
1765 +
1766 + //===============================================
1767 + // 2. Decode the stream from the previous step and render it again.
1768 + // This should preserve existing debug info.
1769 +
1770 + const serverConsumerManifest = {
1771 + moduleMap: null,
1772 + moduleLoading: null,
1773 + };
1774 +
1775 + const {chunks: chunks2, staticEndTime: reencodeStaticEndTime} =
1776 + await reencodeFlightStream(
1777 + chunks1.static,
1778 + chunks1.dynamic,
1779 + // This is load-bearing. If we don't pass a startTime, IO info
1780 + // from the initial render will be skipped (because it finished in the past)
1781 + // and we won't get the precise location of the blocking await in the owner stack.
1782 + startTime,
1783 + serverConsumerManifest,
1784 + );
1785 +
1786 + //===============================================
1787 + // 3. SSR the stream from the previous step and abort it after the static stage
1788 + // (which should trigger `onError` for each "hole" that hasn't resolved yet)
1789 +
1790 + function ClientRoot({response}) {
1791 + return use(response);
1792 + }
1793 +
1794 + let ssrStream;
1795 + let ownerStack;
1796 + let componentStack;
1797 +
1798 + await new Promise(async (resolve, reject) => {
1799 + const renderController = new AbortController();
1800 +
1801 + const serverStream = createReadableWithLateRelease(
1802 + chunks2.static,
1803 + chunks2.dynamic,
1804 + renderController.signal,
1805 + );
1806 +
1807 + const decodedPromise = ReactServerDOMClient.createFromNodeStream(
1808 + serverStream,
1809 + serverConsumerManifest,
1810 + {
1811 + endTime: reencodeStaticEndTime,
1812 + },
1813 + );
1814 +
1815 + setTimeout(() => {
1816 + ssrStream = ReactDOMServer.renderToPipeableStream(
1817 + React.createElement(ClientRoot, {
1818 + response: decodedPromise,
1819 + }),
1820 + {
1821 + onError(err, errorInfo) {
1822 + componentStack = errorInfo.componentStack;
1823 + ownerStack = React.captureOwnerStack
1824 + ? React.captureOwnerStack()
1825 + : null;
1826 + return null;
1827 + },
1828 + },
1829 + );
1830 +
1831 + renderController.signal.addEventListener(
1832 + 'abort',
1833 + () => {
1834 + const {reason} = renderController.signal;
1835 + ssrStream.abort(reason);
1836 + },
1837 + {
1838 + once: true,
1839 + },
1840 + );
1841 + });
1842 +
1843 + setTimeout(() => {
1844 + renderController.abort(new Error('ssr-abort'));
1845 + resolve();
1846 + });
1847 + });
1848 +
1849 + const result = await readResult(ssrStream);
1850 +
1851 + expect(normalizeCodeLocInfo(componentStack)).toBe(
1852 + '\n' +
1853 + // TODO:
1854 + // when we reencode a stream, the component stack doesn't have server frames for the dynamic content
1855 + // (which is what causes the dynamic hole here)
1856 + // because Flight delays forwarding debug info for lazies until they resolve.
1857 + // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine)
1858 + //
1859 + // ' in Dynamic (at **)\n'
1860 + ' in section\n' +
1861 + ' in Suspense\n' +
1862 + ' in body\n' +
1863 + ' in html\n' +
1864 + ' in App (at **)\n' +
1865 + ' in ClientRoot (at **)',
1866 + );
1867 + expect(normalizeCodeLocInfo(ownerStack)).toBe(
1868 + '\n' +
1869 + gate(flags =>
1870 + flags.enableAsyncDebugInfo
1871 + ? ' in Dynamic (at **)\n'
1872 + : ' in section\n',
1873 + ) +
1874 + ' in App (at **)',
1875 + );
1876 +
1877 + expect(result).toContain(
1878 + 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1879 + 'ssr-abort',
1880 + );
1881 + });
1882 });
1883
1884 it('warns with a tailored message if eval is not available in dev', async () => {
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js
+3
@@ -64,6 +64,7 @@ type Options = {
64 signal?: AbortSignal,
65 temporaryReferences?: TemporaryReferenceSet,
66 onError?: (error: mixed) => void,
67 + startTime?: number,
68 };
69
70 function startReadingFromDebugChannelReadableStream(
@@ -126,6 +127,7 @@ function renderToReadableStream(
127 options ? options.onError : undefined,
128 options ? options.identifierPrefix : undefined,
129 options ? options.temporaryReferences : undefined,
130 + options ? options.startTime : undefined,
131 __DEV__ && options ? options.environmentName : undefined,
132 __DEV__ && options ? options.filterStackFrame : undefined,
133 debugChannelReadable !== undefined,
@@ -214,6 +216,7 @@ function prerender(
216 options ? options.onError : undefined,
217 options ? options.identifierPrefix : undefined,
218 options ? options.temporaryReferences : undefined,
219 + options ? options.startTime : undefined,
220 __DEV__ && options ? options.environmentName : undefined,
221 __DEV__ && options ? options.filterStackFrame : undefined,
222 false,
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js
+3
@@ -69,6 +69,7 @@ type Options = {
69 signal?: AbortSignal,
70 temporaryReferences?: TemporaryReferenceSet,
71 onError?: (error: mixed) => void,
72 + startTime?: number,
73 };
74
75 function startReadingFromDebugChannelReadableStream(
@@ -131,6 +132,7 @@ function renderToReadableStream(
132 options ? options.onError : undefined,
133 options ? options.identifierPrefix : undefined,
134 options ? options.temporaryReferences : undefined,
135 + options ? options.startTime : undefined,
136 __DEV__ && options ? options.environmentName : undefined,
137 __DEV__ && options ? options.filterStackFrame : undefined,
138 debugChannelReadable !== undefined,
@@ -219,6 +221,7 @@ function prerender(
221 options ? options.onError : undefined,
222 options ? options.identifierPrefix : undefined,
223 options ? options.temporaryReferences : undefined,
224 + options ? options.startTime : undefined,
225 __DEV__ && options ? options.environmentName : undefined,
226 __DEV__ && options ? options.filterStackFrame : undefined,
227 false,
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
+6
@@ -152,6 +152,7 @@ type Options = {
152 onError?: (error: mixed) => void,
153 identifierPrefix?: string,
154 temporaryReferences?: TemporaryReferenceSet,
155 + startTime?: number,
156 };
157
158 type PipeableStream = {
@@ -189,6 +190,7 @@ function renderToPipeableStream(
190 options ? options.onError : undefined,
191 options ? options.identifierPrefix : undefined,
192 options ? options.temporaryReferences : undefined,
193 + options ? options.startTime : undefined,
194 __DEV__ && options ? options.environmentName : undefined,
195 __DEV__ && options ? options.filterStackFrame : undefined,
196 debugChannelReadable !== undefined,
@@ -347,6 +349,7 @@ function renderToReadableStream(
349 options ? options.onError : undefined,
350 options ? options.identifierPrefix : undefined,
351 options ? options.temporaryReferences : undefined,
352 + options ? options.startTime : undefined,
353 __DEV__ && options ? options.environmentName : undefined,
354 __DEV__ && options ? options.filterStackFrame : undefined,
355 debugChannelReadable !== undefined,
@@ -429,6 +432,7 @@ type PrerenderOptions = {
432 identifierPrefix?: string,
433 temporaryReferences?: TemporaryReferenceSet,
434 signal?: AbortSignal,
435 + startTime?: number,
436 };
437
438 type StaticResult = {
@@ -460,6 +464,7 @@ function prerenderToNodeStream(
464 options ? options.onError : undefined,
465 options ? options.identifierPrefix : undefined,
466 options ? options.temporaryReferences : undefined,
467 + options ? options.startTime : undefined,
468 __DEV__ && options ? options.environmentName : undefined,
469 __DEV__ && options ? options.filterStackFrame : undefined,
470 false,
@@ -523,6 +528,7 @@ function prerender(
528 options ? options.onError : undefined,
529 options ? options.identifierPrefix : undefined,
530 options ? options.temporaryReferences : undefined,
531 + options ? options.startTime : undefined,
532 __DEV__ && options ? options.environmentName : undefined,
533 __DEV__ && options ? options.filterStackFrame : undefined,
534 false,
packages/react-server/src/ReactFlightServer.js
+14 -1
@@ -660,6 +660,7 @@ function RequestInstance(
660 onFatalError: (error: mixed) => void,
661 identifierPrefix?: string,
662 temporaryReferences: void | TemporaryReferenceSet,
663 + debugStartTime: void | number, // Profiling-only
664 environmentName: void | string | (() => string), // DEV-only
665 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
666 keepDebugAlive: boolean, // DEV-only
@@ -751,7 +752,15 @@ function RequestInstance(
752 // This avoids leaking unnecessary information like how long the server has
753 // been running and allows for more compact representation of each timestamp.
754 // The time origin is stored as an offset in the time space of this environment.
754 - timeOrigin = this.timeOrigin = performance.now();
755 + if (typeof debugStartTime === 'number') {
756 + // We expect `startTime` to be an absolute timestamp, so relativize it to match the other case.
757 + timeOrigin = this.timeOrigin =
758 + debugStartTime -
759 + // $FlowFixMe[prop-missing]
760 + performance.timeOrigin;
761 + } else {
762 + timeOrigin = this.timeOrigin = performance.now();
763 + }
764 emitTimeOriginChunk(
765 this,
766 timeOrigin +
@@ -784,6 +793,7 @@ export function createRequest(
793 onError: void | ((error: mixed) => ?string),
794 identifierPrefix: void | string,
795 temporaryReferences: void | TemporaryReferenceSet,
796 + debugStartTime: void | number, // Profiling-only
797 environmentName: void | string | (() => string), // DEV-only
798 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
799 keepDebugAlive: boolean, // DEV-only
@@ -802,6 +812,7 @@ export function createRequest(
812 noop,
813 identifierPrefix,
814 temporaryReferences,
815 + debugStartTime,
816 environmentName,
817 filterStackFrame,
818 keepDebugAlive,
@@ -816,6 +827,7 @@ export function createPrerenderRequest(
827 onError: void | ((error: mixed) => ?string),
828 identifierPrefix: void | string,
829 temporaryReferences: void | TemporaryReferenceSet,
830 + debugStartTime: void | number, // Profiling-only
831 environmentName: void | string | (() => string), // DEV-only
832 filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only
833 keepDebugAlive: boolean, // DEV-only
@@ -834,6 +846,7 @@ export function createPrerenderRequest(
846 onFatalError,
847 identifierPrefix,
848 temporaryReferences,
849 + debugStartTime,
850 environmentName,
851 filterStackFrame,
852 keepDebugAlive,