@samitouri / QOS-React / commits / acee65d6d0

[Flight] Track Awaits on I/O as Debug Info (#33388)

This lets us track what data each Server Component depended on. This will be used by Performance Track and React DevTools. We use Node.js `async_hooks`. This has a number of downside. It is Node.js specific so this feature is not available in other runtimes until something equivalent becomes available. It's [discouraged by Node.js docs](https://nodejs.org/api/async_hooks.html#async-hooks). It's also slow which makes this approach only really viable in development mode. At least with stack traces. However, it's really the only solution that gives us the data that we need. The [Diagnostic Channel](https://nodejs.org/api/diagnostics_channel.html) API is not sufficient. Not only is many Node.js built-in APIs missing but all libraries like databases are also missing. Were as `async_hooks` covers pretty much anything async in the Node.js ecosystem. However, even if coverage was wider it's not actually showing the information we want. It's not enough to show the low level I/O that is happening because that doesn't provide the context. We need the stack trace in user space code where it was initiated and where it was awaited. It's also not each low level socket operation that we want to surface but some higher level concept which can span a sequence of I/O operations but as far as user space is concerned. Therefore this solution is anchored on stack traces and ignore listing to determine what the interesting span is. It is somewhat Promise-centric (and in particular async/await) because it allows us to model an abstract span instead of just random I/O. Async/await points are also especially useful because this allows Async Stacks to show the full sequence which is not supported by random callbacks. However, if no Promises are involved we still to our best to show the stack causing plain I/O callbacks. Additionally, we don't want to track all possible I/O. For example, side-effects like logging that doesn't affect the rendering performance doesn't need to be included. We only want to include things that actually block the rendering output. We also need to track which data blocks each component so that we can track which data caused a particular subtree to suspend. We can do this using `async_hooks` because we can track the graph of what resolved what and then spawned what. To track what suspended what, something has to resolve. Therefore it needs to run to completion before we can show what it was suspended on. So something that never resolves, won't be tracked for example. We use the `async_hooks` in `ReactFlightServerConfigDebugNode` to build up an `ReactFlightAsyncSequence` graph that collects the stack traces for basically all I/O and Promises allocated in the whole app. This is pretty heavy, especially the stack traces, but it's because we don't know which ones we'll need until they resolve. We don't materialize the stacks until we need them though. Once they end up pinging the Flight runtime, we collect which current executing task that pinged the runtime and then log the sequence that led up until that runtime into the RSC protocol. Currently we only include things that weren't already resolved before we started rendering this task/component, so that we don't log the entire history each time. Each operation is split into two parts. First a `ReactIOInfo` which represents an I/O operation and its start/end time. Basically the start point where it was start. This is basically represents where you called `new Promise()` or when entering an `async function` which has an implied Promise. It can be started in a different component than where it's awaited and it can be awaited in multiple places. Therefore this is global information and not associated with a specific Component. The second part is `ReactAsyncInfo`. This represents where this I/O was `await`:ed or `.then()` called. This is associated with a point in the tree (usually the Promise that's a direct child of a Component). Since you can have multiple different I/O awaited in a sequence technically it forms a dependency graph but to simplify the model these awaits as flattened into the `ReactDebugInfo` list. Basically it contains each await in a sequence that affected this part from unblocking. This means that the same `ReactAsyncInfo` can appear in mutliple components if they all await the same `ReactIOInfo` but the same Promise only appears once. Promises that are only resolved by other Promises or immediately are not considered here. Only if they're resolved by an I/O operation. We pick the Promise basically on the border between user space code and ignored listed code (`node_modules`) to pick the most specific span but abstract enough to not give too much detail irrelevant to the current audience. Similarly, the deepest `await` in user space is marked as the relevant `await` point. This feature is only available in the `node` builds of React. Not if you use the `edge` builds inside of Node.js. --------- Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>

Sebastian Markbåge committed Jun 3, 2025 at 14:14 UTC acee65d6d031697ab8c71932a5b028351cbc3b03
15 files changed +840 -76
packages/react-server/src/ReactFlightAsyncSequence.js new
+41
@@ -0,0 +1,41 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export const IO_NODE = 0;
11 +export const PROMISE_NODE = 1;
12 +export const AWAIT_NODE = 2;
13 +
14 +export type IONode = {
15 + tag: 0,
16 + stack: Error, // callsite that spawned the I/O
17 + start: number, // start time when the first part of the I/O sequence started
18 + end: number, // we typically don't use this. only when there's no promise intermediate.
19 + awaited: null, // I/O is only blocked on external.
20 + previous: null | AwaitNode, // the preceeding await that spawned this new work
21 +};
22 +
23 +export type PromiseNode = {
24 + tag: 1,
25 + stack: Error, // callsite that created the Promise
26 + start: number, // start time when the Promise was created
27 + end: number, // end time when the Promise was resolved.
28 + awaited: null | AsyncSequence, // the thing that ended up resolving this promise
29 + previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
30 +};
31 +
32 +export type AwaitNode = {
33 + tag: 2,
34 + stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
35 + start: -1.1, // not used. We use the timing of the awaited promise.
36 + end: -1.1, // not used.
37 + awaited: null | AsyncSequence, // the promise we were waiting on
38 + previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
39 +};
40 +
41 +export type AsyncSequence = IONode | PromiseNode | AwaitNode;
packages/react-server/src/ReactFlightServer.js
+255 -7
@@ -19,6 +19,7 @@ import {
19 enableTaint,
20 enableProfilerTimer,
21 enableComponentPerformanceTrack,
22 + enableAsyncDebugInfo,
23 } from 'shared/ReactFeatureFlags';
24
25 import {
@@ -59,6 +60,7 @@ import type {
60 ReactDebugInfo,
61 ReactComponentInfo,
62 ReactEnvironmentInfo,
63 + ReactIOInfo,
64 ReactAsyncInfo,
65 ReactTimeInfo,
66 ReactStackTrace,
@@ -68,6 +70,11 @@ import type {
70 } from 'shared/ReactTypes';
71 import type {ReactElement} from 'shared/ReactElementType';
72 import type {LazyComponent} from 'react/src/ReactLazy';
73 +import type {
74 + AsyncSequence,
75 + IONode,
76 + PromiseNode,
77 +} from './ReactFlightAsyncSequence';
78
79 import {
80 resolveClientReferenceMetadata,
@@ -81,6 +88,7 @@ import {
88 requestStorage,
89 createHints,
90 initAsyncDebugInfo,
91 + getCurrentAsyncSequence,
92 parseStackTrace,
93 supportsComponentStorage,
94 componentStorage,
@@ -140,6 +148,8 @@ import binaryToComparableString from 'shared/binaryToComparableString';
148
149 import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
150
151 +import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
152 +
153 // DEV-only set containing internal objects that should not be limited and turned into getters.
154 const doNotLimit: WeakSet<Reference> = __DEV__ ? new WeakSet() : (null: any);
155
@@ -356,6 +366,7 @@ type Task = {
366 implicitSlot: boolean, // true if the root server component of this sequence had a null key
367 thenableState: ThenableState | null,
368 timed: boolean, // Profiling-only. Whether we need to track the completion time of this task.
369 + time: number, // Profiling-only. The last time stamp emitted for this task.
370 environmentName: string, // DEV-only. Used to track if the environment for this task changed.
371 debugOwner: null | ReactComponentInfo, // DEV-only
372 debugStack: null | Error, // DEV-only
@@ -529,6 +540,7 @@ function RequestInstance(
540 this.didWarnForKey = null;
541 }
542
543 + let timeOrigin: number;
544 if (enableProfilerTimer && enableComponentPerformanceTrack) {
545 // We start by serializing the time origin. Any future timestamps will be
546 // emitted relatively to this origin. Instead of using performance.timeOrigin
@@ -536,13 +548,15 @@ function RequestInstance(
548 // This avoids leaking unnecessary information like how long the server has
549 // been running and allows for more compact representation of each timestamp.
550 // The time origin is stored as an offset in the time space of this environment.
539 - const timeOrigin = (this.timeOrigin = performance.now());
551 + timeOrigin = this.timeOrigin = performance.now();
552 emitTimeOriginChunk(
553 this,
554 timeOrigin +
555 // $FlowFixMe[prop-missing]
556 performance.timeOrigin,
557 );
558 + } else {
559 + timeOrigin = 0;
560 }
561
562 const rootTask = createTask(
@@ -551,6 +565,7 @@ function RequestInstance(
565 null,
566 false,
567 abortSet,
568 + timeOrigin,
569 null,
570 null,
571 null,
@@ -642,6 +657,7 @@ function serializeThenable(
657 task.keyPath, // the server component sequence continues through Promise-as-a-child.
658 task.implicitSlot,
659 request.abortableTasks,
660 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
661 __DEV__ ? task.debugOwner : null,
662 __DEV__ ? task.debugStack : null,
663 __DEV__ ? task.debugTask : null,
@@ -762,6 +778,7 @@ function serializeReadableStream(
778 task.keyPath,
779 task.implicitSlot,
780 request.abortableTasks,
781 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
782 __DEV__ ? task.debugOwner : null,
783 __DEV__ ? task.debugStack : null,
784 __DEV__ ? task.debugTask : null,
@@ -853,6 +870,7 @@ function serializeAsyncIterable(
870 task.keyPath,
871 task.implicitSlot,
872 request.abortableTasks,
873 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
874 __DEV__ ? task.debugOwner : null,
875 __DEV__ ? task.debugStack : null,
876 __DEV__ ? task.debugTask : null,
@@ -1278,7 +1296,11 @@ function renderFunctionComponent<Props>(
1296 // Track when we started rendering this component.
1297 if (enableProfilerTimer && enableComponentPerformanceTrack) {
1298 task.timed = true;
1281 - emitTimingChunk(request, componentDebugID, performance.now());
1299 + emitTimingChunk(
1300 + request,
1301 + componentDebugID,
1302 + (task.time = performance.now()),
1303 + );
1304 }
1305
1306 emitDebugChunk(request, componentDebugID, componentDebugInfo);
@@ -1629,6 +1651,7 @@ function deferTask(request: Request, task: Task): ReactJSONValue {
1651 task.keyPath, // unlike outlineModel this one carries along context
1652 task.implicitSlot,
1653 request.abortableTasks,
1654 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
1655 __DEV__ ? task.debugOwner : null,
1656 __DEV__ ? task.debugStack : null,
1657 __DEV__ ? task.debugTask : null,
@@ -1645,6 +1668,7 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
1668 task.keyPath, // unlike outlineModel this one carries along context
1669 task.implicitSlot,
1670 request.abortableTasks,
1671 + enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
1672 __DEV__ ? task.debugOwner : null,
1673 __DEV__ ? task.debugStack : null,
1674 __DEV__ ? task.debugTask : null,
@@ -1814,10 +1838,128 @@ function renderElement(
1838 return renderClientElement(request, task, type, key, props, validated);
1839 }
1840
1841 +function visitAsyncNode(
1842 + request: Request,
1843 + task: Task,
1844 + node: AsyncSequence,
1845 + cutOff: number,
1846 + visited: Set<AsyncSequence>,
1847 +): null | PromiseNode | IONode {
1848 + if (visited.has(node)) {
1849 + // It's possible to visit them same node twice when it's part of both an "awaited" path
1850 + // and a "previous" path. This also gracefully handles cycles which would be a bug.
1851 + return null;
1852 + }
1853 + visited.add(node);
1854 + // First visit anything that blocked this sequence to start in the first place.
1855 + if (node.previous !== null) {
1856 + // We ignore the return value here because if it wasn't awaited in user space, then we don't log it.
1857 + // TODO: This means that some I/O can get lost that was still blocking the sequence.
1858 + visitAsyncNode(request, task, node.previous, cutOff, visited);
1859 + }
1860 + switch (node.tag) {
1861 + case IO_NODE: {
1862 + return node;
1863 + }
1864 + case PROMISE_NODE: {
1865 + if (node.end < cutOff) {
1866 + // This was already resolved when we started this sequence. It must have been
1867 + // part of a different component.
1868 + // TODO: Think of some other way to exclude irrelevant data since if we awaited
1869 + // a cached promise, we should still log this component as being dependent on that data.
1870 + return null;
1871 + }
1872 + const awaited = node.awaited;
1873 + if (awaited !== null) {
1874 + const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1875 + if (ioNode !== null) {
1876 + // This Promise was blocked on I/O. That's a signal that this Promise is interesting to log.
1877 + // We don't log it yet though. We return it to be logged by the point where it's awaited.
1878 + // The ioNode might be another PromiseNode in the case where none of the AwaitNode had
1879 + // unfiltered stacks.
1880 + if (filterStackTrace(request, node.stack, 1).length === 0) {
1881 + // Typically we assume that the outer most Promise that was awaited in user space has the
1882 + // most actionable stack trace for the start of the operation. However, if this Promise
1883 + // was created inside only third party code, then try to use the inner node instead.
1884 + // This could happen if you pass a first party Promise into a third party to be awaited there.
1885 + if (ioNode.end < 0) {
1886 + // If we haven't defined an end time, use the resolve of the outer Promise.
1887 + ioNode.end = node.end;
1888 + }
1889 + return ioNode;
1890 + }
1891 + return node;
1892 + }
1893 + }
1894 + return null;
1895 + }
1896 + case AWAIT_NODE: {
1897 + const awaited = node.awaited;
1898 + if (awaited !== null) {
1899 + const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1900 + if (ioNode !== null) {
1901 + const stack = filterStackTrace(request, node.stack, 1);
1902 + if (stack.length === 0) {
1903 + // If this await was fully filtered out, then it was inside third party code
1904 + // such as in an external library. We return the I/O node and try another await.
1905 + return ioNode;
1906 + }
1907 + // Outline the IO node.
1908 + emitIOChunk(request, ioNode);
1909 + // Then emit a reference to us awaiting it in the current task.
1910 + request.pendingChunks++;
1911 + emitDebugChunk(request, task.id, {
1912 + awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
1913 + stack: stack,
1914 + });
1915 + }
1916 + }
1917 + // If we had awaited anything we would have written it now.
1918 + return null;
1919 + }
1920 + default: {
1921 + // eslint-disable-next-line react-internal/prod-error-codes
1922 + throw new Error('Unknown AsyncSequence tag. This is a bug in React.');
1923 + }
1924 + }
1925 +}
1926 +
1927 +function emitAsyncSequence(
1928 + request: Request,
1929 + task: Task,
1930 + node: AsyncSequence,
1931 + cutOff: number,
1932 +): void {
1933 + const visited: Set<AsyncSequence> = new Set();
1934 + const awaitedNode = visitAsyncNode(request, task, node, cutOff, visited);
1935 + if (awaitedNode !== null) {
1936 + // Nothing in user space (unfiltered stack) awaited this.
1937 + if (awaitedNode.end < 0) {
1938 + // If this was I/O directly without a Promise, then it means that some custom Thenable
1939 + // called our ping directly and not from a native .then(). We use the current ping time
1940 + // as the end time and treat it as an await with no stack.
1941 + // TODO: If this I/O is recurring then we really should have different entries for
1942 + // each occurrence. Right now we'll only track the first time it is invoked.
1943 + awaitedNode.end = performance.now();
1944 + }
1945 + emitIOChunk(request, awaitedNode);
1946 + request.pendingChunks++;
1947 + emitDebugChunk(request, task.id, {
1948 + awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
1949 + });
1950 + }
1951 +}
1952 +
1953 function pingTask(request: Request, task: Task): void {
1954 if (enableProfilerTimer && enableComponentPerformanceTrack) {
1955 // If this was async we need to emit the time when it completes.
1956 task.timed = true;
1957 + if (enableAsyncDebugInfo) {
1958 + const sequence = getCurrentAsyncSequence();
1959 + if (sequence !== null) {
1960 + emitAsyncSequence(request, task, sequence, task.time);
1961 + }
1962 + }
1963 }
1964 const pingedTasks = request.pingedTasks;
1965 pingedTasks.push(task);
@@ -1837,6 +1979,7 @@ function createTask(
1979 keyPath: null | string,
1980 implicitSlot: boolean,
1981 abortSet: Set<Task>,
1982 + lastTimestamp: number, // Profiling-only
1983 debugOwner: null | ReactComponentInfo, // DEV-only
1984 debugStack: null | Error, // DEV-only
1985 debugTask: null | ConsoleTask, // DEV-only
@@ -1912,10 +2055,16 @@ function createTask(
2055 thenableState: null,
2056 }: Omit<
2057 Task,
1915 - 'timed' | 'environmentName' | 'debugOwner' | 'debugStack' | 'debugTask',
2058 + | 'timed'
2059 + | 'time'
2060 + | 'environmentName'
2061 + | 'debugOwner'
2062 + | 'debugStack'
2063 + | 'debugTask',
2064 >): any);
2065 if (enableProfilerTimer && enableComponentPerformanceTrack) {
2066 task.timed = false;
2067 + task.time = lastTimestamp;
2068 }
2069 if (__DEV__) {
2070 task.environmentName = request.environmentName();
@@ -2062,6 +2211,9 @@ function outlineModel(request: Request, value: ReactClientValue): number {
2211 null, // The way we use outlining is for reusing an object.
2212 false, // It makes no sense for that use case to be contextual.
2213 request.abortableTasks,
2214 + enableProfilerTimer && enableComponentPerformanceTrack
2215 + ? performance.now() // TODO: This should really inherit the time from the task.
2216 + : 0,
2217 null, // TODO: Currently we don't associate any debug information with
2218 null, // this object on the server. If it ends up erroring, it won't
2219 null, // have any context on the server but can on the client.
@@ -2242,6 +2394,9 @@ function serializeBlob(request: Request, blob: Blob): string {
2394 null,
2395 false,
2396 request.abortableTasks,
2397 + enableProfilerTimer && enableComponentPerformanceTrack
2398 + ? performance.now() // TODO: This should really inherit the time from the task.
2399 + : 0,
2400 null, // TODO: Currently we don't associate any debug information with
2401 null, // this object on the server. If it ends up erroring, it won't
2402 null, // have any context on the server but can on the client.
@@ -2374,6 +2529,9 @@ function renderModel(
2529 task.keyPath,
2530 task.implicitSlot,
2531 request.abortableTasks,
2532 + enableProfilerTimer && enableComponentPerformanceTrack
2533 + ? task.time
2534 + : 0,
2535 __DEV__ ? task.debugOwner : null,
2536 __DEV__ ? task.debugStack : null,
2537 __DEV__ ? task.debugTask : null,
@@ -3335,6 +3493,82 @@ function outlineComponentInfo(
3493 request.writtenObjects.set(componentInfo, serializeByValueID(id));
3494 }
3495
3496 +function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
3497 + if (!__DEV__) {
3498 + // These errors should never make it into a build so we don't need to encode them in codes.json
3499 + // eslint-disable-next-line react-internal/prod-error-codes
3500 + throw new Error(
3501 + 'outlineIOInfo should never be called in production mode. This is a bug in React.',
3502 + );
3503 + }
3504 +
3505 + if (request.writtenObjects.has(ioInfo)) {
3506 + // Already written
3507 + return;
3508 + }
3509 +
3510 + // Limit the number of objects we write to prevent emitting giant props objects.
3511 + let objectLimit = 10;
3512 + if (ioInfo.stack != null) {
3513 + // Ensure we have enough object limit to encode the stack trace.
3514 + objectLimit += ioInfo.stack.length;
3515 + }
3516 +
3517 + // We use the console encoding so that we can dedupe objects but don't necessarily
3518 + // use the full serialization that requires a task.
3519 + const counter = {objectLimit};
3520 +
3521 + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3522 + const relativeStartTimestamp = ioInfo.start - request.timeOrigin;
3523 + const relativeEndTimestamp = ioInfo.end - request.timeOrigin;
3524 + const debugIOInfo: Omit<ReactIOInfo, 'debugTask' | 'debugStack'> = {
3525 + start: relativeStartTimestamp,
3526 + end: relativeEndTimestamp,
3527 + stack: ioInfo.stack,
3528 + };
3529 + const id = outlineConsoleValue(request, counter, debugIOInfo);
3530 + request.writtenObjects.set(ioInfo, serializeByValueID(id));
3531 +}
3532 +
3533 +function emitIOChunk(request: Request, ioNode: IONode | PromiseNode): void {
3534 + if (!__DEV__) {
3535 + // These errors should never make it into a build so we don't need to encode them in codes.json
3536 + // eslint-disable-next-line react-internal/prod-error-codes
3537 + throw new Error(
3538 + 'outlineIOInfo should never be called in production mode. This is a bug in React.',
3539 + );
3540 + }
3541 +
3542 + if (request.writtenObjects.has(ioNode)) {
3543 + // Already written
3544 + return;
3545 + }
3546 +
3547 + // Limit the number of objects we write to prevent emitting giant props objects.
3548 + let objectLimit = 10;
3549 + let stack = null;
3550 + if (ioNode.stack !== null) {
3551 + stack = filterStackTrace(request, ioNode.stack, 1);
3552 + // Ensure we have enough object limit to encode the stack trace.
3553 + objectLimit += stack.length;
3554 + }
3555 +
3556 + // We use the console encoding so that we can dedupe objects but don't necessarily
3557 + // use the full serialization that requires a task.
3558 + const counter = {objectLimit};
3559 +
3560 + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3561 + const relativeStartTimestamp = ioNode.start - request.timeOrigin;
3562 + const relativeEndTimestamp = ioNode.end - request.timeOrigin;
3563 + const debugIOInfo: Omit<ReactIOInfo, 'debugTask' | 'debugStack'> = {
3564 + start: relativeStartTimestamp,
3565 + end: relativeEndTimestamp,
3566 + stack: stack,
3567 + };
3568 + const id = outlineConsoleValue(request, counter, debugIOInfo);
3569 + request.writtenObjects.set(ioNode, serializeByValueID(id));
3570 +}
3571 +
3572 function emitTypedArrayChunk(
3573 request: Request,
3574 id: number,
@@ -3842,8 +4076,22 @@ function forwardDebugInfo(
4076 // If we had a smarter way to dedupe we might not have to do this if there ends up
4077 // being no references to this as an owner.
4078 outlineComponentInfo(request, (debugInfo[i]: any));
4079 + // Emit a reference to the outlined one.
4080 + emitDebugChunk(request, id, debugInfo[i]);
4081 + } else if (debugInfo[i].awaited) {
4082 + const ioInfo = debugInfo[i].awaited;
4083 + // Outline the IO info in case the same I/O is awaited in more than one place.
4084 + outlineIOInfo(request, ioInfo);
4085 + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
4086 + const debugAsyncInfo: Omit<ReactAsyncInfo, 'debugTask' | 'debugStack'> =
4087 + {
4088 + awaited: ioInfo,
4089 + stack: debugInfo[i].stack,
4090 + };
4091 + emitDebugChunk(request, id, debugAsyncInfo);
4092 + } else {
4093 + emitDebugChunk(request, id, debugInfo[i]);
4094 }
3846 - emitDebugChunk(request, id, debugInfo[i]);
4095 }
4096 }
4097 }
@@ -3956,7 +4204,7 @@ function emitChunk(
4204 function erroredTask(request: Request, task: Task, error: mixed): void {
4205 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4206 if (task.timed) {
3959 - emitTimingChunk(request, task.id, performance.now());
4207 + emitTimingChunk(request, task.id, (task.time = performance.now()));
4208 }
4209 }
4210 task.status = ERRORED;
@@ -4039,7 +4287,7 @@ function retryTask(request: Request, task: Task): void {
4287 // We've finished rendering. Log the end time.
4288 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4289 if (task.timed) {
4042 - emitTimingChunk(request, task.id, performance.now());
4290 + emitTimingChunk(request, task.id, (task.time = performance.now()));
4291 }
4292 }
4293
@@ -4164,7 +4412,7 @@ function abortTask(task: Task, request: Request, errorId: number): void {
4412 // Track when we aborted this task as its end time.
4413 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4414 if (task.timed) {
4167 - emitTimingChunk(request, task.id, performance.now());
4415 + emitTimingChunk(request, task.id, (task.time = performance.now()));
4416 }
4417 }
4418 // Instead of emitting an error per task.id, we emit a model that only
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+123 -6
@@ -7,9 +7,20 @@
7 * @flow
8 */
9
10 -import {createAsyncHook, executionAsyncId} from './ReactFlightServerConfig';
10 +import type {
11 + AsyncSequence,
12 + IONode,
13 + PromiseNode,
14 + AwaitNode,
15 +} from './ReactFlightAsyncSequence';
16 +
17 +import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
18 +import {createHook, executionAsyncId} from 'async_hooks';
19 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
20
21 +const pendingOperations: Map<number, AsyncSequence> =
22 + __DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
23 +
24 // Initialize the tracing of async operations.
25 // We do this globally since the async work can potentially eagerly
26 // start before the first request and once requests start they can interleave.
@@ -17,17 +28,123 @@ import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
28 // but given that typically this is just a live server, it doesn't really matter.
29 export function initAsyncDebugInfo(): void {
30 if (__DEV__ && enableAsyncDebugInfo) {
20 - createAsyncHook({
31 + createHook({
32 init(asyncId: number, type: string, triggerAsyncId: number): void {
22 - // TODO
33 + const trigger = pendingOperations.get(triggerAsyncId);
34 + let node: AsyncSequence;
35 + if (type === 'PROMISE') {
36 + const currentAsyncId = executionAsyncId();
37 + if (currentAsyncId !== triggerAsyncId) {
38 + // When you call .then() on a native Promise, or await/Promise.all() a thenable,
39 + // then this intermediate Promise is created. We use this as our await point
40 + if (trigger === undefined) {
41 + // We don't track awaits on things that started outside our tracked scope.
42 + return;
43 + }
44 + const current = pendingOperations.get(currentAsyncId);
45 + // If the thing we're waiting on is another Await we still track that sequence
46 + // so that we can later pick the best stack trace in user space.
47 + node = ({
48 + tag: AWAIT_NODE,
49 + stack: new Error(),
50 + start: -1.1,
51 + end: -1.1,
52 + awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
53 + previous: current === undefined ? null : current, // The path that led us here.
54 + }: AwaitNode);
55 + } else {
56 + node = ({
57 + tag: PROMISE_NODE,
58 + stack: new Error(),
59 + start: performance.now(),
60 + end: -1.1, // Set when we resolve.
61 + awaited:
62 + trigger === undefined
63 + ? null // It might get overridden when we resolve.
64 + : trigger,
65 + previous: null,
66 + }: PromiseNode);
67 + }
68 + } else if (
69 + type !== 'Microtask' &&
70 + type !== 'TickObject' &&
71 + type !== 'Immediate'
72 + ) {
73 + if (trigger === undefined) {
74 + // We have begun a new I/O sequence.
75 + node = ({
76 + tag: IO_NODE,
77 + stack: new Error(), // This is only used if no native promises are used.
78 + start: performance.now(),
79 + end: -1.1, // Only set when pinged.
80 + awaited: null,
81 + previous: null,
82 + }: IONode);
83 + } else if (trigger.tag === AWAIT_NODE) {
84 + // We have begun a new I/O sequence after the await.
85 + node = ({
86 + tag: IO_NODE,
87 + stack: new Error(),
88 + start: performance.now(),
89 + end: -1.1, // Only set when pinged.
90 + awaited: null,
91 + previous: trigger,
92 + }: IONode);
93 + } else {
94 + // Otherwise, this is just a continuation of the same I/O sequence.
95 + node = trigger;
96 + }
97 + } else {
98 + // Ignore nextTick and microtasks as they're not considered I/O operations.
99 + // we just treat the trigger as the node to carry along the sequence.
100 + if (trigger === undefined) {
101 + return;
102 + }
103 + node = trigger;
104 + }
105 + pendingOperations.set(asyncId, node);
106 },
107 promiseResolve(asyncId: number): void {
25 - // TODO
26 - executionAsyncId();
108 + const resolvedNode = pendingOperations.get(asyncId);
109 + if (resolvedNode !== undefined) {
110 + if (resolvedNode.tag === IO_NODE) {
111 + // eslint-disable-next-line react-internal/prod-error-codes
112 + throw new Error(
113 + 'A Promise should never be an IO_NODE. This is a bug in React.',
114 + );
115 + }
116 + if (resolvedNode.tag === PROMISE_NODE) {
117 + // Log the end time when we resolved the promise.
118 + resolvedNode.end = performance.now();
119 + }
120 + const currentAsyncId = executionAsyncId();
121 + if (asyncId !== currentAsyncId) {
122 + // If the promise was not resolved by itself, then that means that
123 + // the trigger that we originally stored wasn't actually the dependency.
124 + // Instead, the current execution context is what ultimately unblocked it.
125 + const awaited = pendingOperations.get(currentAsyncId);
126 + resolvedNode.awaited = awaited === undefined ? null : awaited;
127 + }
128 + }
129 },
130 +
131 destroy(asyncId: number): void {
29 - // TODO
132 + // If we needed the meta data from this operation we should have already
133 + // extracted it or it should be part of a chain of triggers.
134 + pendingOperations.delete(asyncId);
135 },
136 }).enable();
137 }
138 }
139 +
140 +export function getCurrentAsyncSequence(): null | AsyncSequence {
141 + if (!__DEV__ || !enableAsyncDebugInfo) {
142 + return null;
143 + }
144 + const currentNode = pendingOperations.get(executionAsyncId());
145 + if (currentNode === undefined) {
146 + // Nothing that we tracked led to the resolution of this execution context.
147 + return null;
148 + }
149 + return currentNode;
150 +}
packages/react-server/src/ReactFlightServerConfigDebugNoop.js
+5
@@ -7,5 +7,10 @@
7 * @flow
8 */
9
10 +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 getCurrentAsyncSequence(): null | AsyncSequence {
15 + return null;
16 +}
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js new
+354
@@ -0,0 +1,354 @@
1 +'use strict';
2 +
3 +const path = require('path');
4 +
5 +import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
6 +
7 +let React;
8 +let ReactServerDOMServer;
9 +let ReactServerDOMClient;
10 +let Stream;
11 +
12 +const streamOptions = {
13 + objectMode: true,
14 +};
15 +
16 +const repoRoot = path.resolve(__dirname, '../../../../');
17 +
18 +function normalizeStack(stack) {
19 + if (!stack) {
20 + return stack;
21 + }
22 + const copy = [];
23 + for (let i = 0; i < stack.length; i++) {
24 + const [name, file, line, col, enclosingLine, enclosingCol] = stack[i];
25 + copy.push([
26 + name,
27 + file.replace(repoRoot, ''),
28 + line,
29 + col,
30 + enclosingLine,
31 + enclosingCol,
32 + ]);
33 + }
34 + return copy;
35 +}
36 +
37 +function normalizeIOInfo(ioInfo) {
38 + const {debugTask, debugStack, ...copy} = ioInfo;
39 + if (ioInfo.stack) {
40 + copy.stack = normalizeStack(ioInfo.stack);
41 + }
42 + if (typeof ioInfo.start === 'number') {
43 + copy.start = 0;
44 + }
45 + if (typeof ioInfo.end === 'number') {
46 + copy.end = 0;
47 + }
48 + return copy;
49 +}
50 +
51 +function normalizeDebugInfo(debugInfo) {
52 + if (Array.isArray(debugInfo.stack)) {
53 + const {debugTask, debugStack, ...copy} = debugInfo;
54 + copy.stack = normalizeStack(debugInfo.stack);
55 + if (debugInfo.owner) {
56 + copy.owner = normalizeDebugInfo(debugInfo.owner);
57 + }
58 + if (debugInfo.awaited) {
59 + copy.awaited = normalizeIOInfo(copy.awaited);
60 + }
61 + return copy;
62 + } else if (typeof debugInfo.time === 'number') {
63 + return {...debugInfo, time: 0};
64 + } else if (debugInfo.awaited) {
65 + return {...debugInfo, awaited: normalizeIOInfo(debugInfo.awaited)};
66 + } else {
67 + return debugInfo;
68 + }
69 +}
70 +
71 +function getDebugInfo(obj) {
72 + const debugInfo = obj._debugInfo;
73 + if (debugInfo) {
74 + const copy = [];
75 + for (let i = 0; i < debugInfo.length; i++) {
76 + copy.push(normalizeDebugInfo(debugInfo[i]));
77 + }
78 + return copy;
79 + }
80 + return debugInfo;
81 +}
82 +
83 +describe('ReactFlightAsyncDebugInfo', () => {
84 + beforeEach(() => {
85 + jest.resetModules();
86 + jest.useRealTimers();
87 + patchSetImmediate();
88 + global.console = require('console');
89 +
90 + jest.mock('react', () => require('react/react.react-server'));
91 + jest.mock('react-server-dom-webpack/server', () =>
92 + require('react-server-dom-webpack/server.node'),
93 + );
94 + ReactServerDOMServer = require('react-server-dom-webpack/server');
95 +
96 + jest.resetModules();
97 + jest.useRealTimers();
98 + patchSetImmediate();
99 +
100 + __unmockReact();
101 + jest.unmock('react-server-dom-webpack/server');
102 + jest.mock('react-server-dom-webpack/client', () =>
103 + require('react-server-dom-webpack/client.node'),
104 + );
105 +
106 + React = require('react');
107 + ReactServerDOMClient = require('react-server-dom-webpack/client');
108 + Stream = require('stream');
109 + });
110 +
111 + function delay(timeout) {
112 + return new Promise(resolve => {
113 + setTimeout(resolve, timeout);
114 + });
115 + }
116 +
117 + it('can track async information when awaited', async () => {
118 + async function getData() {
119 + await delay(1);
120 + const promise = delay(2);
121 + await Promise.all([promise]);
122 + return 'hi';
123 + }
124 +
125 + async function Component() {
126 + const result = await getData();
127 + return result;
128 + }
129 +
130 + const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
131 +
132 + const readable = new Stream.PassThrough(streamOptions);
133 +
134 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
135 + moduleMap: {},
136 + moduleLoading: {},
137 + });
138 + stream.pipe(readable);
139 +
140 + expect(await result).toBe('hi');
141 + if (
142 + __DEV__ &&
143 + gate(
144 + flags =>
145 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
146 + )
147 + ) {
148 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
149 + [
150 + {
151 + "time": 0,
152 + },
153 + {
154 + "env": "Server",
155 + "key": null,
156 + "name": "Component",
157 + "owner": null,
158 + "props": {},
159 + "stack": [
160 + [
161 + "Object.<anonymous>",
162 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
163 + 130,
164 + 109,
165 + 117,
166 + 50,
167 + ],
168 + ],
169 + },
170 + {
171 + "awaited": {
172 + "end": 0,
173 + "stack": [
174 + [
175 + "delay",
176 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
177 + 112,
178 + 12,
179 + 111,
180 + 3,
181 + ],
182 + [
183 + "getData",
184 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
185 + 119,
186 + 13,
187 + 118,
188 + 5,
189 + ],
190 + [
191 + "Component",
192 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
193 + 126,
194 + 26,
195 + 125,
196 + 5,
197 + ],
198 + ],
199 + "start": 0,
200 + },
201 + "stack": [
202 + [
203 + "getData",
204 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
205 + 119,
206 + 13,
207 + 118,
208 + 5,
209 + ],
210 + [
211 + "Component",
212 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
213 + 126,
214 + 26,
215 + 125,
216 + 5,
217 + ],
218 + ],
219 + },
220 + {
221 + "awaited": {
222 + "end": 0,
223 + "stack": [
224 + [
225 + "delay",
226 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
227 + 112,
228 + 12,
229 + 111,
230 + 3,
231 + ],
232 + [
233 + "getData",
234 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
235 + 120,
236 + 21,
237 + 118,
238 + 5,
239 + ],
240 + [
241 + "Component",
242 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
243 + 126,
244 + 20,
245 + 125,
246 + 5,
247 + ],
248 + ],
249 + "start": 0,
250 + },
251 + "stack": [
252 + [
253 + "getData",
254 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
255 + 121,
256 + 21,
257 + 118,
258 + 5,
259 + ],
260 + [
261 + "Component",
262 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
263 + 126,
264 + 20,
265 + 125,
266 + 5,
267 + ],
268 + ],
269 + },
270 + {
271 + "time": 0,
272 + },
273 + ]
274 + `);
275 + }
276 + });
277 +
278 + it('can track the start of I/O when no native promise is used', async () => {
279 + function Component() {
280 + const callbacks = [];
281 + setTimeout(function timer() {
282 + callbacks.forEach(callback => callback('hi'));
283 + }, 5);
284 + return {
285 + then(callback) {
286 + callbacks.push(callback);
287 + },
288 + };
289 + }
290 +
291 + const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
292 +
293 + const readable = new Stream.PassThrough(streamOptions);
294 +
295 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
296 + moduleMap: {},
297 + moduleLoading: {},
298 + });
299 + stream.pipe(readable);
300 +
301 + expect(await result).toBe('hi');
302 + if (
303 + __DEV__ &&
304 + gate(
305 + flags =>
306 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
307 + )
308 + ) {
309 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
310 + [
311 + {
312 + "time": 0,
313 + },
314 + {
315 + "env": "Server",
316 + "key": null,
317 + "name": "Component",
318 + "owner": null,
319 + "props": {},
320 + "stack": [
321 + [
322 + "Object.<anonymous>",
323 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
324 + 291,
325 + 109,
326 + 278,
327 + 67,
328 + ],
329 + ],
330 + },
331 + {
332 + "awaited": {
333 + "end": 0,
334 + "stack": [
335 + [
336 + "Component",
337 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
338 + 281,
339 + 7,
340 + 279,
341 + 5,
342 + ],
343 + ],
344 + "start": 0,
345 + },
346 + },
347 + {
348 + "time": 0,
349 + },
350 + ]
351 + `);
352 + }
353 + });
354 +});
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-parcel.js
+1 -15
@@ -22,20 +22,6 @@ export const supportsComponentStorage: boolean =
22 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
23 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
24
25 -// We use the Node version but get access to async_hooks from a global.
26 -import type {HookCallbacks, AsyncHook} from 'async_hooks';
27 -export const createAsyncHook: HookCallbacks => AsyncHook =
28 - typeof async_hooks === 'object'
29 - ? async_hooks.createHook
30 - : function () {
31 - return ({
32 - enable() {},
33 - disable() {},
34 - }: any);
35 - };
36 -export const executionAsyncId: () => number =
37 - typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
38 -
39 -export * from '../ReactFlightServerConfigDebugNode';
25 +export * from '../ReactFlightServerConfigDebugNoop';
26
27 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js
+1 -15
@@ -22,20 +22,6 @@ export const supportsComponentStorage: boolean =
22 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
23 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
24
25 -// We use the Node version but get access to async_hooks from a global.
26 -import type {HookCallbacks, AsyncHook} from 'async_hooks';
27 -export const createAsyncHook: HookCallbacks => AsyncHook =
28 - typeof async_hooks === 'object'
29 - ? async_hooks.createHook
30 - : function () {
31 - return ({
32 - enable() {},
33 - disable() {},
34 - }: any);
35 - };
36 -export const executionAsyncId: () => number =
37 - typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
38 -
39 -export * from '../ReactFlightServerConfigDebugNode';
25 +export * from '../ReactFlightServerConfigDebugNoop';
26
27 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js
+1 -15
@@ -23,20 +23,6 @@ export const supportsComponentStorage: boolean =
23 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26 -// We use the Node version but get access to async_hooks from a global.
27 -import type {HookCallbacks, AsyncHook} from 'async_hooks';
28 -export const createAsyncHook: HookCallbacks => AsyncHook =
29 - typeof async_hooks === 'object'
30 - ? async_hooks.createHook
31 - : function () {
32 - return ({
33 - enable() {},
34 - disable() {},
35 - }: any);
36 - };
37 -export const executionAsyncId: () => number =
38 - typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
39 -
40 -export * from '../ReactFlightServerConfigDebugNode';
26 +export * from '../ReactFlightServerConfigDebugNoop';
27
28 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js
-2
@@ -23,8 +23,6 @@ export const supportsComponentStorage = __DEV__;
23 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26 -export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27 -
26 export * from '../ReactFlightServerConfigDebugNode';
27
28 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-parcel.js
-2
@@ -23,8 +23,6 @@ export const supportsComponentStorage = __DEV__;
23 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26 -export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27 -
26 export * from '../ReactFlightServerConfigDebugNode';
27
28 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js
-2
@@ -23,8 +23,6 @@ export const supportsComponentStorage = __DEV__;
23 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26 -export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27 -
26 export * from '../ReactFlightServerConfigDebugNode';
27
28 export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js
-2
@@ -23,8 +23,6 @@ export const supportsComponentStorage = __DEV__;
23 export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24 supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26 -export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27 -
26 export * from '../ReactFlightServerConfigDebugNode';
27
28 export * from '../ReactFlightStackConfigV8';
packages/shared/ReactTypes.js
+12 -2
@@ -229,12 +229,22 @@ export type ReactErrorInfoDev = {
229
230 export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev;
231
232 -export type ReactAsyncInfo = {
233 - +type: string,
232 +// The point where the Async Info started which might not be the same place it was awaited.
233 +export type ReactIOInfo = {
234 + +start: number, // the start time
235 + +end: number, // the end time (this might be different from the time the await was unblocked)
236 + +stack?: null | ReactStackTrace,
237 // Stashed Data for the Specific Execution Environment. Not part of the transport protocol
238 +debugStack?: null | Error,
239 +debugTask?: null | ConsoleTask,
240 +};
241 +
242 +export type ReactAsyncInfo = {
243 + +awaited: ReactIOInfo,
244 +stack?: null | ReactStackTrace,
245 + // Stashed Data for the Specific Execution Environment. Not part of the transport protocol
246 + +debugStack?: null | Error,
247 + +debugTask?: null | ConsoleTask,
248 };
249
250 export type ReactTimeInfo = {
scripts/jest/setupTests.js
+15
@@ -293,3 +293,18 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
293 return require('internal-test-utils/ReactJSDOM.js');
294 });
295 }
296 +
297 +// We mock createHook so that we can automatically clean it up.
298 +let installedHook = null;
299 +jest.mock('async_hooks', () => {
300 + const actual = jest.requireActual('async_hooks');
301 + return {
302 + ...actual,
303 + createHook(config) {
304 + if (installedHook) {
305 + installedHook.disable();
306 + }
307 + return (installedHook = actual.createHook(config));
308 + },
309 + };
310 +});
scripts/shared/inlinedHostConfigs.js
+32 -8
@@ -50,6 +50,7 @@ module.exports = [
50 'react-devtools-shell',
51 'react-devtools-shared',
52 'shared/ReactDOMSharedInternals',
53 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
54 ],
55 isFlowTyped: true,
56 isServerSupported: true,
@@ -247,6 +248,7 @@ module.exports = [
248 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
249 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
250 'shared/ReactDOMSharedInternals',
251 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
252 ],
253 isFlowTyped: true,
254 isServerSupported: true,
@@ -274,6 +276,7 @@ module.exports = [
276 'react-devtools-shell',
277 'react-devtools-shared',
278 'shared/ReactDOMSharedInternals',
279 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
280 ],
281 isFlowTyped: true,
282 isServerSupported: true,
@@ -309,6 +312,7 @@ module.exports = [
312 'react-devtools-shell',
313 'react-devtools-shared',
314 'shared/ReactDOMSharedInternals',
315 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
316 ],
317 isFlowTyped: true,
318 isServerSupported: true,
@@ -343,6 +347,7 @@ module.exports = [
347 'react-devtools-shell',
348 'react-devtools-shared',
349 'shared/ReactDOMSharedInternals',
350 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
351 ],
352 isFlowTyped: true,
353 isServerSupported: true,
@@ -384,7 +389,7 @@ module.exports = [
389 'react-devtools-shell',
390 'react-devtools-shared',
391 'shared/ReactDOMSharedInternals',
387 - 'react-server/src/ReactFlightServerConfigDebugNode.js',
392 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
393 ],
394 isFlowTyped: true,
395 isServerSupported: true,
@@ -424,7 +429,7 @@ module.exports = [
429 'react-devtools-shell',
430 'react-devtools-shared',
431 'shared/ReactDOMSharedInternals',
427 - 'react-server/src/ReactFlightServerConfigDebugNode.js',
432 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
433 ],
434 isFlowTyped: true,
435 isServerSupported: true,
@@ -463,7 +468,7 @@ module.exports = [
468 'react-devtools-shell',
469 'react-devtools-shared',
470 'shared/ReactDOMSharedInternals',
466 - 'react-server/src/ReactFlightServerConfigDebugNode.js',
471 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
472 ],
473 isFlowTyped: true,
474 isServerSupported: true,
@@ -523,6 +528,7 @@ module.exports = [
528 'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser
529 'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node
530 'shared/ReactDOMSharedInternals',
531 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
532 ],
533 isFlowTyped: true,
534 isServerSupported: true,
@@ -539,6 +545,7 @@ module.exports = [
545 'react-dom-bindings',
546 'react-markup',
547 'shared/ReactDOMSharedInternals',
548 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
549 ],
550 isFlowTyped: true,
551 isServerSupported: true,
@@ -558,6 +565,7 @@ module.exports = [
565 'react-dom-bindings',
566 'react-server-dom-fb',
567 'shared/ReactDOMSharedInternals',
568 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
569 ],
570 isFlowTyped: true,
571 isServerSupported: true,
@@ -566,28 +574,40 @@ module.exports = [
574 {
575 shortName: 'native',
576 entryPoints: ['react-native-renderer'],
569 - paths: ['react-native-renderer'],
577 + paths: [
578 + 'react-native-renderer',
579 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
580 + ],
581 isFlowTyped: true,
582 isServerSupported: false,
583 },
584 {
585 shortName: 'fabric',
586 entryPoints: ['react-native-renderer/fabric'],
576 - paths: ['react-native-renderer'],
587 + paths: [
588 + 'react-native-renderer',
589 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
590 + ],
591 isFlowTyped: true,
592 isServerSupported: false,
593 },
594 {
595 shortName: 'test',
596 entryPoints: ['react-test-renderer'],
583 - paths: ['react-test-renderer'],
597 + paths: [
598 + 'react-test-renderer',
599 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
600 + ],
601 isFlowTyped: true,
602 isServerSupported: false,
603 },
604 {
605 shortName: 'art',
606 entryPoints: ['react-art'],
590 - paths: ['react-art'],
607 + paths: [
608 + 'react-art',
609 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
610 + ],
611 isFlowTyped: false, // TODO: type it.
612 isServerSupported: false,
613 },
@@ -599,7 +619,11 @@ module.exports = [
619 'react-server',
620 'react-server/flight',
621 ],
602 - paths: ['react-client/flight', 'react-server/flight'],
622 + paths: [
623 + 'react-client/flight',
624 + 'react-server/flight',
625 + 'react-server/src/ReactFlightServerConfigDebugNoop.js',
626 + ],
627 isFlowTyped: true,
628 isServerSupported: true,
629 },