@samitouri / QOS-React-1 / commits / 6c8bcdaf1b

[Flight] Clarify Semantics for Awaiting Cached Data (#33438)

Technically the async call graph spans basically all the way back to the start of the app potentially, but we don't want to include everything. Similarly we don't want to include everything from previous components in every child component. So we need some heuristics for filtering out data. We roughly want to be able to inspect is what might contribute to a Suspense loading sequence even if it didn't this time e.g. due to a race condition. One flaw with the previous approach was that awaiting a cached promise in a sibling that happened to finish after another sibling would be excluded. However, in a different race condition that might end up being used so I wanted to include an empty "await" in that scenario to have some association from that component. However, for data that resolved fully before the request even started, it's a little different. This can be things that are part of the start up sequence of the app or externally cached data. We decided that this should be excluded because it doesn't contribute to the loading sequence in the expected scenario. I.e. if it's cached. Things that end up being cache misses would still be included. If you want to test externally cached data misses, then it's up to you or the framework to simulate those. E.g. by dropping the cache. This also helps free up some noise since static / cached data can be excluded in visualizations. I also apply this principle to forwarding debug info. If you reuse a cached RSC payload, then the Server Component render time and its awaits gets clamped to the caller as if it has zero render/await time. The I/O entry is still back dated but if it was fully resolved before we started then it's completely excluded.

Sebastian Markbåge committed Jun 7, 2025 at 17:26 UTC 6c8bcdaf1b0c3340150e174a342429d94e729fbb
3 files changed +602 -177
packages/react-client/src/__tests__/ReactFlight-test.js
+8 -8
@@ -2826,7 +2826,7 @@ describe('ReactFlight', () => {
2826 expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
2827 __DEV__
2828 ? [
2829 - {time: 14},
2829 + {time: 22}, // Clamped to the start
2830 {
2831 name: 'ThirdPartyComponent',
2832 env: 'third-party',
@@ -2834,7 +2834,7 @@ describe('ReactFlight', () => {
2834 stack: ' in Object.<anonymous> (at **)',
2835 props: {},
2836 },
2837 - {time: 15},
2837 + {time: 22},
2838 {time: 23}, // This last one is when the promise resolved into the first party.
2839 ]
2840 : undefined,
@@ -2842,7 +2842,7 @@ describe('ReactFlight', () => {
2842 expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
2843 __DEV__
2844 ? [
2845 - {time: 16},
2845 + {time: 22}, // Clamped to the start
2846 {
2847 name: 'ThirdPartyLazyComponent',
2848 env: 'third-party',
@@ -2850,14 +2850,14 @@ describe('ReactFlight', () => {
2850 stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
2851 props: {},
2852 },
2853 - {time: 17},
2853 + {time: 22},
2854 ]
2855 : undefined,
2856 );
2857 expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
2858 __DEV__
2859 ? [
2860 - {time: 12},
2860 + {time: 22},
2861 {
2862 name: 'ThirdPartyFragmentComponent',
2863 env: 'third-party',
@@ -2865,7 +2865,7 @@ describe('ReactFlight', () => {
2865 stack: ' in Object.<anonymous> (at **)',
2866 props: {},
2867 },
2868 - {time: 13},
2868 + {time: 22},
2869 ]
2870 : undefined,
2871 );
@@ -2967,7 +2967,7 @@ describe('ReactFlight', () => {
2967 expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual(
2968 __DEV__
2969 ? [
2970 - {time: 12},
2970 + {time: 19}, // Clamp to the start
2971 {
2972 name: 'ThirdPartyAsyncIterableComponent',
2973 env: 'third-party',
@@ -2975,7 +2975,7 @@ describe('ReactFlight', () => {
2975 stack: ' in Object.<anonymous> (at **)',
2976 props: {},
2977 },
2978 - {time: 13},
2978 + {time: 19},
2979 ]
2980 : undefined,
2981 );
packages/react-server/src/ReactFlightServer.js
+129 -82
@@ -687,22 +687,29 @@ function serializeThenable(
687 __DEV__ ? task.debugStack : null,
688 __DEV__ ? task.debugTask : null,
689 );
690 - if (__DEV__) {
691 - // If this came from Flight, forward any debug info into this new row.
692 - const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
693 - if (debugInfo) {
694 - forwardDebugInfo(request, newTask.id, debugInfo);
695 - }
696 - }
690
691 switch (thenable.status) {
692 case 'fulfilled': {
693 + if (__DEV__) {
694 + // If this came from Flight, forward any debug info into this new row.
695 + const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
696 + if (debugInfo) {
697 + forwardDebugInfo(request, newTask, debugInfo);
698 + }
699 + }
700 // We have the resolved value, we can go ahead and schedule it for serialization.
701 newTask.model = thenable.value;
702 pingTask(request, newTask);
703 return newTask.id;
704 }
705 case 'rejected': {
706 + if (__DEV__) {
707 + // If this came from Flight, forward any debug info into this new row.
708 + const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
709 + if (debugInfo) {
710 + forwardDebugInfo(request, newTask, debugInfo);
711 + }
712 + }
713 const x = thenable.reason;
714 erroredTask(request, newTask, x);
715 return newTask.id;
@@ -751,10 +758,24 @@ function serializeThenable(
758
759 thenable.then(
760 value => {
761 + if (__DEV__) {
762 + // If this came from Flight, forward any debug info into this new row.
763 + const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
764 + if (debugInfo) {
765 + forwardDebugInfo(request, newTask, debugInfo);
766 + }
767 + }
768 newTask.model = value;
769 pingTask(request, newTask);
770 },
771 reason => {
772 + if (__DEV__) {
773 + // If this came from Flight, forward any debug info into this new row.
774 + const debugInfo: ?ReactDebugInfo = (thenable: any)._debugInfo;
775 + if (debugInfo) {
776 + forwardDebugInfo(request, newTask, debugInfo);
777 + }
778 + }
779 if (newTask.status === PENDING) {
780 // We expect that the only status it might be otherwise is ABORTED.
781 // When we abort we emit chunks in each pending task slot and don't need
@@ -911,7 +932,7 @@ function serializeAsyncIterable(
932 if (__DEV__) {
933 const debugInfo: ?ReactDebugInfo = (iterable: any)._debugInfo;
934 if (debugInfo) {
914 - forwardDebugInfo(request, streamTask.id, debugInfo);
935 + forwardDebugInfo(request, streamTask, debugInfo);
936 }
937 }
938
@@ -1278,7 +1299,7 @@ function renderFunctionComponent<Props>(
1299
1300 let componentDebugInfo: ReactComponentInfo;
1301 if (__DEV__) {
1281 - if (debugID === null) {
1302 + if (!canEmitDebugInfo) {
1303 // We don't have a chunk to assign debug info. We need to outline this
1304 // component to assign it an ID.
1305 return outlineTask(request, task);
@@ -1289,7 +1310,7 @@ function renderFunctionComponent<Props>(
1310 componentDebugInfo = (prevThenableState: any)._componentDebugInfo;
1311 } else {
1312 // This is a new component in the same task so we can emit more debug info.
1292 - const componentDebugID = debugID;
1313 + const componentDebugID = task.id;
1314 const componentName =
1315 (Component: any).displayName || Component.name || '';
1316 const componentEnv = (0, request.environmentName)();
@@ -1543,7 +1564,7 @@ function renderFragment(
1564 const debugInfo: ?ReactDebugInfo = (children: any)._debugInfo;
1565 if (debugInfo) {
1566 // If this came from Flight, forward any debug info into this new row.
1546 - if (debugID === null) {
1567 + if (!canEmitDebugInfo) {
1568 // We don't have a chunk to assign debug info. We need to outline this
1569 // component to assign it an ID.
1570 return outlineTask(request, task);
@@ -1551,7 +1572,7 @@ function renderFragment(
1572 // Forward any debug info we have the first time we see it.
1573 // We do this after init so that we have received all the debug info
1574 // from the server by the time we emit it.
1554 - forwardDebugInfo(request, debugID, debugInfo);
1575 + forwardDebugInfo(request, task, debugInfo);
1576 }
1577 // Since we're rendering this array again, create a copy that doesn't
1578 // have the debug info so we avoid outlining or emitting debug info again.
@@ -1659,8 +1680,10 @@ function renderClientElement(
1680 return element;
1681 }
1682
1662 -// The chunk ID we're currently rendering that we can assign debug data to.
1663 -let debugID: null | number = null;
1683 +// Determines if we're currently rendering at the top level of a task and therefore
1684 +// is safe to emit debug info associated with that task. Otherwise, if we're in
1685 +// a nested context, we need to first outline.
1686 +let canEmitDebugInfo: boolean = false;
1687
1688 // Approximate string length of the currently serializing row.
1689 // Used to power outlining heuristics.
@@ -1879,6 +1902,7 @@ function visitAsyncNode(
1902 // First visit anything that blocked this sequence to start in the first place.
1903 if (node.previous !== null) {
1904 // We ignore the return value here because if it wasn't awaited in user space, then we don't log it.
1905 + // It also means that it can just have been part of a previous component's render.
1906 // TODO: This means that some I/O can get lost that was still blocking the sequence.
1907 visitAsyncNode(request, task, node.previous, cutOff, visited);
1908 }
@@ -1890,11 +1914,10 @@ function visitAsyncNode(
1914 return null;
1915 }
1916 case PROMISE_NODE: {
1893 - if (node.end < cutOff) {
1894 - // This was already resolved when we started this sequence. It must have been
1895 - // part of a different component.
1896 - // TODO: Think of some other way to exclude irrelevant data since if we awaited
1897 - // a cached promise, we should still log this component as being dependent on that data.
1917 + if (node.end <= request.timeOrigin) {
1918 + // This was already resolved when we started this render. It must have been either something
1919 + // that's part of a start up sequence or externally cached data. We exclude that information.
1920 + // The technique for debugging the effects of uncached data on the render is to simply uncache it.
1921 return null;
1922 }
1923 const awaited = node.awaited;
@@ -1928,7 +1951,7 @@ function visitAsyncNode(
1951 // the thing that generated this node and its virtual children.
1952 const debugInfo = node.debugInfo;
1953 if (debugInfo !== null) {
1931 - forwardDebugInfo(request, task.id, debugInfo);
1954 + forwardDebugInfo(request, task, debugInfo);
1955 }
1956 return match;
1957 }
@@ -1942,6 +1965,7 @@ function visitAsyncNode(
1965 if (awaited !== null) {
1966 const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1967 if (ioNode !== null) {
1968 + const startTime: number = node.start;
1969 let endTime: number;
1970 if (node.tag === UNRESOLVED_AWAIT_NODE) {
1971 // If we haven't defined an end time, use the resolve of the inner Promise.
@@ -1955,11 +1979,18 @@ function visitAsyncNode(
1979 } else {
1980 endTime = node.end;
1981 }
1958 - if (endTime < cutOff) {
1959 - // This was already resolved when we started this sequence. It must have been
1960 - // part of a different component.
1961 - // TODO: Think of some other way to exclude irrelevant data since if we awaited
1962 - // a cached promise, we should still log this component as being dependent on that data.
1982 + if (endTime <= request.timeOrigin) {
1983 + // This was already resolved when we started this render. It must have been either something
1984 + // that's part of a start up sequence or externally cached data. We exclude that information.
1985 + return null;
1986 + } else if (startTime < cutOff) {
1987 + // We started awaiting this node before we started rendering this sequence.
1988 + // This means that this particular await was never part of the current sequence.
1989 + // If we have another await higher up in the chain it might have a more actionable stack
1990 + // from the perspective of this component. If we end up here from the "previous" path,
1991 + // then this gets I/O ignored, which is what we want because it means it was likely
1992 + // just part of a previous component's rendering.
1993 + match = ioNode;
1994 } else {
1995 const stack = filterStackTrace(
1996 request,
@@ -1978,15 +2009,7 @@ function visitAsyncNode(
2009 // We log the environment at the time when the last promise pigned ping which may
2010 // be later than what the environment was when we actually started awaiting.
2011 const env = (0, request.environmentName)();
1981 - if (node.start <= cutOff) {
1982 - // If this was an await that started before this sequence but finished after,
1983 - // then we clamp it to the start of this sequence. We don't need to emit a time
1984 - // TODO: Typically we'll already have a previous time stamp with the cutOff time
1985 - // so we shouldn't need to emit another one. But not always.
1986 - emitTimingChunk(request, task.id, cutOff);
1987 - } else {
1988 - emitTimingChunk(request, task.id, node.start);
1989 - }
2012 + emitTimingChunk(request, task.id, startTime);
2013 // Then emit a reference to us awaiting it in the current task.
2014 request.pendingChunks++;
2015 emitDebugChunk(request, task.id, {
@@ -1995,7 +2018,7 @@ function visitAsyncNode(
2018 owner: node.owner,
2019 stack: stack,
2020 });
1998 - emitTimingChunk(request, task.id, node.end);
2021 + emitTimingChunk(request, task.id, endTime);
2022 }
2023 }
2024 }
@@ -2013,7 +2036,7 @@ function visitAsyncNode(
2036 debugInfo = node.debugInfo;
2037 }
2038 if (debugInfo !== null) {
2016 - forwardDebugInfo(request, task.id, debugInfo);
2039 + forwardDebugInfo(request, task, debugInfo);
2040 }
2041 return match;
2042 }
@@ -2049,12 +2072,16 @@ function emitAsyncSequence(
2072 const env = (0, request.environmentName)();
2073 // If we don't have any thing awaited, the time we started awaiting was internal
2074 // when we yielded after rendering. The cutOff time is basically that.
2052 - emitTimingChunk(request, task.id, cutOff);
2075 + const awaitStartTime = cutOff;
2076 + // If the end time finished before we started, it could've been a cached thing so
2077 + // we clamp it to the cutOff time. Effectively leading to a zero-time await.
2078 + const awaitEndTime = awaitedNode.end < cutOff ? cutOff : awaitedNode.end;
2079 + emitTimingChunk(request, task.id, awaitStartTime);
2080 emitDebugChunk(request, task.id, {
2081 awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
2082 env: env,
2083 });
2057 - emitTimingChunk(request, task.id, awaitedNode.end);
2084 + emitTimingChunk(request, task.id, awaitEndTime);
2085 }
2086 }
2087
@@ -2763,13 +2790,13 @@ function renderModelDestructive(
2790 const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
2791 if (debugInfo) {
2792 // If this came from Flight, forward any debug info into this new row.
2766 - if (debugID === null) {
2793 + if (!canEmitDebugInfo) {
2794 // We don't have a chunk to assign debug info. We need to outline this
2795 // component to assign it an ID.
2796 return outlineTask(request, task);
2797 } else {
2798 // Forward any debug info we have the first time we see it.
2772 - forwardDebugInfo(request, debugID, debugInfo);
2799 + forwardDebugInfo(request, task, debugInfo);
2800 }
2801 }
2802 }
@@ -2845,7 +2872,7 @@ function renderModelDestructive(
2872 const debugInfo: ?ReactDebugInfo = lazy._debugInfo;
2873 if (debugInfo) {
2874 // If this came from Flight, forward any debug info into this new row.
2848 - if (debugID === null) {
2875 + if (!canEmitDebugInfo) {
2876 // We don't have a chunk to assign debug info. We need to outline this
2877 // component to assign it an ID.
2878 return outlineTask(request, task);
@@ -2853,7 +2880,7 @@ function renderModelDestructive(
2880 // Forward any debug info we have the first time we see it.
2881 // We do this after init so that we have received all the debug info
2882 // from the server by the time we emit it.
2856 - forwardDebugInfo(request, debugID, debugInfo);
2883 + forwardDebugInfo(request, task, debugInfo);
2884 }
2885 }
2886 }
@@ -4264,57 +4291,77 @@ function emitTimeOriginChunk(request: Request, timeOrigin: number): void {
4291
4292 function forwardDebugInfo(
4293 request: Request,
4267 - id: number,
4294 + task: Task,
4295 debugInfo: ReactDebugInfo,
4296 ) {
4297 + const id = task.id;
4298 + const minimumTime =
4299 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0;
4300 for (let i = 0; i < debugInfo.length; i++) {
4301 const info = debugInfo[i];
4302 if (typeof info.time === 'number') {
4303 // When forwarding time we need to ensure to convert it to the time space of the payload.
4274 - emitTimingChunk(request, id, info.time);
4304 + // We clamp the time to the starting render of the current component. It's as if it took
4305 + // no time to render and await if we reuse cached content.
4306 + emitTimingChunk(
4307 + request,
4308 + id,
4309 + info.time < minimumTime ? minimumTime : info.time,
4310 + );
4311 } else {
4276 - request.pendingChunks++;
4312 if (typeof info.name === 'string') {
4313 // We outline this model eagerly so that we can refer to by reference as an owner.
4314 // If we had a smarter way to dedupe we might not have to do this if there ends up
4315 // being no references to this as an owner.
4316 outlineComponentInfo(request, (info: any));
4317 // Emit a reference to the outlined one.
4318 + request.pendingChunks++;
4319 emitDebugChunk(request, id, info);
4320 } else if (info.awaited) {
4321 const ioInfo = info.awaited;
4286 - // Outline the IO info in case the same I/O is awaited in more than one place.
4287 - outlineIOInfo(request, ioInfo);
4288 - // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
4289 - let debugStack;
4290 - if (info.stack == null && info.debugStack != null) {
4291 - // If we have a debugStack but no parsed stack we should parse it.
4292 - debugStack = filterStackTrace(
4293 - request,
4294 - parseStackTrace(info.debugStack, 1),
4295 - );
4322 + if (ioInfo.end <= request.timeOrigin) {
4323 + // This was already resolved when we started this render. It must have been some
4324 + // externally cached data. We exclude that information but we keep components and
4325 + // awaits that happened inside this render but might have been deduped within the
4326 + // render.
4327 } else {
4297 - debugStack = info.stack;
4298 - }
4299 - const debugAsyncInfo: Omit<ReactAsyncInfo, 'debugTask' | 'debugStack'> =
4300 - {
4328 + // Outline the IO info in case the same I/O is awaited in more than one place.
4329 + outlineIOInfo(request, ioInfo);
4330 + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
4331 + let debugStack;
4332 + if (info.stack == null && info.debugStack != null) {
4333 + // If we have a debugStack but no parsed stack we should parse it.
4334 + debugStack = filterStackTrace(
4335 + request,
4336 + parseStackTrace(info.debugStack, 1),
4337 + );
4338 + } else {
4339 + debugStack = info.stack;
4340 + }
4341 + const debugAsyncInfo: Omit<
4342 + ReactAsyncInfo,
4343 + 'debugTask' | 'debugStack',
4344 + > = {
4345 awaited: ioInfo,
4346 };
4303 - if (info.env != null) {
4304 - // $FlowFixMe[cannot-write]
4305 - debugAsyncInfo.env = info.env;
4306 - }
4307 - if (info.owner != null) {
4308 - // $FlowFixMe[cannot-write]
4309 - debugAsyncInfo.owner = info.owner;
4310 - }
4311 - if (debugStack != null) {
4312 - // $FlowFixMe[cannot-write]
4313 - debugAsyncInfo.stack = debugStack;
4347 + if (info.env != null) {
4348 + // $FlowFixMe[cannot-write]
4349 + debugAsyncInfo.env = info.env;
4350 + }
4351 + if (info.owner != null) {
4352 + // $FlowFixMe[cannot-write]
4353 + debugAsyncInfo.owner = info.owner;
4354 + }
4355 + if (debugStack != null) {
4356 + // $FlowFixMe[cannot-write]
4357 + debugAsyncInfo.stack = debugStack;
4358 + }
4359 + request.pendingChunks++;
4360 + emitDebugChunk(request, id, debugAsyncInfo);
4361 }
4315 - emitDebugChunk(request, id, debugAsyncInfo);
4362 } else {
4317 - emitDebugChunk(request, id, debugInfo[i]);
4363 + request.pendingChunks++;
4364 + emitDebugChunk(request, id, info);
4365 }
4366 }
4367 }
@@ -4457,7 +4504,7 @@ function retryTask(request: Request, task: Task): void {
4504 return;
4505 }
4506
4460 - const prevDebugID = debugID;
4507 + const prevCanEmitDebugInfo = canEmitDebugInfo;
4508 task.status = RENDERING;
4509
4510 // We stash the outer parent size so we can restore it when we exit.
@@ -4472,8 +4519,8 @@ function retryTask(request: Request, task: Task): void {
4519 modelRoot = task.model;
4520
4521 if (__DEV__) {
4475 - // Track the ID of the current task so we can assign debug info to this id.
4476 - debugID = task.id;
4522 + // Track that we can emit debug info for the current task.
4523 + canEmitDebugInfo = true;
4524 }
4525
4526 // We call the destructive form that mutates this task. That way if something
@@ -4489,7 +4536,7 @@ function retryTask(request: Request, task: Task): void {
4536 if (__DEV__) {
4537 // We're now past rendering this task and future renders will spawn new tasks for their
4538 // debug info.
4492 - debugID = null;
4539 + canEmitDebugInfo = false;
4540 }
4541
4542 // Track the root again for the resolved object.
@@ -4574,7 +4621,7 @@ function retryTask(request: Request, task: Task): void {
4621 erroredTask(request, task, x);
4622 } finally {
4623 if (__DEV__) {
4577 - debugID = prevDebugID;
4624 + canEmitDebugInfo = prevCanEmitDebugInfo;
4625 }
4626 serializedSize = parentSerializedSize;
4627 }
@@ -4583,11 +4630,11 @@ function retryTask(request: Request, task: Task): void {
4630 function tryStreamTask(request: Request, task: Task): void {
4631 // This is used to try to emit something synchronously but if it suspends,
4632 // we emit a reference to a new outlined task immediately instead.
4586 - const prevDebugID = debugID;
4633 + const prevCanEmitDebugInfo = canEmitDebugInfo;
4634 if (__DEV__) {
4588 - // We don't use the id of the stream task for debugID. Instead we leave it null
4589 - // so that we instead outline the row to get a new debugID if needed.
4590 - debugID = null;
4635 + // We can't emit debug into to a specific row of a stream task. Instead we leave
4636 + // it false so that we instead outline the row to get a new canEmitDebugInfo if needed.
4637 + canEmitDebugInfo = false;
4638 }
4639 const parentSerializedSize = serializedSize;
4640 try {
@@ -4595,7 +4642,7 @@ function tryStreamTask(request: Request, task: Task): void {
4642 } finally {
4643 serializedSize = parentSerializedSize;
4644 if (__DEV__) {
4598 - debugID = prevDebugID;
4645 + canEmitDebugInfo = prevCanEmitDebugInfo;
4646 }
4647 }
4648 }
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+465 -87
@@ -5,6 +5,8 @@ const path = require('path');
5 import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
6
7 let React;
8 +let ReactServer;
9 +let cache;
10 let ReactServerDOMServer;
11 let ReactServerDOMClient;
12 let Stream;
@@ -61,6 +63,9 @@ function normalizeDebugInfo(debugInfo) {
63 if (debugInfo.awaited) {
64 copy.awaited = normalizeIOInfo(copy.awaited);
65 }
66 + if (debugInfo.props) {
67 + copy.props = {};
68 + }
69 return copy;
70 } else if (typeof debugInfo.time === 'number') {
71 return {...debugInfo, time: 0};
@@ -83,6 +88,17 @@ function getDebugInfo(obj) {
88 return debugInfo;
89 }
90
91 +function filterStackFrame(filename, functionName) {
92 + return (
93 + filename !== '' &&
94 + !filename.startsWith('node:') &&
95 + !filename.includes('node_modules') &&
96 + // Filter out our own internal source code since it'll typically be in node_modules
97 + (!filename.includes('/packages/') || filename.includes('/__tests__/')) &&
98 + !filename.includes('/build/')
99 + );
100 +}
101 +
102 describe('ReactFlightAsyncDebugInfo', () => {
103 beforeEach(() => {
104 jest.resetModules();
@@ -94,7 +110,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
110 jest.mock('react-server-dom-webpack/server', () =>
111 require('react-server-dom-webpack/server.node'),
112 );
113 + ReactServer = require('react');
114 ReactServerDOMServer = require('react-server-dom-webpack/server');
115 + cache = ReactServer.cache;
116
117 jest.resetModules();
118 jest.useRealTimers();
@@ -135,16 +153,23 @@ describe('ReactFlightAsyncDebugInfo', () => {
153 }
154
155 it('can track async information when awaited', async () => {
138 - async function getData() {
156 + async function getData(text) {
157 await delay(1);
158 const promise = delay(2);
159 await Promise.all([promise]);
142 - return 'hi';
160 + return text.toUpperCase();
161 }
162
163 async function Component() {
146 - const result = await getData();
147 - return result;
164 + const result = await getData('hi');
165 + const moreData = getData('seb');
166 + return <InnerComponent text={result} promise={moreData} />;
167 + }
168 +
169 + async function InnerComponent({text, promise}) {
170 + // This async function depends on the I/O in parent components but it should not
171 + // include that I/O as part of its own meta data.
172 + return text + ', ' + (await promise);
173 }
174
175 const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
@@ -157,7 +182,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
182 });
183 stream.pipe(readable);
184
160 - expect(await result).toBe('hi');
185 + expect(await result).toBe('HI, SEB');
186 if (
187 __DEV__ &&
188 gate(
@@ -179,9 +204,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
204 [
205 "Object.<anonymous>",
206 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
182 - 150,
207 + 175,
208 109,
184 - 137,
209 + 155,
210 50,
211 ],
212 ],
@@ -203,9 +228,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
228 [
229 "Object.<anonymous>",
230 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
206 - 150,
231 + 175,
232 109,
208 - 137,
233 + 155,
234 50,
235 ],
236 ],
@@ -214,25 +239,25 @@ describe('ReactFlightAsyncDebugInfo', () => {
239 [
240 "delay",
241 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
217 - 115,
242 + 133,
243 12,
219 - 114,
244 + 132,
245 3,
246 ],
247 [
248 "getData",
249 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
225 - 139,
250 + 157,
251 13,
227 - 138,
252 + 156,
253 5,
254 ],
255 [
256 "Component",
257 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
233 - 146,
258 + 164,
259 26,
235 - 145,
260 + 163,
261 5,
262 ],
263 ],
@@ -248,9 +273,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
273 [
274 "Object.<anonymous>",
275 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
251 - 150,
276 + 175,
277 109,
253 - 137,
278 + 155,
279 50,
280 ],
281 ],
@@ -259,17 +284,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
284 [
285 "getData",
286 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
262 - 139,
287 + 157,
288 13,
264 - 138,
289 + 156,
290 5,
291 ],
292 [
293 "Component",
294 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
270 - 146,
295 + 164,
296 26,
272 - 145,
297 + 163,
298 5,
299 ],
300 ],
@@ -294,9 +319,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
319 [
320 "Object.<anonymous>",
321 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
297 - 150,
322 + 175,
323 109,
299 - 137,
324 + 155,
325 50,
326 ],
327 ],
@@ -305,25 +330,25 @@ describe('ReactFlightAsyncDebugInfo', () => {
330 [
331 "delay",
332 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
308 - 115,
333 + 133,
334 12,
310 - 114,
335 + 132,
336 3,
337 ],
338 [
339 "getData",
340 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
316 - 140,
341 + 158,
342 21,
318 - 138,
343 + 156,
344 5,
345 ],
346 [
347 "Component",
348 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
324 - 146,
349 + 164,
350 20,
326 - 145,
351 + 163,
352 5,
353 ],
354 ],
@@ -339,9 +364,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
364 [
365 "Object.<anonymous>",
366 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
342 - 150,
367 + 175,
368 109,
344 - 137,
369 + 155,
370 50,
371 ],
372 ],
@@ -350,17 +375,111 @@ describe('ReactFlightAsyncDebugInfo', () => {
375 [
376 "getData",
377 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
353 - 141,
378 + 159,
379 21,
355 - 138,
380 + 156,
381 5,
382 ],
383 [
384 "Component",
385 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
361 - 146,
386 + 164,
387 20,
363 - 145,
388 + 163,
389 + 5,
390 + ],
391 + ],
392 + },
393 + {
394 + "time": 0,
395 + },
396 + {
397 + "time": 0,
398 + },
399 + {
400 + "env": "Server",
401 + "key": null,
402 + "name": "InnerComponent",
403 + "props": {},
404 + "stack": [
405 + [
406 + "Component",
407 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
408 + 166,
409 + 60,
410 + 163,
411 + 5,
412 + ],
413 + ],
414 + },
415 + {
416 + "time": 0,
417 + },
418 + {
419 + "awaited": {
420 + "end": 0,
421 + "env": "Server",
422 + "name": "getData",
423 + "owner": {
424 + "env": "Server",
425 + "key": null,
426 + "name": "Component",
427 + "props": {},
428 + "stack": [
429 + [
430 + "Object.<anonymous>",
431 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
432 + 175,
433 + 109,
434 + 155,
435 + 50,
436 + ],
437 + ],
438 + },
439 + "stack": [
440 + [
441 + "getData",
442 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
443 + 156,
444 + 27,
445 + 156,
446 + 5,
447 + ],
448 + [
449 + "Component",
450 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
451 + 165,
452 + 22,
453 + 163,
454 + 5,
455 + ],
456 + ],
457 + "start": 0,
458 + },
459 + "env": "Server",
460 + "owner": {
461 + "env": "Server",
462 + "key": null,
463 + "name": "InnerComponent",
464 + "props": {},
465 + "stack": [
466 + [
467 + "Component",
468 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
469 + 166,
470 + 60,
471 + 163,
472 + 5,
473 + ],
474 + ],
475 + },
476 + "stack": [
477 + [
478 + "InnerComponent",
479 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
480 + 172,
481 + 35,
482 + 169,
483 5,
484 ],
485 ],
@@ -421,9 +540,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
540 [
541 "Object.<anonymous>",
542 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
424 - 392,
543 + 511,
544 109,
426 - 379,
545 + 498,
546 67,
547 ],
548 ],
@@ -445,9 +564,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
564 [
565 "Object.<anonymous>",
566 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
448 - 392,
567 + 511,
568 109,
450 - 379,
569 + 498,
570 67,
571 ],
572 ],
@@ -456,9 +575,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
575 [
576 "Component",
577 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
459 - 382,
578 + 501,
579 7,
461 - 380,
580 + 499,
581 5,
582 ],
583 ],
@@ -518,9 +637,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
637 [
638 "Object.<anonymous>",
639 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
521 - 489,
640 + 608,
641 109,
523 - 480,
642 + 599,
643 94,
644 ],
645 ],
@@ -589,9 +708,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
708 [
709 "Object.<anonymous>",
710 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
592 - 560,
711 + 679,
712 109,
594 - 536,
713 + 655,
714 50,
715 ],
716 ],
@@ -671,9 +790,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
790 [
791 "Object.<anonymous>",
792 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
674 - 642,
793 + 761,
794 109,
676 - 625,
795 + 744,
796 63,
797 ],
798 ],
@@ -690,17 +809,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
809 [
810 "fetchThirdParty",
811 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
693 - 122,
812 + 140,
813 40,
695 - 120,
814 + 138,
815 3,
816 ],
817 [
818 "Component",
819 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
701 - 638,
820 + 757,
821 24,
703 - 637,
822 + 756,
823 5,
824 ],
825 ],
@@ -722,17 +841,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
841 [
842 "fetchThirdParty",
843 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
725 - 122,
844 + 140,
845 40,
727 - 120,
846 + 138,
847 3,
848 ],
849 [
850 "Component",
851 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
733 - 638,
852 + 757,
853 24,
735 - 637,
854 + 756,
855 5,
856 ],
857 ],
@@ -741,25 +860,25 @@ describe('ReactFlightAsyncDebugInfo', () => {
860 [
861 "delay",
862 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
744 - 115,
863 + 133,
864 12,
746 - 114,
865 + 132,
866 3,
867 ],
868 [
869 "getData",
870 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
752 - 627,
871 + 746,
872 13,
754 - 626,
873 + 745,
874 5,
875 ],
876 [
877 "ThirdPartyComponent",
878 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
760 - 633,
879 + 752,
880 24,
762 - 632,
881 + 751,
882 5,
883 ],
884 ],
@@ -775,17 +894,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
894 [
895 "fetchThirdParty",
896 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
778 - 122,
897 + 140,
898 40,
780 - 120,
899 + 138,
900 3,
901 ],
902 [
903 "Component",
904 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
786 - 638,
905 + 757,
906 24,
788 - 637,
907 + 756,
908 5,
909 ],
910 ],
@@ -794,17 +913,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
913 [
914 "getData",
915 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
797 - 627,
916 + 746,
917 13,
799 - 626,
918 + 745,
919 5,
920 ],
921 [
922 "ThirdPartyComponent",
923 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
805 - 633,
924 + 752,
925 24,
807 - 632,
926 + 751,
927 5,
928 ],
929 ],
@@ -829,17 +948,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
948 [
949 "fetchThirdParty",
950 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
832 - 122,
951 + 140,
952 40,
834 - 120,
953 + 138,
954 3,
955 ],
956 [
957 "Component",
958 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
840 - 638,
959 + 757,
960 24,
842 - 637,
961 + 756,
962 5,
963 ],
964 ],
@@ -848,25 +967,25 @@ describe('ReactFlightAsyncDebugInfo', () => {
967 [
968 "delay",
969 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
851 - 115,
970 + 133,
971 12,
853 - 114,
972 + 132,
973 3,
974 ],
975 [
976 "getData",
977 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
859 - 628,
978 + 747,
979 13,
861 - 626,
980 + 745,
981 5,
982 ],
983 [
984 "ThirdPartyComponent",
985 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
867 - 633,
986 + 752,
987 18,
869 - 632,
988 + 751,
989 5,
990 ],
991 ],
@@ -882,17 +1001,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
1001 [
1002 "fetchThirdParty",
1003 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
885 - 122,
1004 + 140,
1005 40,
887 - 120,
1006 + 138,
1007 3,
1008 ],
1009 [
1010 "Component",
1011 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
893 - 638,
1012 + 757,
1013 24,
895 - 637,
1014 + 756,
1015 5,
1016 ],
1017 ],
@@ -901,17 +1020,185 @@ describe('ReactFlightAsyncDebugInfo', () => {
1020 [
1021 "getData",
1022 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
904 - 628,
1023 + 747,
1024 13,
906 - 626,
1025 + 745,
1026 5,
1027 ],
1028 [
1029 "ThirdPartyComponent",
1030 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
912 - 633,
1031 + 752,
1032 18,
914 - 632,
1033 + 751,
1034 + 5,
1035 + ],
1036 + ],
1037 + },
1038 + {
1039 + "time": 0,
1040 + },
1041 + {
1042 + "time": 0,
1043 + },
1044 + {
1045 + "time": 0,
1046 + },
1047 + ]
1048 + `);
1049 + }
1050 + });
1051 +
1052 + it('can track cached entries awaited in later components', async () => {
1053 + let cacheKey;
1054 + let cacheValue;
1055 + const getData = cache(async function getData(text) {
1056 + if (cacheKey === text) {
1057 + return cacheValue;
1058 + }
1059 + await delay(1);
1060 + return text.toUpperCase();
1061 + });
1062 +
1063 + async function Child() {
1064 + const greeting = await getData('hi');
1065 + return greeting + ', Seb';
1066 + }
1067 +
1068 + async function Component() {
1069 + await getData('hi');
1070 + return <Child />;
1071 + }
1072 +
1073 + const stream = ReactServerDOMServer.renderToPipeableStream(
1074 + <Component />,
1075 + {},
1076 + {
1077 + filterStackFrame,
1078 + },
1079 + );
1080 +
1081 + const readable = new Stream.PassThrough(streamOptions);
1082 +
1083 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
1084 + moduleMap: {},
1085 + moduleLoading: {},
1086 + });
1087 + stream.pipe(readable);
1088 +
1089 + expect(await result).toBe('HI, Seb');
1090 + if (
1091 + __DEV__ &&
1092 + gate(
1093 + flags =>
1094 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
1095 + )
1096 + ) {
1097 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
1098 + [
1099 + {
1100 + "time": 0,
1101 + },
1102 + {
1103 + "env": "Server",
1104 + "key": null,
1105 + "name": "Component",
1106 + "props": {},
1107 + "stack": [
1108 + [
1109 + "Object.<anonymous>",
1110 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1111 + 1074,
1112 + 40,
1113 + 1052,
1114 + 62,
1115 + ],
1116 + ],
1117 + },
1118 + {
1119 + "time": 0,
1120 + },
1121 + {
1122 + "awaited": {
1123 + "end": 0,
1124 + "env": "Server",
1125 + "name": "delay",
1126 + "owner": {
1127 + "env": "Server",
1128 + "key": null,
1129 + "name": "Component",
1130 + "props": {},
1131 + "stack": [
1132 + [
1133 + "Object.<anonymous>",
1134 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1135 + 1074,
1136 + 40,
1137 + 1052,
1138 + 62,
1139 + ],
1140 + ],
1141 + },
1142 + "stack": [
1143 + [
1144 + "delay",
1145 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1146 + 133,
1147 + 12,
1148 + 132,
1149 + 3,
1150 + ],
1151 + [
1152 + "getData",
1153 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1154 + 1059,
1155 + 13,
1156 + 1055,
1157 + 25,
1158 + ],
1159 + [
1160 + "Component",
1161 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1162 + 1069,
1163 + 13,
1164 + 1068,
1165 + 5,
1166 + ],
1167 + ],
1168 + "start": 0,
1169 + },
1170 + "env": "Server",
1171 + "owner": {
1172 + "env": "Server",
1173 + "key": null,
1174 + "name": "Component",
1175 + "props": {},
1176 + "stack": [
1177 + [
1178 + "Object.<anonymous>",
1179 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1180 + 1074,
1181 + 40,
1182 + 1052,
1183 + 62,
1184 + ],
1185 + ],
1186 + },
1187 + "stack": [
1188 + [
1189 + "getData",
1190 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1191 + 1059,
1192 + 13,
1193 + 1055,
1194 + 25,
1195 + ],
1196 + [
1197 + "Component",
1198 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1199 + 1069,
1200 + 13,
1201 + 1068,
1202 5,
1203 ],
1204 ],
@@ -922,6 +1209,97 @@ describe('ReactFlightAsyncDebugInfo', () => {
1209 {
1210 "time": 0,
1211 },
1212 + {
1213 + "env": "Server",
1214 + "key": null,
1215 + "name": "Child",
1216 + "props": {},
1217 + "stack": [
1218 + [
1219 + "Component",
1220 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1221 + 1070,
1222 + 60,
1223 + 1068,
1224 + 5,
1225 + ],
1226 + ],
1227 + },
1228 + {
1229 + "time": 0,
1230 + },
1231 + {
1232 + "awaited": {
1233 + "end": 0,
1234 + "env": "Server",
1235 + "name": "getData",
1236 + "owner": {
1237 + "env": "Server",
1238 + "key": null,
1239 + "name": "Component",
1240 + "props": {},
1241 + "stack": [
1242 + [
1243 + "Object.<anonymous>",
1244 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1245 + 1074,
1246 + 40,
1247 + 1052,
1248 + 62,
1249 + ],
1250 + ],
1251 + },
1252 + "stack": [
1253 + [
1254 + "getData",
1255 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1256 + 1055,
1257 + 47,
1258 + 1055,
1259 + 25,
1260 + ],
1261 + [
1262 + "Component",
1263 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1264 + 1069,
1265 + 13,
1266 + 1068,
1267 + 5,
1268 + ],
1269 + ],
1270 + "start": 0,
1271 + },
1272 + "env": "Server",
1273 + "owner": {
1274 + "env": "Server",
1275 + "key": null,
1276 + "name": "Child",
1277 + "props": {},
1278 + "stack": [
1279 + [
1280 + "Component",
1281 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1282 + 1070,
1283 + 60,
1284 + 1068,
1285 + 5,
1286 + ],
1287 + ],
1288 + },
1289 + "stack": [
1290 + [
1291 + "Child",
1292 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1293 + 1064,
1294 + 28,
1295 + 1063,
1296 + 5,
1297 + ],
1298 + ],
1299 + },
1300 + {
1301 + "time": 0,
1302 + },
1303 {
1304 "time": 0,
1305 },