19
enableTaint,
20
enableProfilerTimer,
21
enableComponentPerformanceTrack,
22
+ enableAsyncDebugInfo,
23
} from 'shared/ReactFeatureFlags';
24
25
import {
60
ReactDebugInfo,
61
ReactComponentInfo,
62
ReactEnvironmentInfo,
63
+ ReactIOInfo,
64
ReactAsyncInfo,
65
ReactTimeInfo,
66
ReactStackTrace,
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,
88
requestStorage,
89
createHints,
90
initAsyncDebugInfo,
91
+ getCurrentAsyncSequence,
92
parseStackTrace,
93
supportsComponentStorage,
94
componentStorage,
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
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
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
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(
565
null,
566
false,
567
abortSet,
568
+ timeOrigin,
569
null,
570
null,
571
null,
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,
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,
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,
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);
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,
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,
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);
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
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();
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.
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.
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,
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,
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
}
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;
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
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