@samitouri / QOS-React-2 / commits / 37054867c1

[Flight] Forward debugInfo from awaited instrumented Promises (#33415)

Stacked on #33403. When a Promise is coming from React such as when it's passed from another environment, we should forward the debug information from that environment. We already do that when rendered as a child. This makes it possible to also `await promise` and have the information from that instrumented promise carry through to the next render. This is a bit tricky because the current protocol is that we have to read it from the Promise after it resolves so it has time to be assigned to the promise. `async_hooks` doesn't pass us the instance (even though it has it) when it gets resolved so we need to keep it around. However, we have to be very careful because if we get this wrong it'll cause a memory leak since we retain things by `asyncId` and then manually listen for `destroy()` which can only be called once a Promise is GC:ed, which it can't be if we retain it. We have to therefore use a `WeakRef` in case it never resolves, and then read the `_debugInfo` when it resolves. We could maybe install a setter or something instead but that's also heavy. The other issues is that we don't use native Promises in ReactFlightClient so our instrumented promises aren't picked up by the `async_hooks` implementation and so we never get a handle to our thenable instance. To solve this we can create a native wrapper only in DEV.

Sebastian Markbåge committed Jun 4, 2025 at 00:49 UTC 37054867c15a7381abe0f73d98f3fecd06da52da
12 files changed +733 -92
.eslintrc.js
+1
@@ -561,6 +561,7 @@ module.exports = {
561 ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
562 ReturnType: 'readonly',
563 AnimationFrameID: 'readonly',
564 + WeakRef: 'readonly',
565 // For Flow type annotation. Only `BigInt` is valid at runtime.
566 bigint: 'readonly',
567 BigInt: 'readonly',
packages/react-client/src/ReactFlightClient.js
+21
@@ -266,6 +266,27 @@ ReactPromise.prototype.then = function <T>(
266 initializeModuleChunk(chunk);
267 break;
268 }
269 + if (__DEV__ && enableAsyncDebugInfo) {
270 + // Because only native Promises get picked up when we're awaiting we need to wrap
271 + // this in a native Promise in DEV. This means that these callbacks are no longer sync
272 + // but the lazy initialization is still sync and the .value can be inspected after,
273 + // allowing it to be read synchronously anyway.
274 + const resolveCallback = resolve;
275 + const rejectCallback = reject;
276 + const wrapperPromise: Promise<T> = new Promise((res, rej) => {
277 + resolve = value => {
278 + // $FlowFixMe
279 + wrapperPromise._debugInfo = this._debugInfo;
280 + res(value);
281 + };
282 + reject = reason => {
283 + // $FlowFixMe
284 + wrapperPromise._debugInfo = this._debugInfo;
285 + rej(reason);
286 + };
287 + });
288 + wrapperPromise.then(resolveCallback, rejectCallback);
289 + }
290 // The status might have changed after initialization.
291 switch (chunk.status) {
292 case INITIALIZED:
packages/react-server/src/ReactFlightAsyncSequence.js
+39 -3
@@ -7,25 +7,33 @@
7 * @flow
8 */
9
10 -import type {ReactComponentInfo} from 'shared/ReactTypes';
10 +import type {ReactDebugInfo, ReactComponentInfo} from 'shared/ReactTypes';
11
12 export const IO_NODE = 0;
13 export const PROMISE_NODE = 1;
14 export const AWAIT_NODE = 2;
15 +export const UNRESOLVED_PROMISE_NODE = 3;
16 +export const UNRESOLVED_AWAIT_NODE = 4;
17 +
18 +type PromiseWithDebugInfo = interface extends Promise<any> {
19 + _debugInfo?: ReactDebugInfo,
20 +};
21
22 export type IONode = {
23 tag: 0,
24 owner: null | ReactComponentInfo,
25 stack: Error, // callsite that spawned the I/O
26 + debugInfo: null, // not used on I/O
27 start: number, // start time when the first part of the I/O sequence started
28 end: number, // we typically don't use this. only when there's no promise intermediate.
29 awaited: null, // I/O is only blocked on external.
23 - previous: null | AwaitNode, // the preceeding await that spawned this new work
30 + previous: null | AwaitNode | UnresolvedAwaitNode, // the preceeding await that spawned this new work
31 };
32
33 export type PromiseNode = {
34 tag: 1,
35 owner: null | ReactComponentInfo,
36 + debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
37 stack: Error, // callsite that created the Promise
38 start: number, // start time when the Promise was created
39 end: number, // end time when the Promise was resolved.
@@ -36,6 +44,7 @@ export type PromiseNode = {
44 export type AwaitNode = {
45 tag: 2,
46 owner: null | ReactComponentInfo,
47 + debugInfo: null | ReactDebugInfo, // forwarded debugInfo from the Promise
48 stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
49 start: number, // when we started blocking. This might be later than the I/O started.
50 end: number, // when we unblocked. This might be later than the I/O resolved if there's CPU time.
@@ -43,4 +52,31 @@ export type AwaitNode = {
52 previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
53 };
54
46 -export type AsyncSequence = IONode | PromiseNode | AwaitNode;
55 +export type UnresolvedPromiseNode = {
56 + tag: 3,
57 + owner: null | ReactComponentInfo,
58 + debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
59 + stack: Error, // callsite that created the Promise
60 + start: number, // start time when the Promise was created
61 + end: -1.1, // set when we resolve.
62 + awaited: null | AsyncSequence, // the thing that ended up resolving this promise
63 + previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
64 +};
65 +
66 +export type UnresolvedAwaitNode = {
67 + tag: 4,
68 + owner: null | ReactComponentInfo,
69 + debugInfo: WeakRef<PromiseWithDebugInfo>, // holds onto the Promise until we can extract debugInfo when it resolves
70 + stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
71 + start: number, // when we started blocking. This might be later than the I/O started.
72 + end: -1.1, // set when we resolve.
73 + awaited: null | AsyncSequence, // the promise we were waiting on
74 + previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
75 +};
76 +
77 +export type AsyncSequence =
78 + | IONode
79 + | PromiseNode
80 + | AwaitNode
81 + | UnresolvedPromiseNode
82 + | UnresolvedAwaitNode;
packages/react-server/src/ReactFlightServer.js
+88 -43
@@ -89,6 +89,7 @@ import {
89 requestStorage,
90 createHints,
91 initAsyncDebugInfo,
92 + markAsyncSequenceRootTask,
93 getCurrentAsyncSequence,
94 parseStackTrace,
95 supportsComponentStorage,
@@ -149,7 +150,13 @@ import binaryToComparableString from 'shared/binaryToComparableString';
150
151 import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
152
152 -import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
153 +import {
154 + IO_NODE,
155 + PROMISE_NODE,
156 + AWAIT_NODE,
157 + UNRESOLVED_AWAIT_NODE,
158 + UNRESOLVED_PROMISE_NODE,
159 +} from './ReactFlightAsyncSequence';
160
161 // DEV-only set containing internal objects that should not be limited and turned into getters.
162 const doNotLimit: WeakSet<Reference> = __DEV__ ? new WeakSet() : (null: any);
@@ -1879,6 +1886,9 @@ function visitAsyncNode(
1886 case IO_NODE: {
1887 return node;
1888 }
1889 + case UNRESOLVED_PROMISE_NODE: {
1890 + return null;
1891 + }
1892 case PROMISE_NODE: {
1893 if (node.end < cutOff) {
1894 // This was already resolved when we started this sequence. It must have been
@@ -1888,6 +1898,7 @@ function visitAsyncNode(
1898 return null;
1899 }
1900 const awaited = node.awaited;
1901 + let match = null;
1902 if (awaited !== null) {
1903 const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1904 if (ioNode !== null) {
@@ -1907,72 +1918,104 @@ function visitAsyncNode(
1918 // If we haven't defined an end time, use the resolve of the outer Promise.
1919 ioNode.end = node.end;
1920 }
1910 - return ioNode;
1921 + match = ioNode;
1922 + } else {
1923 + match = node;
1924 }
1912 - return node;
1925 }
1926 }
1915 - return null;
1927 + // We need to forward after we visit awaited nodes because what ever I/O we requested that's
1928 + // the thing that generated this node and its virtual children.
1929 + const debugInfo = node.debugInfo;
1930 + if (debugInfo !== null) {
1931 + forwardDebugInfo(request, task.id, debugInfo);
1932 + }
1933 + return match;
1934 }
1935 + case UNRESOLVED_AWAIT_NODE:
1936 + // We could be inside the .then() which is about to resolve this node.
1937 + // TODO: We could call emitAsyncSequence in a microtask to avoid this issue.
1938 + // Fallthrough to the resolved path.
1939 case AWAIT_NODE: {
1940 const awaited = node.awaited;
1941 + let match = null;
1942 if (awaited !== null) {
1943 const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1944 if (ioNode !== null) {
1922 - if (node.end < 0) {
1945 + let endTime: number;
1946 + if (node.tag === UNRESOLVED_AWAIT_NODE) {
1947 // If we haven't defined an end time, use the resolve of the inner Promise.
1948 // This can happen because the ping gets invoked before the await gets resolved.
1949 if (ioNode.end < node.start) {
1950 // If we're awaiting a resolved Promise it could have finished before we started.
1927 - node.end = node.start;
1951 + endTime = node.start;
1952 } else {
1929 - node.end = ioNode.end;
1953 + endTime = ioNode.end;
1954 }
1955 + } else {
1956 + endTime = node.end;
1957 }
1932 - if (node.end < cutOff) {
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.
1937 - return null;
1938 - }
1939 -
1940 - const stack = filterStackTrace(
1941 - request,
1942 - parseStackTrace(node.stack, 1),
1943 - );
1944 - if (stack.length === 0) {
1945 - // If this await was fully filtered out, then it was inside third party code
1946 - // such as in an external library. We return the I/O node and try another await.
1947 - return ioNode;
1948 - }
1949 - // Outline the IO node.
1950 - serializeIONode(request, ioNode);
1951 - // We log the environment at the time when the last promise pigned ping which may
1952 - // be later than what the environment was when we actually started awaiting.
1953 - const env = (0, request.environmentName)();
1954 - if (node.start <= cutOff) {
1955 - // If this was an await that started before this sequence but finished after,
1956 - // then we clamp it to the start of this sequence. We don't need to emit a time
1957 - // TODO: Typically we'll already have a previous time stamp with the cutOff time
1958 - // so we shouldn't need to emit another one. But not always.
1959 - emitTimingChunk(request, task.id, cutOff);
1963 } else {
1961 - emitTimingChunk(request, task.id, node.start);
1964 + const stack = filterStackTrace(
1965 + request,
1966 + parseStackTrace(node.stack, 1),
1967 + );
1968 + if (stack.length === 0) {
1969 + // If this await was fully filtered out, then it was inside third party code
1970 + // such as in an external library. We return the I/O node and try another await.
1971 + match = ioNode;
1972 + } else {
1973 + // Outline the IO node.
1974 + if (ioNode.end < 0) {
1975 + ioNode.end = endTime;
1976 + }
1977 + serializeIONode(request, ioNode);
1978 + // We log the environment at the time when the last promise pigned ping which may
1979 + // be later than what the environment was when we actually started awaiting.
1980 + 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 + }
1990 + // Then emit a reference to us awaiting it in the current task.
1991 + request.pendingChunks++;
1992 + emitDebugChunk(request, task.id, {
1993 + awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
1994 + env: env,
1995 + owner: node.owner,
1996 + stack: stack,
1997 + });
1998 + emitTimingChunk(request, task.id, node.end);
1999 + }
2000 }
1963 - // Then emit a reference to us awaiting it in the current task.
1964 - request.pendingChunks++;
1965 - emitDebugChunk(request, task.id, {
1966 - awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
1967 - env: env,
1968 - owner: node.owner,
1969 - stack: stack,
1970 - });
1971 - emitTimingChunk(request, task.id, node.end);
2001 }
2002 }
1974 - // If we had awaited anything we would have written it now.
1975 - return null;
2003 + // We need to forward after we visit awaited nodes because what ever I/O we requested that's
2004 + // the thing that generated this node and its virtual children.
2005 + let debugInfo: null | ReactDebugInfo;
2006 + if (node.tag === UNRESOLVED_AWAIT_NODE) {
2007 + const promise = node.debugInfo.deref();
2008 + debugInfo =
2009 + promise === undefined || promise._debugInfo === undefined
2010 + ? null
2011 + : promise._debugInfo;
2012 + } else {
2013 + debugInfo = node.debugInfo;
2014 + }
2015 + if (debugInfo !== null) {
2016 + forwardDebugInfo(request, task.id, debugInfo);
2017 + }
2018 + return match;
2019 }
2020 default: {
2021 // eslint-disable-next-line react-internal/prod-error-codes
@@ -4513,6 +4556,8 @@ function tryStreamTask(request: Request, task: Task): void {
4556 }
4557
4558 function performWork(request: Request): void {
4559 + markAsyncSequenceRootTask();
4560 +
4561 const prevDispatcher = ReactSharedInternals.H;
4562 ReactSharedInternals.H = HooksDispatcher;
4563 const prevRequest = currentRequest;
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+68 -14
@@ -11,10 +11,18 @@ import type {
11 AsyncSequence,
12 IONode,
13 PromiseNode,
14 + UnresolvedPromiseNode,
15 AwaitNode,
16 + UnresolvedAwaitNode,
17 } from './ReactFlightAsyncSequence';
18
17 -import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
19 +import {
20 + IO_NODE,
21 + PROMISE_NODE,
22 + UNRESOLVED_PROMISE_NODE,
23 + AWAIT_NODE,
24 + UNRESOLVED_AWAIT_NODE,
25 +} from './ReactFlightAsyncSequence';
26 import {resolveOwner} from './flight/ReactFlightCurrentOwner';
27 import {createHook, executionAsyncId} from 'async_hooks';
28 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
@@ -30,7 +38,12 @@ const pendingOperations: Map<number, AsyncSequence> =
38 export function initAsyncDebugInfo(): void {
39 if (__DEV__ && enableAsyncDebugInfo) {
40 createHook({
33 - init(asyncId: number, type: string, triggerAsyncId: number): void {
41 + init(
42 + asyncId: number,
43 + type: string,
44 + triggerAsyncId: number,
45 + resource: any,
46 + ): void {
47 const trigger = pendingOperations.get(triggerAsyncId);
48 let node: AsyncSequence;
49 if (type === 'PROMISE') {
@@ -46,18 +59,20 @@ export function initAsyncDebugInfo(): void {
59 // If the thing we're waiting on is another Await we still track that sequence
60 // so that we can later pick the best stack trace in user space.
61 node = ({
49 - tag: AWAIT_NODE,
62 + tag: UNRESOLVED_AWAIT_NODE,
63 owner: resolveOwner(),
64 + debugInfo: new WeakRef((resource: Promise<any>)),
65 stack: new Error(),
66 start: performance.now(),
67 end: -1.1, // set when resolved.
68 awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
69 previous: current === undefined ? null : current, // The path that led us here.
56 - }: AwaitNode);
70 + }: UnresolvedAwaitNode);
71 } else {
72 node = ({
59 - tag: PROMISE_NODE,
73 + tag: UNRESOLVED_PROMISE_NODE,
74 owner: resolveOwner(),
75 + debugInfo: new WeakRef((resource: Promise<any>)),
76 stack: new Error(),
77 start: performance.now(),
78 end: -1.1, // Set when we resolve.
@@ -66,7 +81,7 @@ export function initAsyncDebugInfo(): void {
81 ? null // It might get overridden when we resolve.
82 : trigger,
83 previous: null,
69 - }: PromiseNode);
84 + }: UnresolvedPromiseNode);
85 }
86 } else if (
87 type !== 'Microtask' &&
@@ -78,17 +93,22 @@ export function initAsyncDebugInfo(): void {
93 node = ({
94 tag: IO_NODE,
95 owner: resolveOwner(),
96 + debugInfo: null,
97 stack: new Error(), // This is only used if no native promises are used.
98 start: performance.now(),
99 end: -1.1, // Only set when pinged.
100 awaited: null,
101 previous: null,
102 }: IONode);
87 - } else if (trigger.tag === AWAIT_NODE) {
103 + } else if (
104 + trigger.tag === AWAIT_NODE ||
105 + trigger.tag === UNRESOLVED_AWAIT_NODE
106 + ) {
107 // We have begun a new I/O sequence after the await.
108 node = ({
109 tag: IO_NODE,
110 owner: resolveOwner(),
111 + debugInfo: null,
112 stack: new Error(),
113 start: performance.now(),
114 end: -1.1, // Only set when pinged.
@@ -110,16 +130,41 @@ export function initAsyncDebugInfo(): void {
130 pendingOperations.set(asyncId, node);
131 },
132 promiseResolve(asyncId: number): void {
113 - const resolvedNode = pendingOperations.get(asyncId);
114 - if (resolvedNode !== undefined) {
115 - if (resolvedNode.tag === IO_NODE) {
116 - // eslint-disable-next-line react-internal/prod-error-codes
117 - throw new Error(
118 - 'A Promise should never be an IO_NODE. This is a bug in React.',
119 - );
133 + const node = pendingOperations.get(asyncId);
134 + if (node !== undefined) {
135 + let resolvedNode: AwaitNode | PromiseNode;
136 + switch (node.tag) {
137 + case UNRESOLVED_AWAIT_NODE: {
138 + const awaitNode: AwaitNode = (node: any);
139 + awaitNode.tag = AWAIT_NODE;
140 + resolvedNode = awaitNode;
141 + break;
142 + }
143 + case UNRESOLVED_PROMISE_NODE: {
144 + const promiseNode: PromiseNode = (node: any);
145 + promiseNode.tag = PROMISE_NODE;
146 + resolvedNode = promiseNode;
147 + break;
148 + }
149 + case IO_NODE:
150 + // eslint-disable-next-line react-internal/prod-error-codes
151 + throw new Error(
152 + 'A Promise should never be an IO_NODE. This is a bug in React.',
153 + );
154 + default:
155 + // eslint-disable-next-line react-internal/prod-error-codes
156 + throw new Error(
157 + 'A Promise should never be resolved twice. This is a bug in React or Node.js.',
158 + );
159 }
160 // Log the end time when we resolved the promise.
161 resolvedNode.end = performance.now();
162 + // The Promise can be garbage collected after this so we should extract debugInfo first.
163 + const promise = node.debugInfo.deref();
164 + resolvedNode.debugInfo =
165 + promise === undefined || promise._debugInfo === undefined
166 + ? null
167 + : promise._debugInfo;
168 const currentAsyncId = executionAsyncId();
169 if (asyncId !== currentAsyncId) {
170 // If the promise was not resolved by itself, then that means that
@@ -140,6 +185,15 @@ export function initAsyncDebugInfo(): void {
185 }
186 }
187
188 +export function markAsyncSequenceRootTask(): void {
189 + if (__DEV__ && enableAsyncDebugInfo) {
190 + // Whatever Task we're running now is spawned by React itself to perform render work.
191 + // Don't track any cause beyond this task. We may still track I/O that was started outside
192 + // React but just not the cause of entering the render.
193 + pendingOperations.delete(executionAsyncId());
194 + }
195 +}
196 +
197 export function getCurrentAsyncSequence(): null | AsyncSequence {
198 if (!__DEV__ || !enableAsyncDebugInfo) {
199 return null;
packages/react-server/src/ReactFlightServerConfigDebugNoop.js
+1
@@ -11,6 +11,7 @@ import type {AsyncSequence} from './ReactFlightAsyncSequence';
11
12 // Exported for runtimes that don't support Promise instrumentation for async debugging.
13 export function initAsyncDebugInfo(): void {}
14 +export function markAsyncSequenceRootTask(): void {}
15 export function getCurrentAsyncSequence(): null | AsyncSequence {
16 return null;
17 }
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+510 -32
@@ -117,6 +117,23 @@ describe('ReactFlightAsyncDebugInfo', () => {
117 });
118 }
119
120 + function fetchThirdParty(Component) {
121 + const stream = ReactServerDOMServer.renderToPipeableStream(
122 + <Component />,
123 + {},
124 + {
125 + environmentName: 'third-party',
126 + },
127 + );
128 + const readable = new Stream.PassThrough(streamOptions);
129 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
130 + moduleMap: {},
131 + moduleLoading: {},
132 + });
133 + stream.pipe(readable);
134 + return result;
135 + }
136 +
137 it('can track async information when awaited', async () => {
138 async function getData() {
139 await delay(1);
@@ -163,9 +180,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
180 [
181 "Object.<anonymous>",
182 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
166 - 133,
183 + 150,
184 109,
168 - 120,
185 + 137,
186 50,
187 ],
188 ],
@@ -188,9 +205,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
205 [
206 "Object.<anonymous>",
207 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
191 - 133,
208 + 150,
209 109,
193 - 120,
210 + 137,
211 50,
212 ],
213 ],
@@ -207,17 +224,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
224 [
225 "getData",
226 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
210 - 122,
227 + 139,
228 13,
212 - 121,
229 + 138,
230 5,
231 ],
232 [
233 "Component",
234 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
218 - 129,
235 + 146,
236 26,
220 - 128,
237 + 145,
238 5,
239 ],
240 ],
@@ -234,9 +251,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
251 [
252 "Object.<anonymous>",
253 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
237 - 133,
254 + 150,
255 109,
239 - 120,
256 + 137,
257 50,
258 ],
259 ],
@@ -245,17 +262,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
262 [
263 "getData",
264 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
248 - 122,
265 + 139,
266 13,
250 - 121,
267 + 138,
268 5,
269 ],
270 [
271 "Component",
272 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
256 - 129,
273 + 146,
274 26,
258 - 128,
275 + 145,
276 5,
277 ],
278 ],
@@ -281,9 +298,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
298 [
299 "Object.<anonymous>",
300 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
284 - 133,
301 + 150,
302 109,
286 - 120,
303 + 137,
304 50,
305 ],
306 ],
@@ -300,17 +317,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
317 [
318 "getData",
319 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
303 - 123,
320 + 140,
321 21,
305 - 121,
322 + 138,
323 5,
324 ],
325 [
326 "Component",
327 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
311 - 129,
328 + 146,
329 20,
313 - 128,
330 + 145,
331 5,
332 ],
333 ],
@@ -327,9 +344,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
344 [
345 "Object.<anonymous>",
346 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
330 - 133,
347 + 150,
348 109,
332 - 120,
349 + 137,
350 50,
351 ],
352 ],
@@ -338,17 +355,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
355 [
356 "getData",
357 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
341 - 124,
358 + 141,
359 21,
343 - 121,
360 + 138,
361 5,
362 ],
363 [
364 "Component",
365 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
349 - 129,
366 + 146,
367 20,
351 - 128,
368 + 145,
369 5,
370 ],
371 ],
@@ -410,9 +427,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
427 [
428 "Object.<anonymous>",
429 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
413 - 380,
430 + 397,
431 109,
415 - 367,
432 + 384,
433 67,
434 ],
435 ],
@@ -435,9 +452,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
452 [
453 "Object.<anonymous>",
454 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
438 - 380,
455 + 397,
456 109,
440 - 367,
457 + 384,
458 67,
459 ],
460 ],
@@ -446,9 +463,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
463 [
464 "Component",
465 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
449 - 370,
466 + 387,
467 7,
451 - 368,
468 + 385,
469 5,
470 ],
471 ],
@@ -466,4 +483,465 @@ describe('ReactFlightAsyncDebugInfo', () => {
483 `);
484 }
485 });
486 +
487 + it('can ingores the start of I/O when immediately resolved non-native promise is awaited', async () => {
488 + async function Component() {
489 + return await {
490 + then(callback) {
491 + callback('hi');
492 + },
493 + };
494 + }
495 +
496 + const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
497 +
498 + const readable = new Stream.PassThrough(streamOptions);
499 +
500 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
501 + moduleMap: {},
502 + moduleLoading: {},
503 + });
504 + stream.pipe(readable);
505 +
506 + expect(await result).toBe('hi');
507 + if (
508 + __DEV__ &&
509 + gate(
510 + flags =>
511 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
512 + )
513 + ) {
514 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
515 + [
516 + {
517 + "time": 0,
518 + },
519 + {
520 + "env": "Server",
521 + "key": null,
522 + "name": "Component",
523 + "owner": null,
524 + "props": {},
525 + "stack": [
526 + [
527 + "Object.<anonymous>",
528 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
529 + 496,
530 + 109,
531 + 487,
532 + 94,
533 + ],
534 + ],
535 + },
536 + {
537 + "time": 0,
538 + },
539 + ]
540 + `);
541 + }
542 + });
543 +
544 + it('forwards debugInfo from awaited Promises', async () => {
545 + async function Component() {
546 + let resolve;
547 + const promise = new Promise(r => (resolve = r));
548 + promise._debugInfo = [
549 + {time: performance.now()},
550 + {
551 + name: 'Virtual Component',
552 + },
553 + {time: performance.now()},
554 + ];
555 + const promise2 = promise.then(value => value);
556 + promise2._debugInfo = [
557 + {time: performance.now()},
558 + {
559 + name: 'Virtual Component2',
560 + },
561 + {time: performance.now()},
562 + ];
563 + resolve('hi');
564 + const result = await promise2;
565 + return result.toUpperCase();
566 + }
567 +
568 + const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
569 +
570 + const readable = new Stream.PassThrough(streamOptions);
571 +
572 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
573 + moduleMap: {},
574 + moduleLoading: {},
575 + });
576 + stream.pipe(readable);
577 +
578 + expect(await result).toBe('HI');
579 + if (
580 + __DEV__ &&
581 + gate(
582 + flags =>
583 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
584 + )
585 + ) {
586 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
587 + [
588 + {
589 + "time": 0,
590 + },
591 + {
592 + "env": "Server",
593 + "key": null,
594 + "name": "Component",
595 + "owner": null,
596 + "props": {},
597 + "stack": [
598 + [
599 + "Object.<anonymous>",
600 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
601 + 568,
602 + 109,
603 + 544,
604 + 50,
605 + ],
606 + ],
607 + },
608 + {
609 + "time": 0,
610 + },
611 + {
612 + "name": "Virtual Component",
613 + },
614 + {
615 + "time": 0,
616 + },
617 + {
618 + "time": 0,
619 + },
620 + {
621 + "name": "Virtual Component2",
622 + },
623 + {
624 + "time": 0,
625 + },
626 + {
627 + "time": 0,
628 + },
629 + ]
630 + `);
631 + }
632 + });
633 +
634 + it('forwards async debug info one environment to the next', async () => {
635 + async function getData() {
636 + await delay(1);
637 + await delay(2);
638 + return 'hi';
639 + }
640 +
641 + async function ThirdPartyComponent() {
642 + const data = await getData();
643 + return data;
644 + }
645 +
646 + async function Component() {
647 + const data = await fetchThirdParty(ThirdPartyComponent);
648 + return data.toUpperCase();
649 + }
650 +
651 + const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
652 +
653 + const readable = new Stream.PassThrough(streamOptions);
654 +
655 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
656 + moduleMap: {},
657 + moduleLoading: {},
658 + });
659 + stream.pipe(readable);
660 +
661 + expect(await result).toBe('HI');
662 + if (
663 + __DEV__ &&
664 + gate(
665 + flags =>
666 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
667 + )
668 + ) {
669 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
670 + [
671 + {
672 + "time": 0,
673 + },
674 + {
675 + "env": "Server",
676 + "key": null,
677 + "name": "Component",
678 + "owner": null,
679 + "props": {},
680 + "stack": [
681 + [
682 + "Object.<anonymous>",
683 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
684 + 651,
685 + 109,
686 + 634,
687 + 63,
688 + ],
689 + ],
690 + },
691 + {
692 + "time": 0,
693 + },
694 + {
695 + "env": "third-party",
696 + "key": null,
697 + "name": "ThirdPartyComponent",
698 + "owner": null,
699 + "props": {},
700 + "stack": [
701 + [
702 + "fetchThirdParty",
703 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
704 + 122,
705 + 40,
706 + 120,
707 + 3,
708 + ],
709 + [
710 + "Component",
711 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
712 + 647,
713 + 24,
714 + 646,
715 + 5,
716 + ],
717 + ],
718 + },
719 + {
720 + "time": 0,
721 + },
722 + {
723 + "awaited": {
724 + "end": 0,
725 + "env": "third-party",
726 + "name": "delay",
727 + "owner": {
728 + "env": "third-party",
729 + "key": null,
730 + "name": "ThirdPartyComponent",
731 + "owner": null,
732 + "props": {},
733 + "stack": [
734 + [
735 + "fetchThirdParty",
736 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
737 + 122,
738 + 40,
739 + 120,
740 + 3,
741 + ],
742 + [
743 + "Component",
744 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
745 + 647,
746 + 24,
747 + 646,
748 + 5,
749 + ],
750 + ],
751 + },
752 + "stack": [
753 + [
754 + "delay",
755 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
756 + 115,
757 + 12,
758 + 114,
759 + 3,
760 + ],
761 + [
762 + "getData",
763 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
764 + 636,
765 + 13,
766 + 635,
767 + 5,
768 + ],
769 + [
770 + "ThirdPartyComponent",
771 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
772 + 642,
773 + 24,
774 + 641,
775 + 5,
776 + ],
777 + ],
778 + "start": 0,
779 + },
780 + "env": "third-party",
781 + "owner": {
782 + "env": "third-party",
783 + "key": null,
784 + "name": "ThirdPartyComponent",
785 + "owner": null,
786 + "props": {},
787 + "stack": [
788 + [
789 + "fetchThirdParty",
790 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
791 + 122,
792 + 40,
793 + 120,
794 + 3,
795 + ],
796 + [
797 + "Component",
798 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
799 + 647,
800 + 24,
801 + 646,
802 + 5,
803 + ],
804 + ],
805 + },
806 + "stack": [
807 + [
808 + "getData",
809 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
810 + 636,
811 + 13,
812 + 635,
813 + 5,
814 + ],
815 + [
816 + "ThirdPartyComponent",
817 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
818 + 642,
819 + 24,
820 + 641,
821 + 5,
822 + ],
823 + ],
824 + },
825 + {
826 + "time": 0,
827 + },
828 + {
829 + "time": 0,
830 + },
831 + {
832 + "awaited": {
833 + "end": 0,
834 + "env": "third-party",
835 + "name": "delay",
836 + "owner": {
837 + "env": "third-party",
838 + "key": null,
839 + "name": "ThirdPartyComponent",
840 + "owner": null,
841 + "props": {},
842 + "stack": [
843 + [
844 + "fetchThirdParty",
845 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
846 + 122,
847 + 40,
848 + 120,
849 + 3,
850 + ],
851 + [
852 + "Component",
853 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
854 + 647,
855 + 24,
856 + 646,
857 + 5,
858 + ],
859 + ],
860 + },
861 + "stack": [
862 + [
863 + "delay",
864 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
865 + 115,
866 + 12,
867 + 114,
868 + 3,
869 + ],
870 + [
871 + "getData",
872 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
873 + 637,
874 + 13,
875 + 635,
876 + 5,
877 + ],
878 + [
879 + "ThirdPartyComponent",
880 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
881 + 642,
882 + 18,
883 + 641,
884 + 5,
885 + ],
886 + ],
887 + "start": 0,
888 + },
889 + "env": "third-party",
890 + "owner": {
891 + "env": "third-party",
892 + "key": null,
893 + "name": "ThirdPartyComponent",
894 + "owner": null,
895 + "props": {},
896 + "stack": [
897 + [
898 + "fetchThirdParty",
899 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
900 + 122,
901 + 40,
902 + 120,
903 + 3,
904 + ],
905 + [
906 + "Component",
907 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
908 + 647,
909 + 24,
910 + 646,
911 + 5,
912 + ],
913 + ],
914 + },
915 + "stack": [
916 + [
917 + "getData",
918 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
919 + 637,
920 + 13,
921 + 635,
922 + 5,
923 + ],
924 + [
925 + "ThirdPartyComponent",
926 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
927 + 642,
928 + 18,
929 + 641,
930 + 5,
931 + ],
932 + ],
933 + },
934 + {
935 + "time": 0,
936 + },
937 + {
938 + "time": 0,
939 + },
940 + {
941 + "time": 0,
942 + },
943 + ]
944 + `);
945 + }
946 + });
947 });
scripts/rollup/validate/eslintrc.cjs.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 + WeakRef: 'readonly',
18
19 Int8Array: 'readonly',
20 Uint8Array: 'readonly',
scripts/rollup/validate/eslintrc.cjs2015.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 + WeakRef: 'readonly',
18
19 Int8Array: 'readonly',
20 Uint8Array: 'readonly',
scripts/rollup/validate/eslintrc.esm.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 Symbol: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 + WeakRef: 'readonly',
18
19 Int8Array: 'readonly',
20 Uint8Array: 'readonly',
scripts/rollup/validate/eslintrc.fb.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 Proxy: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 + WeakRef: 'readonly',
18
19 Int8Array: 'readonly',
20 Uint8Array: 'readonly',
scripts/rollup/validate/eslintrc.rn.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 Proxy: 'readonly',
15 WeakMap: 'readonly',
16 WeakSet: 'readonly',
17 + WeakRef: 'readonly',
18
19 Int8Array: 'readonly',
20 Uint8Array: 'readonly',