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