@samitouri / QOS-React / commits / 56408a5b12

[Flight] Emit timestamps only in forwards advancing time in debug info (#33482)

Previously you weren't guaranteed to have only advancing time entries, you could jump back in time, but now it omits unnecessary duplicates and clamps automatically if you emit a previous time entry to enforce forwards order only. The reason I didn't do this originally is because `await` can jump in the order because we're trying to encode a graph into a flat timeline for simplicity of the protocol and consumers. ```js async function a() { await fetch1(); await fetch2(); } async function b() { await fetch3(); } async function foo() { const p = a(); await b(); return p; } ``` This can effectively create two parallel sequences: ``` --1.................----2.......-- ------3......--------------------- ``` This can now be flattened to either: ``` --1.................3---2.......-- ``` Or: ``` ------3......1......----2.......-- ``` Depending on which one we visit first. Regardless, information is lost. I'd say that the second one is worse encoding of this scenario because it pretends that we weren't waiting for part of the timespan that we were. To solve this I think we should probably make `emitAsyncSequence` create a temporary flat list and then sort it by start time before emitting. Although we weren't actually blocked since there was some CPU time that was able to proceed to get to 3. So maybe the second one is actually better. If we wanted that consistently we'd have to figure out what the intersection was. --------- Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>

Sebastian Markbåge committed Jun 10, 2025 at 11:03 UTC 56408a5b12fa4099e9dbbeca7f6bc59e1307e507
6 files changed +670 -252
fixtures/flight/src/App.js
+12 -1
@@ -37,8 +37,19 @@ async function delay(text, ms) {
37 return new Promise(resolve => setTimeout(() => resolve(text), ms));
38 }
39
40 +async function delayTwice() {
41 + await delay('', 20);
42 + await delay('', 10);
43 +}
44 +
45 +async function delayTrice() {
46 + const p = delayTwice();
47 + await delay('', 40);
48 + return p;
49 +}
50 +
51 async function Bar({children}) {
41 - await delay('deferred text', 10);
52 + await delayTrice();
53 return <div>{children}</div>;
54 }
55
packages/react-client/src/ReactFlightClient.js
+97 -70
@@ -2902,6 +2902,46 @@ function resolveTypedArray(
2902 resolveBuffer(response, id, view);
2903 }
2904
2905 +function logComponentInfo(
2906 + response: Response,
2907 + root: SomeChunk<any>,
2908 + componentInfo: ReactComponentInfo,
2909 + trackIdx: number,
2910 + startTime: number,
2911 + componentEndTime: number,
2912 + childrenEndTime: number,
2913 + isLastComponent: boolean,
2914 +): void {
2915 + // $FlowFixMe: Refined.
2916 + if (
2917 + isLastComponent &&
2918 + root.status === ERRORED &&
2919 + root.reason !== response._closedReason
2920 + ) {
2921 + // If this is the last component to render before this chunk rejected, then conceptually
2922 + // this component errored. If this was a cancellation then it wasn't this component that
2923 + // errored.
2924 + logComponentErrored(
2925 + componentInfo,
2926 + trackIdx,
2927 + startTime,
2928 + componentEndTime,
2929 + childrenEndTime,
2930 + response._rootEnvironmentName,
2931 + root.reason,
2932 + );
2933 + } else {
2934 + logComponentRender(
2935 + componentInfo,
2936 + trackIdx,
2937 + startTime,
2938 + componentEndTime,
2939 + childrenEndTime,
2940 + response._rootEnvironmentName,
2941 + );
2942 + }
2943 +}
2944 +
2945 function flushComponentPerformance(
2946 response: Response,
2947 root: SomeChunk<any>,
@@ -2957,21 +2997,20 @@ function flushComponentPerformance(
2997 // in parallel with the previous.
2998 const debugInfo = __DEV__ && root._debugInfo;
2999 if (debugInfo) {
2960 - for (let i = 1; i < debugInfo.length; i++) {
3000 + let startTime = 0;
3001 + for (let i = 0; i < debugInfo.length; i++) {
3002 const info = debugInfo[i];
3003 + if (typeof info.time === 'number') {
3004 + startTime = info.time;
3005 + }
3006 if (typeof info.name === 'string') {
2963 - // $FlowFixMe: Refined.
2964 - const startTimeInfo = debugInfo[i - 1];
2965 - if (typeof startTimeInfo.time === 'number') {
2966 - const startTime = startTimeInfo.time;
2967 - if (startTime < trackTime) {
2968 - // The start time of this component is before the end time of the previous
2969 - // component on this track so we need to bump the next one to a parallel track.
2970 - trackIdx++;
2971 - }
2972 - trackTime = startTime;
2973 - break;
3007 + if (startTime < trackTime) {
3008 + // The start time of this component is before the end time of the previous
3009 + // component on this track so we need to bump the next one to a parallel track.
3010 + trackIdx++;
3011 }
3012 + trackTime = startTime;
3013 + break;
3014 }
3015 }
3016 for (let i = debugInfo.length - 1; i >= 0; i--) {
@@ -2979,6 +3018,7 @@ function flushComponentPerformance(
3018 if (typeof info.time === 'number') {
3019 if (info.time > parentEndTime) {
3020 parentEndTime = info.time;
3021 + break; // We assume the highest number is at the end.
3022 }
3023 }
3024 }
@@ -3006,85 +3046,72 @@ function flushComponentPerformance(
3046 }
3047 childTrackIdx = childResult.track;
3048 const childEndTime = childResult.endTime;
3009 - childTrackTime = childEndTime;
3049 + if (childEndTime > childTrackTime) {
3050 + childTrackTime = childEndTime;
3051 + }
3052 if (childEndTime > childrenEndTime) {
3053 childrenEndTime = childEndTime;
3054 }
3055 }
3056
3057 if (debugInfo) {
3016 - let endTime = 0;
3058 + // Write debug info in reverse order (just like stack traces).
3059 + let componentEndTime = 0;
3060 let isLastComponent = true;
3061 + let endTime = -1;
3062 + let endTimeIdx = -1;
3063 for (let i = debugInfo.length - 1; i >= 0; i--) {
3064 const info = debugInfo[i];
3020 - if (typeof info.time === 'number') {
3021 - if (info.time > childrenEndTime) {
3022 - childrenEndTime = info.time;
3023 - }
3024 - if (endTime === 0) {
3025 - // Last timestamp is the end of the last component.
3026 - endTime = info.time;
3027 - }
3065 + if (typeof info.time !== 'number') {
3066 + continue;
3067 }
3029 - if (typeof info.name === 'string' && i > 0) {
3030 - // $FlowFixMe: Refined.
3031 - const componentInfo: ReactComponentInfo = info;
3032 - const startTimeInfo = debugInfo[i - 1];
3033 - if (typeof startTimeInfo.time === 'number') {
3034 - const startTime = startTimeInfo.time;
3035 - if (
3036 - isLastComponent &&
3037 - root.status === ERRORED &&
3038 - root.reason !== response._closedReason
3039 - ) {
3040 - // If this is the last component to render before this chunk rejected, then conceptually
3041 - // this component errored. If this was a cancellation then it wasn't this component that
3042 - // errored.
3043 - logComponentErrored(
3068 + if (componentEndTime === 0) {
3069 + // Last timestamp is the end of the last component.
3070 + componentEndTime = info.time;
3071 + }
3072 + const time = info.time;
3073 + if (endTimeIdx > -1) {
3074 + // Now that we know the start and end time, we can emit the entries between.
3075 + for (let j = endTimeIdx - 1; j > i; j--) {
3076 + const candidateInfo = debugInfo[j];
3077 + if (typeof candidateInfo.name === 'string') {
3078 + if (componentEndTime > childrenEndTime) {
3079 + childrenEndTime = componentEndTime;
3080 + }
3081 + // $FlowFixMe: Refined.
3082 + const componentInfo: ReactComponentInfo = candidateInfo;
3083 + logComponentInfo(
3084 + response,
3085 + root,
3086 componentInfo,
3087 trackIdx,
3046 - startTime,
3047 - endTime,
3088 + time,
3089 + componentEndTime,
3090 childrenEndTime,
3049 - response._rootEnvironmentName,
3050 - root.reason,
3091 + isLastComponent,
3092 );
3052 - } else {
3053 - logComponentRender(
3054 - componentInfo,
3093 + componentEndTime = time; // The end time of previous component is the start time of the next.
3094 + // Track the root most component of the result for deduping logging.
3095 + result.component = componentInfo;
3096 + isLastComponent = false;
3097 + } else if (candidateInfo.awaited) {
3098 + if (endTime > childrenEndTime) {
3099 + childrenEndTime = endTime;
3100 + }
3101 + // $FlowFixMe: Refined.
3102 + const asyncInfo: ReactAsyncInfo = candidateInfo;
3103 + logComponentAwait(
3104 + asyncInfo,
3105 trackIdx,
3056 - startTime,
3106 + time,
3107 endTime,
3058 - childrenEndTime,
3108 response._rootEnvironmentName,
3109 );
3110 }
3062 - // Track the root most component of the result for deduping logging.
3063 - result.component = componentInfo;
3064 - // Set the end time of the previous component to the start of the previous.
3065 - endTime = startTime;
3066 - }
3067 - isLastComponent = false;
3068 - } else if (info.awaited && i > 0 && i < debugInfo.length - 2) {
3069 - // $FlowFixMe: Refined.
3070 - const asyncInfo: ReactAsyncInfo = info;
3071 - const startTimeInfo = debugInfo[i - 1];
3072 - const endTimeInfo = debugInfo[i + 1];
3073 - if (
3074 - typeof startTimeInfo.time === 'number' &&
3075 - typeof endTimeInfo.time === 'number'
3076 - ) {
3077 - const awaitStartTime = startTimeInfo.time;
3078 - const awaitEndTime = endTimeInfo.time;
3079 - logComponentAwait(
3080 - asyncInfo,
3081 - trackIdx,
3082 - awaitStartTime,
3083 - awaitEndTime,
3084 - response._rootEnvironmentName,
3085 - );
3111 }
3112 }
3113 + endTime = time; // The end time of the next entry is this time.
3114 + endTimeIdx = i;
3115 }
3116 }
3117 result.endTime = childrenEndTime;
packages/react-server/src/ReactFlightAsyncSequence.js
+1 -1
@@ -38,7 +38,7 @@ export type PromiseNode = {
38 start: number, // start time when the Promise was created
39 end: number, // end time when the Promise was resolved.
40 awaited: null | AsyncSequence, // the thing that ended up resolving this promise
41 - previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
41 + previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
42 };
43
44 export type AwaitNode = {
packages/react-server/src/ReactFlightServer.js
+68 -85
@@ -777,6 +777,10 @@ function serializeThenable(
777 }
778 }
779 if (newTask.status === PENDING) {
780 + if (enableProfilerTimer && enableComponentPerformanceTrack) {
781 + // If this is async we need to time when this task finishes.
782 + newTask.timed = true;
783 + }
784 // We expect that the only status it might be otherwise is ABORTED.
785 // When we abort we emit chunks in each pending task slot and don't need
786 // to do so again here.
@@ -786,11 +790,6 @@ function serializeThenable(
790 },
791 );
792
789 - if (enableProfilerTimer && enableComponentPerformanceTrack) {
790 - // If this is async we need to time when this task finishes.
791 - newTask.timed = true;
792 - }
793 -
793 return newTask.id;
794 }
795
@@ -1341,12 +1340,7 @@ function renderFunctionComponent<Props>(
1340
1341 // Track when we started rendering this component.
1342 if (enableProfilerTimer && enableComponentPerformanceTrack) {
1344 - task.timed = true;
1345 - emitTimingChunk(
1346 - request,
1347 - componentDebugID,
1348 - (task.time = performance.now()),
1349 - );
1343 + advanceTaskTime(request, task, performance.now());
1344 }
1345
1346 emitDebugChunk(request, componentDebugID, componentDebugInfo);
@@ -1890,8 +1884,8 @@ function visitAsyncNode(
1884 request: Request,
1885 task: Task,
1886 node: AsyncSequence,
1893 - cutOff: number,
1887 visited: Set<AsyncSequence>,
1888 + cutOff: number,
1889 ): null | PromiseNode | IONode {
1890 if (visited.has(node)) {
1891 // It's possible to visit them same node twice when it's part of both an "awaited" path
@@ -1900,11 +1894,11 @@ function visitAsyncNode(
1894 }
1895 visited.add(node);
1896 // First visit anything that blocked this sequence to start in the first place.
1903 - if (node.previous !== null) {
1897 + if (node.previous !== null && node.end > request.timeOrigin) {
1898 // We ignore the return value here because if it wasn't awaited in user space, then we don't log it.
1899 // It also means that it can just have been part of a previous component's render.
1900 // TODO: This means that some I/O can get lost that was still blocking the sequence.
1907 - visitAsyncNode(request, task, node.previous, cutOff, visited);
1901 + visitAsyncNode(request, task, node.previous, visited, cutOff);
1902 }
1903 switch (node.tag) {
1904 case IO_NODE: {
@@ -1923,24 +1917,23 @@ function visitAsyncNode(
1917 const awaited = node.awaited;
1918 let match = null;
1919 if (awaited !== null) {
1926 - const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1920 + const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
1921 if (ioNode !== null) {
1922 // This Promise was blocked on I/O. That's a signal that this Promise is interesting to log.
1923 // We don't log it yet though. We return it to be logged by the point where it's awaited.
1924 // The ioNode might be another PromiseNode in the case where none of the AwaitNode had
1925 // unfiltered stacks.
1932 - if (
1926 + if (ioNode.tag === PROMISE_NODE) {
1927 + // If the ioNode was a Promise, then that means we found one in user space since otherwise
1928 + // we would've returned an IO node. We assume this has the best stack.
1929 + match = ioNode;
1930 + } else if (
1931 filterStackTrace(request, parseStackTrace(node.stack, 1)).length ===
1932 0
1933 ) {
1936 - // Typically we assume that the outer most Promise that was awaited in user space has the
1937 - // most actionable stack trace for the start of the operation. However, if this Promise
1938 - // was created inside only third party code, then try to use the inner node instead.
1939 - // This could happen if you pass a first party Promise into a third party to be awaited there.
1940 - if (ioNode.end < 0) {
1941 - // If we haven't defined an end time, use the resolve of the outer Promise.
1942 - ioNode.end = node.end;
1943 - }
1934 + // If this Promise was created inside only third party code, then try to use
1935 + // the inner I/O node instead. This could happen if third party calls into first
1936 + // party to perform some I/O.
1937 match = ioNode;
1938 } else {
1939 match = node;
@@ -1955,30 +1948,17 @@ function visitAsyncNode(
1948 }
1949 return match;
1950 }
1958 - case UNRESOLVED_AWAIT_NODE:
1959 - // We could be inside the .then() which is about to resolve this node.
1960 - // TODO: We could call emitAsyncSequence in a microtask to avoid this issue.
1961 - // Fallthrough to the resolved path.
1951 + case UNRESOLVED_AWAIT_NODE: {
1952 + return null;
1953 + }
1954 case AWAIT_NODE: {
1955 const awaited = node.awaited;
1956 let match = null;
1957 if (awaited !== null) {
1966 - const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
1958 + const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff);
1959 if (ioNode !== null) {
1960 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.
1972 - // This can happen because the ping gets invoked before the await gets resolved.
1973 - if (ioNode.end < node.start) {
1974 - // If we're awaiting a resolved Promise it could have finished before we started.
1975 - endTime = node.start;
1976 - } else {
1977 - endTime = ioNode.end;
1978 - }
1979 - } else {
1980 - endTime = node.end;
1981 - }
1961 + const endTime: number = node.end;
1962 if (endTime <= request.timeOrigin) {
1963 // This was already resolved when we started this render. It must have been either something
1964 // that's part of a start up sequence or externally cached data. We exclude that information.
@@ -2002,14 +1982,12 @@ function visitAsyncNode(
1982 match = ioNode;
1983 } else {
1984 // Outline the IO node.
2005 - if (ioNode.end < 0) {
2006 - ioNode.end = endTime;
2007 - }
1985 serializeIONode(request, ioNode);
1986 +
1987 // We log the environment at the time when the last promise pigned ping which may
1988 // be later than what the environment was when we actually started awaiting.
1989 const env = (0, request.environmentName)();
2012 - emitTimingChunk(request, task.id, startTime);
1990 + advanceTaskTime(request, task, startTime);
1991 // Then emit a reference to us awaiting it in the current task.
1992 request.pendingChunks++;
1993 emitDebugChunk(request, task.id, {
@@ -2018,23 +1996,14 @@ function visitAsyncNode(
1996 owner: node.owner,
1997 stack: stack,
1998 });
2021 - emitTimingChunk(request, task.id, endTime);
1999 + markOperationEndTime(request, task, endTime);
2000 }
2001 }
2002 }
2003 }
2004 // We need to forward after we visit awaited nodes because what ever I/O we requested that's
2005 // the thing that generated this node and its virtual children.
2028 - let debugInfo: null | ReactDebugInfo;
2029 - if (node.tag === UNRESOLVED_AWAIT_NODE) {
2030 - const promise = node.debugInfo.deref();
2031 - debugInfo =
2032 - promise === undefined || promise._debugInfo === undefined
2033 - ? null
2034 - : promise._debugInfo;
2035 - } else {
2036 - debugInfo = node.debugInfo;
2037 - }
2006 + const debugInfo: null | ReactDebugInfo = node.debugInfo;
2007 if (debugInfo !== null) {
2008 forwardDebugInfo(request, task, debugInfo);
2009 }
@@ -2051,37 +2020,23 @@ function emitAsyncSequence(
2020 request: Request,
2021 task: Task,
2022 node: AsyncSequence,
2054 - cutOff: number,
2023 ): void {
2024 const visited: Set<AsyncSequence> = new Set();
2057 - const awaitedNode = visitAsyncNode(request, task, node, cutOff, visited);
2025 + const awaitedNode = visitAsyncNode(request, task, node, visited, task.time);
2026 if (awaitedNode !== null) {
2027 // Nothing in user space (unfiltered stack) awaited this.
2060 - if (awaitedNode.end < 0) {
2061 - // If this was I/O directly without a Promise, then it means that some custom Thenable
2062 - // called our ping directly and not from a native .then(). We use the current ping time
2063 - // as the end time and treat it as an await with no stack.
2064 - // TODO: If this I/O is recurring then we really should have different entries for
2065 - // each occurrence. Right now we'll only track the first time it is invoked.
2066 - awaitedNode.end = performance.now();
2067 - }
2028 serializeIONode(request, awaitedNode);
2029 request.pendingChunks++;
2030 // We log the environment at the time when we ping which may be later than what the
2031 // environment was when we actually started awaiting.
2032 const env = (0, request.environmentName)();
2033 // 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.
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);
2034 + // when we yielded after rendering. The current task time is basically that.
2035 emitDebugChunk(request, task.id, {
2036 awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
2037 env: env,
2038 });
2084 - emitTimingChunk(request, task.id, awaitEndTime);
2039 + markOperationEndTime(request, task, awaitedNode.end);
2040 }
2041 }
2042
@@ -2092,7 +2047,7 @@ function pingTask(request: Request, task: Task): void {
2047 if (enableAsyncDebugInfo) {
2048 const sequence = getCurrentAsyncSequence();
2049 if (sequence !== null) {
2095 - emitAsyncSequence(request, task, sequence, task.time);
2050 + emitAsyncSequence(request, task, sequence);
2051 }
2052 }
2053 }
@@ -4295,19 +4250,13 @@ function forwardDebugInfo(
4250 debugInfo: ReactDebugInfo,
4251 ) {
4252 const id = task.id;
4298 - const minimumTime =
4299 - enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0;
4253 for (let i = 0; i < debugInfo.length; i++) {
4254 const info = debugInfo[i];
4255 if (typeof info.time === 'number') {
4256 // When forwarding time we need to ensure to convert it to the time space of the payload.
4257 // We clamp the time to the starting render of the current component. It's as if it took
4258 // 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 - );
4259 + markOperationEndTime(request, task, info.time);
4260 } else {
4261 if (typeof info.name === 'string') {
4262 // We outline this model eagerly so that we can refer to by reference as an owner.
@@ -4384,6 +4333,40 @@ function emitTimingChunk(
4333 request.completedRegularChunks.push(processedChunk);
4334 }
4335
4336 +function advanceTaskTime(
4337 + request: Request,
4338 + task: Task,
4339 + timestamp: number,
4340 +): void {
4341 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
4342 + return;
4343 + }
4344 + // Emits a timing chunk, if the new timestamp is higher than the previous timestamp of this task.
4345 + if (timestamp > task.time) {
4346 + emitTimingChunk(request, task.id, timestamp);
4347 + task.time = timestamp;
4348 + } else if (!task.timed) {
4349 + // If it wasn't timed before, e.g. an outlined object, we need to emit the first timestamp and
4350 + // it is now timed.
4351 + emitTimingChunk(request, task.id, task.time);
4352 + }
4353 + task.timed = true;
4354 +}
4355 +
4356 +function markOperationEndTime(request: Request, task: Task, timestamp: number) {
4357 + if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
4358 + return;
4359 + }
4360 + // This is like advanceTaskTime() but always emits a timing chunk even if it doesn't advance.
4361 + // This ensures that the end time of the previous entry isn't implied to be the start of the next one.
4362 + if (timestamp > task.time) {
4363 + emitTimingChunk(request, task.id, timestamp);
4364 + task.time = timestamp;
4365 + } else {
4366 + emitTimingChunk(request, task.id, task.time);
4367 + }
4368 +}
4369 +
4370 function emitChunk(
4371 request: Request,
4372 task: Task,
@@ -4475,7 +4458,7 @@ function emitChunk(
4458 function erroredTask(request: Request, task: Task, error: mixed): void {
4459 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4460 if (task.timed) {
4478 - emitTimingChunk(request, task.id, (task.time = performance.now()));
4461 + markOperationEndTime(request, task, performance.now());
4462 }
4463 }
4464 task.status = ERRORED;
@@ -4558,7 +4541,7 @@ function retryTask(request: Request, task: Task): void {
4541 // We've finished rendering. Log the end time.
4542 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4543 if (task.timed) {
4561 - emitTimingChunk(request, task.id, (task.time = performance.now()));
4544 + markOperationEndTime(request, task, performance.now());
4545 }
4546 }
4547
@@ -4685,7 +4668,7 @@ function abortTask(task: Task, request: Request, errorId: number): void {
4668 // Track when we aborted this task as its end time.
4669 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4670 if (task.timed) {
4688 - emitTimingChunk(request, task.id, (task.time = performance.now()));
4671 + markOperationEndTime(request, task, performance.now());
4672 }
4673 }
4674 // Instead of emitting an error per task.id, we emit a model that only
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+77 -22
@@ -30,6 +30,27 @@ import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
30 const pendingOperations: Map<number, AsyncSequence> =
31 __DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
32
33 +// Keep the last resolved await as a workaround for async functions missing data.
34 +let lastRanAwait: null | AwaitNode = null;
35 +
36 +function resolvePromiseOrAwaitNode(
37 + unresolvedNode: UnresolvedAwaitNode | UnresolvedPromiseNode,
38 + endTime: number,
39 +): AwaitNode | PromiseNode {
40 + const resolvedNode: AwaitNode | PromiseNode = (unresolvedNode: any);
41 + resolvedNode.tag = ((unresolvedNode.tag === UNRESOLVED_PROMISE_NODE
42 + ? PROMISE_NODE
43 + : AWAIT_NODE): any);
44 + // The Promise can be garbage collected after this so we should extract debugInfo first.
45 + const promise = unresolvedNode.debugInfo.deref();
46 + resolvedNode.debugInfo =
47 + promise === undefined || promise._debugInfo === undefined
48 + ? null
49 + : promise._debugInfo;
50 + resolvedNode.end = endTime;
51 + return resolvedNode;
52 +}
53 +
54 // Initialize the tracing of async operations.
55 // We do this globally since the async work can potentially eagerly
56 // start before the first request and once requests start they can interleave.
@@ -129,42 +150,76 @@ export function initAsyncDebugInfo(): void {
150 }
151 pendingOperations.set(asyncId, node);
152 },
132 - promiseResolve(asyncId: number): void {
153 + before(asyncId: number): void {
154 const node = pendingOperations.get(asyncId);
155 if (node !== undefined) {
135 - let resolvedNode: AwaitNode | PromiseNode;
156 switch (node.tag) {
157 + case IO_NODE: {
158 + lastRanAwait = null;
159 + // Log the end time when we resolved the I/O. This can happen
160 + // more than once if it's a recurring resource like a connection.
161 + const ioNode: IONode = (node: any);
162 + ioNode.end = performance.now();
163 + break;
164 + }
165 case UNRESOLVED_AWAIT_NODE: {
138 - const awaitNode: AwaitNode = (node: any);
139 - awaitNode.tag = AWAIT_NODE;
140 - resolvedNode = awaitNode;
166 + // If we begin before we resolve, that means that this is actually already resolved but
167 + // the promiseResolve hook is called at the end of the execution. So we track the time
168 + // in the before call instead.
169 + // $FlowFixMe
170 + lastRanAwait = resolvePromiseOrAwaitNode(node, performance.now());
171 break;
172 }
143 - case UNRESOLVED_PROMISE_NODE: {
144 - const promiseNode: PromiseNode = (node: any);
145 - promiseNode.tag = PROMISE_NODE;
146 - resolvedNode = promiseNode;
173 + case AWAIT_NODE: {
174 + lastRanAwait = node;
175 break;
176 }
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.',
177 + case UNRESOLVED_PROMISE_NODE: {
178 + // We typically don't expected Promises to have an execution scope since only the awaits
179 + // have a then() callback. However, this can happen for native async functions. The last
180 + // piece of code that executes the return after the last await has the execution context
181 + // of the Promise.
182 + const resolvedNode = resolvePromiseOrAwaitNode(
183 + node,
184 + performance.now(),
185 );
186 + // We are missing information about what this was unblocked by but we can guess that it
187 + // was whatever await we ran last since this will continue in a microtask after that.
188 + // This is not perfect because there could potentially be other microtasks getting in
189 + // between.
190 + resolvedNode.previous = lastRanAwait;
191 + lastRanAwait = null;
192 + break;
193 + }
194 + default: {
195 + lastRanAwait = null;
196 + }
197 + }
198 + }
199 + },
200 +
201 + promiseResolve(asyncId: number): void {
202 + const node = pendingOperations.get(asyncId);
203 + if (node !== undefined) {
204 + let resolvedNode: AwaitNode | PromiseNode;
205 + switch (node.tag) {
206 + case UNRESOLVED_AWAIT_NODE:
207 + case UNRESOLVED_PROMISE_NODE: {
208 + resolvedNode = resolvePromiseOrAwaitNode(node, performance.now());
209 + break;
210 + }
211 + case AWAIT_NODE:
212 + case PROMISE_NODE: {
213 + // We already resolved this in the before hook.
214 + resolvedNode = node;
215 + break;
216 + }
217 default:
218 // eslint-disable-next-line react-internal/prod-error-codes
219 throw new Error(
157 - 'A Promise should never be resolved twice. This is a bug in React or Node.js.',
220 + 'A Promise should never be an IO_NODE. This is a bug in React.',
221 );
222 }
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;
223 const currentAsyncId = executionAsyncId();
224 if (asyncId !== currentAsyncId) {
225 // If the promise was not resolved by itself, then that means that
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+415 -73
@@ -419,7 +419,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
419 "awaited": {
420 "end": 0,
421 "env": "Server",
422 - "name": "getData",
422 + "name": "delay",
423 "owner": {
424 "env": "Server",
425 "key": null,
@@ -438,19 +438,19 @@ describe('ReactFlightAsyncDebugInfo', () => {
438 },
439 "stack": [
440 [
441 - "getData",
441 + "delay",
442 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
443 - 156,
444 - 27,
445 - 156,
446 - 5,
443 + 133,
444 + 12,
445 + 132,
446 + 3,
447 ],
448 [
449 - "Component",
449 + "getData",
450 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
451 - 165,
452 - 22,
453 - 163,
451 + 158,
452 + 21,
453 + 156,
454 5,
455 ],
456 ],
@@ -547,9 +547,6 @@ describe('ReactFlightAsyncDebugInfo', () => {
547 ],
548 ],
549 },
550 - {
551 - "time": 0,
552 - },
550 {
551 "awaited": {
552 "end": 0,
@@ -637,9 +634,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
634 [
635 "Object.<anonymous>",
636 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
640 - 608,
637 + 605,
638 109,
642 - 599,
639 + 596,
640 94,
641 ],
642 ],
@@ -708,9 +705,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
705 [
706 "Object.<anonymous>",
707 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
711 - 679,
708 + 676,
709 109,
713 - 655,
710 + 652,
711 50,
712 ],
713 ],
@@ -790,9 +787,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
787 [
788 "Object.<anonymous>",
789 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
793 - 761,
790 + 758,
791 109,
795 - 744,
792 + 741,
793 63,
794 ],
795 ],
@@ -817,9 +814,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
814 [
815 "Component",
816 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
820 - 757,
817 + 754,
818 24,
822 - 756,
819 + 753,
820 5,
821 ],
822 ],
@@ -849,9 +846,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
846 [
847 "Component",
848 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
852 - 757,
849 + 754,
850 24,
854 - 756,
851 + 753,
852 5,
853 ],
854 ],
@@ -868,17 +865,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
865 [
866 "getData",
867 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
871 - 746,
868 + 743,
869 13,
873 - 745,
870 + 742,
871 5,
872 ],
873 [
874 "ThirdPartyComponent",
875 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
879 - 752,
876 + 749,
877 24,
881 - 751,
878 + 748,
879 5,
880 ],
881 ],
@@ -902,9 +899,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
899 [
900 "Component",
901 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
905 - 757,
902 + 754,
903 24,
907 - 756,
904 + 753,
905 5,
906 ],
907 ],
@@ -913,17 +910,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
910 [
911 "getData",
912 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
916 - 746,
913 + 743,
914 13,
918 - 745,
915 + 742,
916 5,
917 ],
918 [
919 "ThirdPartyComponent",
920 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
924 - 752,
921 + 749,
922 24,
926 - 751,
923 + 748,
924 5,
925 ],
926 ],
@@ -956,9 +953,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
953 [
954 "Component",
955 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
959 - 757,
956 + 754,
957 24,
961 - 756,
958 + 753,
959 5,
960 ],
961 ],
@@ -975,17 +972,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
972 [
973 "getData",
974 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
978 - 747,
975 + 744,
976 13,
980 - 745,
977 + 742,
978 5,
979 ],
980 [
981 "ThirdPartyComponent",
982 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
986 - 752,
983 + 749,
984 18,
988 - 751,
985 + 748,
986 5,
987 ],
988 ],
@@ -1009,9 +1006,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1006 [
1007 "Component",
1008 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1012 - 757,
1009 + 754,
1010 24,
1014 - 756,
1011 + 753,
1012 5,
1013 ],
1014 ],
@@ -1020,17 +1017,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
1017 [
1018 "getData",
1019 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1023 - 747,
1020 + 744,
1021 13,
1025 - 745,
1022 + 742,
1023 5,
1024 ],
1025 [
1026 "ThirdPartyComponent",
1027 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1031 - 752,
1028 + 749,
1029 18,
1033 - 751,
1030 + 748,
1031 5,
1032 ],
1033 ],
@@ -1108,9 +1105,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1105 [
1106 "Object.<anonymous>",
1107 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1111 - 1074,
1108 + 1071,
1109 40,
1113 - 1052,
1110 + 1049,
1111 62,
1112 ],
1113 ],
@@ -1132,9 +1129,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1129 [
1130 "Object.<anonymous>",
1131 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1135 - 1074,
1132 + 1071,
1133 40,
1137 - 1052,
1134 + 1049,
1135 62,
1136 ],
1137 ],
@@ -1151,17 +1148,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
1148 [
1149 "getData",
1150 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1154 - 1059,
1151 + 1056,
1152 13,
1156 - 1055,
1153 + 1052,
1154 25,
1155 ],
1156 [
1157 "Component",
1158 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1162 - 1069,
1159 + 1066,
1160 13,
1164 - 1068,
1161 + 1065,
1162 5,
1163 ],
1164 ],
@@ -1177,9 +1174,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1174 [
1175 "Object.<anonymous>",
1176 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1180 - 1074,
1177 + 1071,
1178 40,
1182 - 1052,
1179 + 1049,
1180 62,
1181 ],
1182 ],
@@ -1188,17 +1185,17 @@ describe('ReactFlightAsyncDebugInfo', () => {
1185 [
1186 "getData",
1187 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1191 - 1059,
1188 + 1056,
1189 13,
1193 - 1055,
1190 + 1052,
1191 25,
1192 ],
1193 [
1194 "Component",
1195 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1199 - 1069,
1196 + 1066,
1197 13,
1201 - 1068,
1198 + 1065,
1199 5,
1200 ],
1201 ],
@@ -1218,9 +1215,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1215 [
1216 "Component",
1217 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1221 - 1070,
1218 + 1067,
1219 60,
1223 - 1068,
1220 + 1065,
1221 5,
1222 ],
1223 ],
@@ -1232,7 +1229,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
1229 "awaited": {
1230 "end": 0,
1231 "env": "Server",
1235 - "name": "getData",
1232 + "name": "delay",
1233 "owner": {
1234 "env": "Server",
1235 "key": null,
@@ -1242,28 +1239,36 @@ describe('ReactFlightAsyncDebugInfo', () => {
1239 [
1240 "Object.<anonymous>",
1241 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1245 - 1074,
1242 + 1071,
1243 40,
1247 - 1052,
1244 + 1049,
1245 62,
1246 ],
1247 ],
1248 },
1249 "stack": [
1250 + [
1251 + "delay",
1252 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1253 + 133,
1254 + 12,
1255 + 132,
1256 + 3,
1257 + ],
1258 [
1259 "getData",
1260 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1256 - 1055,
1257 - 47,
1258 - 1055,
1261 + 1056,
1262 + 13,
1263 + 1052,
1264 25,
1265 ],
1266 [
1267 "Component",
1268 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1264 - 1069,
1269 + 1066,
1270 13,
1266 - 1068,
1271 + 1065,
1272 5,
1273 ],
1274 ],
@@ -1279,9 +1284,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
1284 [
1285 "Component",
1286 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1282 - 1070,
1287 + 1067,
1288 60,
1284 - 1068,
1289 + 1065,
1290 5,
1291 ],
1292 ],
@@ -1290,9 +1295,346 @@ describe('ReactFlightAsyncDebugInfo', () => {
1295 [
1296 "Child",
1297 "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1293 - 1064,
1298 + 1061,
1299 28,
1295 - 1063,
1300 + 1060,
1301 + 5,
1302 + ],
1303 + ],
1304 + },
1305 + {
1306 + "time": 0,
1307 + },
1308 + {
1309 + "time": 0,
1310 + },
1311 + ]
1312 + `);
1313 + }
1314 + });
1315 +
1316 + it('can track implicit returned promises that are blocked by previous data', async () => {
1317 + async function delayTwice() {
1318 + await delay('', 20);
1319 + await delay('', 10);
1320 + }
1321 +
1322 + async function delayTrice() {
1323 + const p = delayTwice();
1324 + await delay('', 40);
1325 + return p;
1326 + }
1327 +
1328 + async function Bar({children}) {
1329 + await delayTrice();
1330 + return 'hi';
1331 + }
1332 +
1333 + const stream = ReactServerDOMServer.renderToPipeableStream(
1334 + <Bar />,
1335 + {},
1336 + {
1337 + filterStackFrame,
1338 + },
1339 + );
1340 +
1341 + const readable = new Stream.PassThrough(streamOptions);
1342 +
1343 + const result = ReactServerDOMClient.createFromNodeStream(readable, {
1344 + moduleMap: {},
1345 + moduleLoading: {},
1346 + });
1347 + stream.pipe(readable);
1348 +
1349 + expect(await result).toBe('hi');
1350 + if (
1351 + __DEV__ &&
1352 + gate(
1353 + flags =>
1354 + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
1355 + )
1356 + ) {
1357 + expect(getDebugInfo(result)).toMatchInlineSnapshot(`
1358 + [
1359 + {
1360 + "time": 0,
1361 + },
1362 + {
1363 + "env": "Server",
1364 + "key": null,
1365 + "name": "Bar",
1366 + "props": {},
1367 + "stack": [
1368 + [
1369 + "Object.<anonymous>",
1370 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1371 + 1334,
1372 + 40,
1373 + 1316,
1374 + 80,
1375 + ],
1376 + ],
1377 + },
1378 + {
1379 + "time": 0,
1380 + },
1381 + {
1382 + "awaited": {
1383 + "end": 0,
1384 + "env": "Server",
1385 + "name": "delay",
1386 + "owner": {
1387 + "env": "Server",
1388 + "key": null,
1389 + "name": "Bar",
1390 + "props": {},
1391 + "stack": [
1392 + [
1393 + "Object.<anonymous>",
1394 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1395 + 1334,
1396 + 40,
1397 + 1316,
1398 + 80,
1399 + ],
1400 + ],
1401 + },
1402 + "stack": [
1403 + [
1404 + "delay",
1405 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1406 + 133,
1407 + 12,
1408 + 132,
1409 + 3,
1410 + ],
1411 + [
1412 + "delayTrice",
1413 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1414 + 1324,
1415 + 13,
1416 + 1322,
1417 + 5,
1418 + ],
1419 + [
1420 + "Bar",
1421 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1422 + 1329,
1423 + 13,
1424 + 1328,
1425 + 5,
1426 + ],
1427 + ],
1428 + "start": 0,
1429 + },
1430 + "env": "Server",
1431 + "owner": {
1432 + "env": "Server",
1433 + "key": null,
1434 + "name": "Bar",
1435 + "props": {},
1436 + "stack": [
1437 + [
1438 + "Object.<anonymous>",
1439 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1440 + 1334,
1441 + 40,
1442 + 1316,
1443 + 80,
1444 + ],
1445 + ],
1446 + },
1447 + "stack": [
1448 + [
1449 + "delayTrice",
1450 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1451 + 1324,
1452 + 13,
1453 + 1322,
1454 + 5,
1455 + ],
1456 + [
1457 + "Bar",
1458 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1459 + 1329,
1460 + 13,
1461 + 1328,
1462 + 5,
1463 + ],
1464 + ],
1465 + },
1466 + {
1467 + "time": 0,
1468 + },
1469 + {
1470 + "awaited": {
1471 + "end": 0,
1472 + "env": "Server",
1473 + "name": "delay",
1474 + "owner": {
1475 + "env": "Server",
1476 + "key": null,
1477 + "name": "Bar",
1478 + "props": {},
1479 + "stack": [
1480 + [
1481 + "Object.<anonymous>",
1482 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1483 + 1334,
1484 + 40,
1485 + 1316,
1486 + 80,
1487 + ],
1488 + ],
1489 + },
1490 + "stack": [
1491 + [
1492 + "delay",
1493 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1494 + 133,
1495 + 12,
1496 + 132,
1497 + 3,
1498 + ],
1499 + [
1500 + "delayTwice",
1501 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1502 + 1318,
1503 + 13,
1504 + 1317,
1505 + 5,
1506 + ],
1507 + [
1508 + "delayTrice",
1509 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1510 + 1323,
1511 + 15,
1512 + 1322,
1513 + 5,
1514 + ],
1515 + [
1516 + "Bar",
1517 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1518 + 1329,
1519 + 13,
1520 + 1328,
1521 + 5,
1522 + ],
1523 + ],
1524 + "start": 0,
1525 + },
1526 + "env": "Server",
1527 + "owner": {
1528 + "env": "Server",
1529 + "key": null,
1530 + "name": "Bar",
1531 + "props": {},
1532 + "stack": [
1533 + [
1534 + "Object.<anonymous>",
1535 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1536 + 1334,
1537 + 40,
1538 + 1316,
1539 + 80,
1540 + ],
1541 + ],
1542 + },
1543 + "stack": [
1544 + [
1545 + "delayTwice",
1546 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1547 + 1318,
1548 + 13,
1549 + 1317,
1550 + 5,
1551 + ],
1552 + [
1553 + "delayTrice",
1554 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1555 + 1323,
1556 + 15,
1557 + 1322,
1558 + 5,
1559 + ],
1560 + [
1561 + "Bar",
1562 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1563 + 1329,
1564 + 13,
1565 + 1328,
1566 + 5,
1567 + ],
1568 + ],
1569 + },
1570 + {
1571 + "time": 0,
1572 + },
1573 + {
1574 + "awaited": {
1575 + "end": 0,
1576 + "env": "Server",
1577 + "name": "delay",
1578 + "owner": {
1579 + "env": "Server",
1580 + "key": null,
1581 + "name": "Bar",
1582 + "props": {},
1583 + "stack": [
1584 + [
1585 + "Object.<anonymous>",
1586 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1587 + 1334,
1588 + 40,
1589 + 1316,
1590 + 80,
1591 + ],
1592 + ],
1593 + },
1594 + "stack": [
1595 + [
1596 + "delay",
1597 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1598 + 133,
1599 + 12,
1600 + 132,
1601 + 3,
1602 + ],
1603 + [
1604 + "delayTwice",
1605 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1606 + 1319,
1607 + 13,
1608 + 1317,
1609 + 5,
1610 + ],
1611 + ],
1612 + "start": 0,
1613 + },
1614 + "env": "Server",
1615 + "owner": {
1616 + "env": "Server",
1617 + "key": null,
1618 + "name": "Bar",
1619 + "props": {},
1620 + "stack": [
1621 + [
1622 + "Object.<anonymous>",
1623 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1624 + 1334,
1625 + 40,
1626 + 1316,
1627 + 80,
1628 + ],
1629 + ],
1630 + },
1631 + "stack": [
1632 + [
1633 + "delayTwice",
1634 + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1635 + 1319,
1636 + 13,
1637 + 1317,
1638 5,
1639 ],
1640 ],