main
js 388 lines 15.9 KB
Raw
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 import type {ReactStackTrace} from 'shared/ReactTypes';
11
12 import type {
13 AsyncSequence,
14 IONode,
15 PromiseNode,
16 UnresolvedPromiseNode,
17 AwaitNode,
18 UnresolvedAwaitNode,
19 } from './ReactFlightAsyncSequence';
20
21 import {
22 IO_NODE,
23 PROMISE_NODE,
24 UNRESOLVED_PROMISE_NODE,
25 AWAIT_NODE,
26 UNRESOLVED_AWAIT_NODE,
27 } from './ReactFlightAsyncSequence';
28 import {resolveOwner} from './flight/ReactFlightCurrentOwner';
29 import {resolveRequest, isAwaitInUserspace} from './ReactFlightServer';
30 import {createHook, executionAsyncId, AsyncResource} from 'async_hooks';
31 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
32 import {parseStackTracePrivate} from './ReactFlightServerConfig';
33
34 // $FlowFixMe[method-unbinding]
35 const getAsyncId = AsyncResource.prototype.asyncId;
36
37 const pendingOperations: Map<number, AsyncSequence> =
38 __DEV__ && enableAsyncDebugInfo ? new Map() : (null as any);
39
40 // Keep the last resolved await as a workaround for async functions missing data.
41 let lastRanAwait: null | AwaitNode = null;
42
43 function resolvePromiseOrAwaitNode(
44 unresolvedNode: UnresolvedAwaitNode | UnresolvedPromiseNode,
45 endTime: number,
46 ): AwaitNode | PromiseNode {
47 const resolvedNode: AwaitNode | PromiseNode = unresolvedNode as any;
48 resolvedNode.tag = (
49 unresolvedNode.tag === UNRESOLVED_PROMISE_NODE ? PROMISE_NODE : AWAIT_NODE
50 ) as any;
51 resolvedNode.end = endTime;
52 return resolvedNode;
53 }
54
55 const emptyStack: ReactStackTrace = [];
56
57 // Initialize the tracing of async operations.
58 // We do this globally since the async work can potentially eagerly
59 // start before the first request and once requests start they can interleave.
60 // In theory we could enable and disable using a ref count of active requests
61 // but given that typically this is just a live server, it doesn't really matter.
62 export function initAsyncDebugInfo(): void {
63 if (__DEV__ && enableAsyncDebugInfo) {
64 createHook({
65 init(
66 asyncId: number,
67 type: string,
68 triggerAsyncId: number,
69 resource: any,
70 ): void {
71 const trigger = pendingOperations.get(triggerAsyncId);
72 let node: AsyncSequence;
73 if (type === 'PROMISE') {
74 const currentAsyncId = executionAsyncId();
75 if (currentAsyncId !== triggerAsyncId) {
76 // When you call .then() on a native Promise, or await/Promise.all() a thenable,
77 // then this intermediate Promise is created. We use this as our await point
78 if (trigger === undefined) {
79 // We don't track awaits on things that started outside our tracked scope.
80 return;
81 }
82 // If the thing we're waiting on is another Await we still track that sequence
83 // so that we can later pick the best stack trace in user space.
84 let stack = null;
85 let promiseRef: WeakRef<Promise<any>>;
86 if (
87 trigger.stack !== null &&
88 (trigger.tag === AWAIT_NODE ||
89 trigger.tag === UNRESOLVED_AWAIT_NODE)
90 ) {
91 // We already had a stack for an await. In a chain of awaits we'll only need one good stack.
92 // We mark it with an empty stack to signal to any await on this await that we have a stack.
93 stack = emptyStack;
94 if (resource._debugInfo !== undefined) {
95 // We may need to forward this debug info at the end so we need to retain this promise.
96 promiseRef = new WeakRef(resource as Promise<any>);
97 } else {
98 // Otherwise, we can just refer to the inner one since that's the one we'll log anyway.
99 promiseRef = trigger.promise;
100 }
101 } else {
102 promiseRef = new WeakRef(resource as Promise<any>);
103 const request = resolveRequest();
104 if (request === null) {
105 // We don't collect stacks for awaits that weren't in the scope of a specific render.
106 } else {
107 stack = parseStackTracePrivate(new Error(), 5);
108 if (stack !== null && !isAwaitInUserspace(request, stack)) {
109 // If this await was not done directly in user space, then clear the stack. We won't use it
110 // anyway. This lets future awaits on this await know that we still need to get their stacks
111 // until we find one in user space.
112 stack = null;
113 }
114 }
115 }
116 const current = pendingOperations.get(currentAsyncId);
117 node = {
118 tag: UNRESOLVED_AWAIT_NODE,
119 owner: resolveOwner(),
120 stack: stack,
121 start: performance.now(),
122 end: -1.1, // set when resolved.
123 promise: promiseRef,
124 awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
125 previous: current === undefined ? null : current, // The path that led us here.
126 } as UnresolvedAwaitNode;
127 } else {
128 const owner = resolveOwner();
129 node = {
130 tag: UNRESOLVED_PROMISE_NODE,
131 owner: owner,
132 stack:
133 owner === null ? null : parseStackTracePrivate(new Error(), 5),
134 start: performance.now(),
135 end: -1.1, // Set when we resolve.
136 promise: new WeakRef(resource as Promise<any>),
137 awaited:
138 trigger === undefined
139 ? null // It might get overridden when we resolve.
140 : trigger,
141 previous: null,
142 } as UnresolvedPromiseNode;
143 }
144 } else if (
145 // bound-anonymous-fn is the default name for snapshots and .bind() without a name.
146 // This isn't I/O by itself but likely just a continuation. If the bound function
147 // has a name, we might treat it as I/O but we can't tell the difference.
148 type === 'bound-anonymous-fn' ||
149 // queueMicroTask, process.nextTick and setImmediate aren't considered new I/O
150 // for our purposes but just continuation of existing I/O.
151 type === 'Microtask' ||
152 type === 'TickObject' ||
153 type === 'Immediate'
154 ) {
155 // Treat the trigger as the node to carry along the sequence.
156 // For "bound-anonymous-fn" this will be the callsite of the .bind() which may not
157 // be the best if the callsite of the .run() call is within I/O which should be
158 // tracked. It might be better to track the execution context of "before()" as the
159 // execution context for anything spawned from within the run(). Basically as if
160 // it wasn't an AsyncResource at all.
161 if (trigger === undefined) {
162 return;
163 }
164 node = trigger;
165 } else {
166 // New I/O
167 if (trigger === undefined) {
168 // We have begun a new I/O sequence.
169 const owner = resolveOwner();
170 node = {
171 tag: IO_NODE,
172 owner: owner,
173 stack:
174 owner === null ? parseStackTracePrivate(new Error(), 3) : null,
175 start: performance.now(),
176 end: -1.1, // Only set when pinged.
177 promise: null,
178 awaited: null,
179 previous: null,
180 } as IONode;
181 } else if (
182 trigger.tag === AWAIT_NODE ||
183 trigger.tag === UNRESOLVED_AWAIT_NODE
184 ) {
185 // We have begun a new I/O sequence after the await.
186 const owner = resolveOwner();
187 node = {
188 tag: IO_NODE,
189 owner: owner,
190 stack:
191 owner === null ? parseStackTracePrivate(new Error(), 3) : null,
192 start: performance.now(),
193 end: -1.1, // Only set when pinged.
194 promise: null,
195 awaited: null,
196 previous: trigger,
197 } as IONode;
198 } else {
199 // Otherwise, this is just a continuation of the same I/O sequence.
200 node = trigger;
201 }
202 }
203 pendingOperations.set(asyncId, node);
204 },
205 before(asyncId: number): void {
206 const node = pendingOperations.get(asyncId);
207 if (node !== undefined) {
208 switch (node.tag) {
209 case IO_NODE: {
210 lastRanAwait = null;
211 // Log the end time when we resolved the I/O.
212 const ioNode: IONode = node as any;
213 if (ioNode.end < 0) {
214 ioNode.end = performance.now();
215 } else {
216 // This can happen more than once if it's a recurring resource like a connection.
217 // Even for single events like setTimeout, this can happen three times due to ticks
218 // and microtasks each running its own scope.
219 // To preserve each operation's separate end time, we create a clone of the IO node.
220 // Any pre-existing reference will refer to the first resolution and any new resolutions
221 // will refer to the new node.
222 const clonedNode: IONode = {
223 tag: IO_NODE,
224 owner: ioNode.owner,
225 stack: ioNode.stack,
226 start: ioNode.start,
227 end: performance.now(),
228 promise: ioNode.promise,
229 awaited: ioNode.awaited,
230 previous: ioNode.previous,
231 };
232 pendingOperations.set(asyncId, clonedNode);
233 }
234 break;
235 }
236 case UNRESOLVED_AWAIT_NODE: {
237 // If we begin before we resolve, that means that this is actually already resolved but
238 // the promiseResolve hook is called at the end of the execution. So we track the time
239 // in the before call instead.
240 // $FlowFixMe[incompatible-type]
241 lastRanAwait = resolvePromiseOrAwaitNode(node, performance.now());
242 break;
243 }
244 case AWAIT_NODE: {
245 lastRanAwait = node;
246 break;
247 }
248 case UNRESOLVED_PROMISE_NODE: {
249 // We typically don't expected Promises to have an execution scope since only the awaits
250 // have a then() callback. However, this can happen for native async functions. The last
251 // piece of code that executes the return after the last await has the execution context
252 // of the Promise.
253 const resolvedNode = resolvePromiseOrAwaitNode(
254 node,
255 performance.now(),
256 );
257 // We are missing information about what this was unblocked by but we can guess that it
258 // was whatever await we ran last since this will continue in a microtask after that.
259 // This is not perfect because there could potentially be other microtasks getting in
260 // between.
261 resolvedNode.previous = lastRanAwait;
262 lastRanAwait = null;
263 break;
264 }
265 default: {
266 lastRanAwait = null;
267 }
268 }
269 }
270 },
271
272 promiseResolve(asyncId: number): void {
273 const node = pendingOperations.get(asyncId);
274 if (node !== undefined) {
275 let resolvedNode: AwaitNode | PromiseNode;
276 switch (node.tag) {
277 case UNRESOLVED_AWAIT_NODE:
278 case UNRESOLVED_PROMISE_NODE: {
279 resolvedNode = resolvePromiseOrAwaitNode(node, performance.now());
280 break;
281 }
282 case AWAIT_NODE:
283 case PROMISE_NODE: {
284 // We already resolved this in the before hook.
285 resolvedNode = node;
286 break;
287 }
288 default:
289 // eslint-disable-next-line react-internal/prod-error-codes
290 throw new Error(
291 'A Promise should never be an IO_NODE. This is a bug in React.',
292 );
293 }
294 const currentAsyncId = executionAsyncId();
295 if (asyncId !== currentAsyncId) {
296 // If the promise was not resolved by itself, then that means that
297 // the trigger that we originally stored wasn't actually the dependency.
298 // Instead, the current execution context is what ultimately unblocked it.
299 const awaited = pendingOperations.get(currentAsyncId);
300 if (resolvedNode.tag === PROMISE_NODE) {
301 // For a Promise we just override the await. We're not interested in
302 // what created the Promise itself.
303 resolvedNode.awaited = awaited === undefined ? null : awaited;
304 } else {
305 // For an await, there's really two things awaited here. It's the trigger
306 // that .then() was called on but there seems to also be something else
307 // in the .then() callback that blocked the returned Promise from resolving
308 // immediately. We create a fork node which essentially represents an await
309 // of the Promise returned from the .then() callback. That Promise was blocked
310 // on the original awaited thing which we stored as "previous".
311 if (awaited !== undefined) {
312 const clonedNode: AwaitNode = {
313 tag: AWAIT_NODE,
314 owner: resolvedNode.owner,
315 stack: resolvedNode.stack,
316 start: resolvedNode.start,
317 end: resolvedNode.end,
318 promise: resolvedNode.promise,
319 awaited: resolvedNode.awaited,
320 previous: resolvedNode.previous,
321 };
322 // We started awaiting on the callback when the original .then() resolved.
323 resolvedNode.start = resolvedNode.end;
324 // It resolved now. We could use the end time of "awaited" maybe.
325 resolvedNode.end = performance.now();
326 resolvedNode.previous = clonedNode;
327 resolvedNode.awaited = awaited;
328 }
329 }
330 }
331 }
332 },
333
334 destroy(asyncId: number): void {
335 // If we needed the meta data from this operation we should have already
336 // extracted it or it should be part of a chain of triggers.
337 pendingOperations.delete(asyncId);
338 },
339 }).enable();
340 }
341 }
342
343 export function markAsyncSequenceRootTask(): void {
344 if (__DEV__ && enableAsyncDebugInfo) {
345 // Whatever Task we're running now is spawned by React itself to perform render work.
346 // Don't track any cause beyond this task. We may still track I/O that was started outside
347 // React but just not the cause of entering the render.
348 pendingOperations.delete(executionAsyncId());
349 }
350 }
351
352 export function getCurrentAsyncSequence(): null | AsyncSequence {
353 if (!__DEV__ || !enableAsyncDebugInfo) {
354 return null;
355 }
356 const currentNode = pendingOperations.get(executionAsyncId());
357 if (currentNode === undefined) {
358 // Nothing that we tracked led to the resolution of this execution context.
359 return null;
360 }
361 return currentNode;
362 }
363
364 export function getAsyncSequenceFromPromise(
365 promise: any,
366 ): null | AsyncSequence {
367 if (!__DEV__ || !enableAsyncDebugInfo) {
368 return null;
369 }
370 // A Promise is conceptually an AsyncResource but doesn't have its own methods.
371 // We use this hack to extract the internal asyncId off the Promise.
372 let asyncId: void | number;
373 try {
374 asyncId = getAsyncId.call(promise);
375 } catch (x) {
376 // Ignore errors extracting the ID. We treat it as missing.
377 // This could happen if our hack stops working or in the case where this is
378 // a Proxy that throws such as our own ClientReference proxies.
379 }
380 if (asyncId === undefined) {
381 return null;
382 }
383 const node = pendingOperations.get(asyncId);
384 if (node === undefined) {
385 return null;
386 }
387 return node;
388 }