@samitouri / QOS-React / commits / 67f7d47a9b

[Flight] Fix debug info filtering to include later resolved I/O (#35036)

In #35019, we excluded debug I/O info from being considered for enhancing the owner stack if it resolved after the defined `endTime` option that can be passed to the Flight client. However, we should include any I/O that was awaited before that end time, even if it resolved later.

Hendrik Liebau committed Nov 3, 2025 at 22:59 UTC 67f7d47a9b8f450c93ee69edd16aed68e910ff1e
2 files changed +45 -58
packages/react-client/src/ReactFlightClient.js
+3 -4
@@ -510,7 +510,9 @@ function filterDebugInfo(
510 return;
511 }
512
513 - // Remove any debug info entries that arrived after the defined end time.
513 + // Remove any debug info entries after the defined end time. For async info
514 + // that means we're including anything that was awaited before the end time,
515 + // but it doesn't need to be resolved before the end time.
516 const relativeEndTime =
517 response._debugEndTime -
518 // $FlowFixMe[prop-missing]
@@ -521,9 +523,6 @@ function filterDebugInfo(
523 if (typeof info.time === 'number' && info.time > relativeEndTime) {
524 break;
525 }
524 - if (info.awaited != null && info.awaited.end > relativeEndTime) {
525 - break;
526 - }
526 debugInfo.push(info);
527 }
528 value._debugInfo = debugInfo;
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js
+42 -54
@@ -1197,35 +1197,28 @@ describe('ReactFlightDOMNode', () => {
1197 });
1198
1199 it('should use late-arriving I/O debug info to enhance component and owner stacks when aborting a prerender', async () => {
1200 - // This test is constructing a scenario where a framework might separate
1201 - // I/O into different phases, e.g. runtime I/O and dynamic I/O. The
1202 - // framework might choose to define an end time for the Flight client,
1203 - // indicating that all I/O info (or any debug info for that matter) that
1204 - // arrives after that time should be ignored. When rendering in Fizz is
1205 - // then aborted, the late-arriving debug info that's used to enhance the
1206 - // owner stack only includes I/O info up to that end time.
1207 - let resolveRuntimeData;
1208 - let resolveDynamicData;
1209 -
1210 - async function getRuntimeData() {
1200 + let resolveDynamicData1;
1201 + let resolveDynamicData2;
1202 +
1203 + async function getDynamicData1() {
1204 return new Promise(resolve => {
1212 - resolveRuntimeData = resolve;
1205 + resolveDynamicData1 = resolve;
1206 });
1207 }
1208
1216 - async function getDynamicData() {
1209 + async function getDynamicData2() {
1210 return new Promise(resolve => {
1218 - resolveDynamicData = resolve;
1211 + resolveDynamicData2 = resolve;
1212 });
1213 }
1214
1215 async function Dynamic() {
1223 - const runtimeData = await getRuntimeData();
1224 - const dynamicData = await getDynamicData();
1216 + const data1 = await getDynamicData1();
1217 + const data2 = await getDynamicData2();
1218
1219 return (
1220 <p>
1228 - {runtimeData} {dynamicData}
1221 + {data1} {data2}
1222 </p>
1223 );
1224 }
@@ -1242,45 +1235,40 @@ describe('ReactFlightDOMNode', () => {
1235 );
1236 }
1237
1245 - const stream = await ReactServerDOMServer.renderToPipeableStream(
1246 - ReactServer.createElement(App),
1247 - webpackMap,
1248 - {filterStackFrame},
1249 - );
1250 -
1238 + let staticEndTime = -1;
1239 const initialChunks = [];
1240 const dynamicChunks = [];
1253 - let isDynamic = false;
1241
1255 - const passThrough = new Stream.PassThrough(streamOptions);
1256 - stream.pipe(passThrough);
1242 + await new Promise(resolve => {
1243 + setTimeout(async () => {
1244 + const stream = ReactServerDOMServer.renderToPipeableStream(
1245 + ReactServer.createElement(App),
1246 + webpackMap,
1247 + {filterStackFrame},
1248 + );
1249
1258 - passThrough.on('data', chunk => {
1259 - if (isDynamic) {
1260 - dynamicChunks.push(chunk);
1261 - } else {
1262 - initialChunks.push(chunk);
1263 - }
1264 - });
1250 + const passThrough = new Stream.PassThrough(streamOptions);
1251 + stream.pipe(passThrough);
1252
1266 - let endTime;
1253 + passThrough.on('data', chunk => {
1254 + if (staticEndTime < 0) {
1255 + initialChunks.push(chunk);
1256 + } else {
1257 + dynamicChunks.push(chunk);
1258 + }
1259 + });
1260
1268 - await new Promise(resolve => {
1269 - setTimeout(() => {
1270 - resolveRuntimeData('Hi');
1261 + passThrough.on('end', resolve);
1262 });
1263 setTimeout(() => {
1273 - isDynamic = true;
1274 - endTime = performance.now() + performance.timeOrigin;
1275 - resolveDynamicData('Josh');
1276 - resolve();
1264 + staticEndTime = performance.now() + performance.timeOrigin;
1265 + resolveDynamicData1('Hi');
1266 + setTimeout(() => {
1267 + resolveDynamicData2('Josh');
1268 + });
1269 });
1270 });
1271
1280 - await new Promise(resolve => {
1281 - passThrough.on('end', resolve);
1282 - });
1283 -
1272 // Create a new Readable and push all initial chunks immediately.
1273 const readable = new Stream.Readable({...streamOptions, read() {}});
1274 for (let i = 0; i < initialChunks.length; i++) {
@@ -1311,8 +1299,8 @@ describe('ReactFlightDOMNode', () => {
1299 },
1300 {
1301 // Debug info arriving after this end time will be ignored, e.g. the
1314 - // I/O info for the dynamic data.
1315 - endTime,
1302 + // I/O info for the second dynamic data.
1303 + endTime: staticEndTime,
1304 },
1305 );
1306
@@ -1358,12 +1346,12 @@ describe('ReactFlightDOMNode', () => {
1346 '\n' +
1347 ' in Dynamic' +
1348 (gate(flags => flags.enableAsyncDebugInfo)
1361 - ? ' (file://ReactFlightDOMNode-test.js:1223:33)\n'
1349 + ? ' (file://ReactFlightDOMNode-test.js:1216:27)\n'
1350 : '\n') +
1351 ' in body\n' +
1352 ' in html\n' +
1365 - ' in App (file://ReactFlightDOMNode-test.js:1240:25)\n' +
1366 - ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1353 + ' in App (file://ReactFlightDOMNode-test.js:1233:25)\n' +
1354 + ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
1355 );
1356 } else {
1357 expect(
@@ -1372,7 +1360,7 @@ describe('ReactFlightDOMNode', () => {
1360 '\n' +
1361 ' in body\n' +
1362 ' in html\n' +
1375 - ' in ClientRoot (ReactFlightDOMNode-test.js:1320:16)',
1363 + ' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
1364 );
1365 }
1366
@@ -1382,8 +1370,8 @@ describe('ReactFlightDOMNode', () => {
1370 normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
1371 ).toBe(
1372 '\n' +
1385 - ' in Dynamic (file://ReactFlightDOMNode-test.js:1223:33)\n' +
1386 - ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1373 + ' in Dynamic (file://ReactFlightDOMNode-test.js:1216:27)\n' +
1374 + ' in App (file://ReactFlightDOMNode-test.js:1233:25)',
1375 );
1376 } else {
1377 expect(
@@ -1391,7 +1379,7 @@ describe('ReactFlightDOMNode', () => {
1379 ).toBe(
1380 '' +
1381 '\n' +
1394 - ' in App (file://ReactFlightDOMNode-test.js:1240:25)',
1382 + ' in App (file://ReactFlightDOMNode-test.js:1233:25)',
1383 );
1384 }
1385 } else {