[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,