mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 12:20:38 +01:00
Stacked on #33588, #33589 and #33590. This lets us automatically show the resolved value in the UI. <img width="863" alt="Screenshot 2025-06-22 at 12 54 41 AM" src="https://github.com/user-attachments/assets/a66d1d5e-0513-4767-910c-5c7169fc2df4" /> We can also show rejected I/O that may or may not have been handled with the error message. <img width="838" alt="Screenshot 2025-06-22 at 12 55 06 AM" src="https://github.com/user-attachments/assets/e0a8b6ae-08ba-46d8-8cc5-efb60956a1d1" /> To get this working we need to keep the Promise around for longer so that we can access it once we want to emit an async sequence. I do this by storing the WeakRefs but to ensure that the Promise doesn't get garbage collected, I keep a WeakMap of Promise to the Promise that it depended on. This lets the VM still clean up any Promise chains that have leaves that are cleaned up. So this makes Promises live until the last Promise downstream is done. At that point we can go back up the chain to read the values out of them. Additionally, to get the best possible value we don't want to get a Promise that's used by internals of a third-party function. We want the value that the first party gets to observe. To do this I had to change the logic for which "await" to use, to be the one that is the first await that happened in user space. It's not enough that the await has any first party at all on the stack - it has to be the very first frame. This is a little sketchy because it relies on the `.then()` call or `await` call not having any third party wrappers. But it gives the best object since it hides all the internals. For example when you call `fetch()` we now log that actual `Response` object.
562 lines
15 KiB
JavaScript
562 lines
15 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
/* eslint-disable react-internal/no-production-logging */
|
|
|
|
import type {
|
|
ReactComponentInfo,
|
|
ReactIOInfo,
|
|
ReactAsyncInfo,
|
|
} from 'shared/ReactTypes';
|
|
|
|
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
|
|
|
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
|
|
|
import hasOwnProperty from 'shared/hasOwnProperty';
|
|
import isArray from 'shared/isArray';
|
|
|
|
const supportsUserTiming =
|
|
enableProfilerTimer &&
|
|
typeof console !== 'undefined' &&
|
|
typeof console.timeStamp === 'function' &&
|
|
typeof performance !== 'undefined' &&
|
|
// $FlowFixMe[method-unbinding]
|
|
typeof performance.measure === 'function';
|
|
|
|
const IO_TRACK = 'Server Requests ⚛';
|
|
const COMPONENTS_TRACK = 'Server Components ⚛';
|
|
|
|
const EMPTY_ARRAY = 0;
|
|
const COMPLEX_ARRAY = 1;
|
|
const PRIMITIVE_ARRAY = 2; // Primitive values only
|
|
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
|
|
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
|
|
let kind = EMPTY_ARRAY;
|
|
for (let i = 0; i < array.length; i++) {
|
|
const value = array[i];
|
|
if (typeof value === 'object' && value !== null) {
|
|
if (
|
|
isArray(value) &&
|
|
value.length === 2 &&
|
|
typeof value[0] === 'string'
|
|
) {
|
|
// Key value tuple
|
|
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
|
|
return COMPLEX_ARRAY;
|
|
}
|
|
kind = ENTRIES_ARRAY;
|
|
} else {
|
|
return COMPLEX_ARRAY;
|
|
}
|
|
} else if (typeof value === 'function') {
|
|
return COMPLEX_ARRAY;
|
|
} else if (typeof value === 'string' && value.length > 50) {
|
|
return COMPLEX_ARRAY;
|
|
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
|
|
return COMPLEX_ARRAY;
|
|
} else {
|
|
kind = PRIMITIVE_ARRAY;
|
|
}
|
|
}
|
|
return kind;
|
|
}
|
|
|
|
function addObjectToProperties(
|
|
object: Object,
|
|
properties: Array<[string, string]>,
|
|
indent: number,
|
|
): void {
|
|
for (const key in object) {
|
|
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
|
|
const value = object[key];
|
|
addValueToProperties(key, value, properties, indent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function addValueToProperties(
|
|
propertyName: string,
|
|
value: mixed,
|
|
properties: Array<[string, string]>,
|
|
indent: number,
|
|
): void {
|
|
let desc;
|
|
switch (typeof value) {
|
|
case 'object':
|
|
if (value === null) {
|
|
desc = 'null';
|
|
break;
|
|
} else {
|
|
// $FlowFixMe[method-unbinding]
|
|
const objectToString = Object.prototype.toString.call(value);
|
|
let objectName = objectToString.slice(8, objectToString.length - 1);
|
|
if (objectName === 'Array') {
|
|
const array: Array<any> = (value: any);
|
|
const kind = getArrayKind(array);
|
|
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
|
|
desc = JSON.stringify(array);
|
|
break;
|
|
} else if (kind === ENTRIES_ARRAY) {
|
|
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, '']);
|
|
for (let i = 0; i < array.length; i++) {
|
|
const entry = array[i];
|
|
addValueToProperties(entry[0], entry[1], properties, indent + 1);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (objectName === 'Object') {
|
|
const proto: any = Object.getPrototypeOf(value);
|
|
if (proto && typeof proto.constructor === 'function') {
|
|
objectName = proto.constructor.name;
|
|
}
|
|
}
|
|
properties.push([
|
|
'\xa0\xa0'.repeat(indent) + propertyName,
|
|
objectName === 'Object' ? '' : objectName,
|
|
]);
|
|
if (indent < 3) {
|
|
addObjectToProperties(value, properties, indent + 1);
|
|
}
|
|
return;
|
|
}
|
|
case 'function':
|
|
if (value.name === '') {
|
|
desc = '() => {}';
|
|
} else {
|
|
desc = value.name + '() {}';
|
|
}
|
|
break;
|
|
case 'string':
|
|
if (value === OMITTED_PROP_ERROR) {
|
|
desc = '...';
|
|
} else {
|
|
desc = JSON.stringify(value);
|
|
}
|
|
break;
|
|
case 'undefined':
|
|
desc = 'undefined';
|
|
break;
|
|
case 'boolean':
|
|
desc = value ? 'true' : 'false';
|
|
break;
|
|
default:
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
desc = String(value);
|
|
}
|
|
properties.push(['\xa0\xa0'.repeat(indent) + propertyName, desc]);
|
|
}
|
|
|
|
export function markAllTracksInOrder() {
|
|
if (supportsUserTiming) {
|
|
// Ensure we create the Server Component track groups earlier than the Client Scheduler
|
|
// and Client Components. We can always add the 0 time slot even if it's in the past.
|
|
// That's still considered for ordering.
|
|
console.timeStamp(
|
|
'Server Requests Track',
|
|
0.001,
|
|
0.001,
|
|
IO_TRACK,
|
|
undefined,
|
|
'primary-light',
|
|
);
|
|
console.timeStamp(
|
|
'Server Components Track',
|
|
0.001,
|
|
0.001,
|
|
'Primary',
|
|
COMPONENTS_TRACK,
|
|
'primary-light',
|
|
);
|
|
}
|
|
}
|
|
|
|
const trackNames = [
|
|
'Primary',
|
|
'Parallel',
|
|
'Parallel\u200b', // Padded with zero-width space to give each track a unique name.
|
|
'Parallel\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
|
|
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
|
|
];
|
|
|
|
export function logComponentRender(
|
|
componentInfo: ReactComponentInfo,
|
|
trackIdx: number,
|
|
startTime: number,
|
|
endTime: number,
|
|
childrenEndTime: number,
|
|
rootEnv: string,
|
|
): void {
|
|
if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
|
|
const env = componentInfo.env;
|
|
const name = componentInfo.name;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const selfTime = endTime - startTime;
|
|
const color =
|
|
selfTime < 0.5
|
|
? isPrimaryEnv
|
|
? 'primary-light'
|
|
: 'secondary-light'
|
|
: selfTime < 50
|
|
? isPrimaryEnv
|
|
? 'primary'
|
|
: 'secondary'
|
|
: selfTime < 500
|
|
? isPrimaryEnv
|
|
? 'primary-dark'
|
|
: 'secondary-dark'
|
|
: 'error';
|
|
const entryName =
|
|
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
|
const debugTask = componentInfo.debugTask;
|
|
if (__DEV__ && debugTask) {
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
console.timeStamp.bind(
|
|
console,
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
childrenEndTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
color,
|
|
),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
childrenEndTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
color,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function logComponentErrored(
|
|
componentInfo: ReactComponentInfo,
|
|
trackIdx: number,
|
|
startTime: number,
|
|
endTime: number,
|
|
childrenEndTime: number,
|
|
rootEnv: string,
|
|
error: mixed,
|
|
): void {
|
|
if (supportsUserTiming) {
|
|
const env = componentInfo.env;
|
|
const name = componentInfo.name;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const entryName =
|
|
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
|
if (__DEV__) {
|
|
const message =
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
typeof error.message === 'string'
|
|
? // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error.message)
|
|
: // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error);
|
|
const properties = [['Error', message]];
|
|
performance.measure(entryName, {
|
|
start: startTime < 0 ? 0 : startTime,
|
|
end: childrenEndTime,
|
|
detail: {
|
|
devtools: {
|
|
color: 'error',
|
|
track: trackNames[trackIdx],
|
|
trackGroup: COMPONENTS_TRACK,
|
|
tooltipText: entryName + ' Errored',
|
|
properties,
|
|
},
|
|
},
|
|
});
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
childrenEndTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
'error',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function logDedupedComponentRender(
|
|
componentInfo: ReactComponentInfo,
|
|
trackIdx: number,
|
|
startTime: number,
|
|
endTime: number,
|
|
rootEnv: string,
|
|
): void {
|
|
if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
|
|
const env = componentInfo.env;
|
|
const name = componentInfo.name;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const color = isPrimaryEnv ? 'primary-light' : 'secondary-light';
|
|
const entryName = name + ' [deduped]';
|
|
const debugTask = componentInfo.debugTask;
|
|
if (__DEV__ && debugTask) {
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
console.timeStamp.bind(
|
|
console,
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
color,
|
|
),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
color,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getIOColor(
|
|
functionName: string,
|
|
): 'tertiary-light' | 'tertiary' | 'tertiary-dark' {
|
|
// Add some color variation to be able to distinguish various sources.
|
|
switch (functionName.charCodeAt(0) % 3) {
|
|
case 0:
|
|
return 'tertiary-light';
|
|
case 1:
|
|
return 'tertiary';
|
|
default:
|
|
return 'tertiary-dark';
|
|
}
|
|
}
|
|
|
|
export function logComponentAwaitErrored(
|
|
asyncInfo: ReactAsyncInfo,
|
|
trackIdx: number,
|
|
startTime: number,
|
|
endTime: number,
|
|
rootEnv: string,
|
|
error: mixed,
|
|
): void {
|
|
if (supportsUserTiming && endTime > 0) {
|
|
const env = asyncInfo.env;
|
|
const name = asyncInfo.awaited.name;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const entryName =
|
|
'await ' +
|
|
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
|
const debugTask = asyncInfo.debugTask;
|
|
if (__DEV__ && debugTask) {
|
|
const message =
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
typeof error.message === 'string'
|
|
? // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error.message)
|
|
: // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error);
|
|
const properties = [['Rejected', message]];
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
performance.measure.bind(performance, entryName, {
|
|
start: startTime < 0 ? 0 : startTime,
|
|
end: endTime,
|
|
detail: {
|
|
devtools: {
|
|
color: 'error',
|
|
track: trackNames[trackIdx],
|
|
trackGroup: COMPONENTS_TRACK,
|
|
properties,
|
|
tooltipText: entryName + ' Rejected',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
'error',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function logComponentAwait(
|
|
asyncInfo: ReactAsyncInfo,
|
|
trackIdx: number,
|
|
startTime: number,
|
|
endTime: number,
|
|
rootEnv: string,
|
|
value: mixed,
|
|
): void {
|
|
if (supportsUserTiming && endTime > 0) {
|
|
const env = asyncInfo.env;
|
|
const name = asyncInfo.awaited.name;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const color = getIOColor(name);
|
|
const entryName =
|
|
'await ' +
|
|
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
|
|
const debugTask = asyncInfo.debugTask;
|
|
if (__DEV__ && debugTask) {
|
|
const properties: Array<[string, string]> = [];
|
|
if (typeof value === 'object' && value !== null) {
|
|
addObjectToProperties(value, properties, 0);
|
|
} else if (value !== undefined) {
|
|
addValueToProperties('Resolved', value, properties, 0);
|
|
}
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
performance.measure.bind(performance, entryName, {
|
|
start: startTime < 0 ? 0 : startTime,
|
|
end: endTime,
|
|
detail: {
|
|
devtools: {
|
|
color: color,
|
|
track: trackNames[trackIdx],
|
|
trackGroup: COMPONENTS_TRACK,
|
|
properties,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
trackNames[trackIdx],
|
|
COMPONENTS_TRACK,
|
|
color,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function logIOInfoErrored(
|
|
ioInfo: ReactIOInfo,
|
|
rootEnv: string,
|
|
error: mixed,
|
|
): void {
|
|
const startTime = ioInfo.start;
|
|
const endTime = ioInfo.end;
|
|
if (supportsUserTiming && endTime >= 0) {
|
|
const name = ioInfo.name;
|
|
const env = ioInfo.env;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const entryName =
|
|
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
|
const debugTask = ioInfo.debugTask;
|
|
if (__DEV__ && debugTask) {
|
|
const message =
|
|
typeof error === 'object' &&
|
|
error !== null &&
|
|
typeof error.message === 'string'
|
|
? // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error.message)
|
|
: // eslint-disable-next-line react-internal/safe-string-coercion
|
|
String(error);
|
|
const properties = [['Rejected', message]];
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
performance.measure.bind(performance, entryName, {
|
|
start: startTime < 0 ? 0 : startTime,
|
|
end: endTime,
|
|
detail: {
|
|
devtools: {
|
|
color: 'error',
|
|
track: IO_TRACK,
|
|
properties,
|
|
tooltipText: entryName + ' Rejected',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
IO_TRACK,
|
|
undefined,
|
|
'error',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function logIOInfo(
|
|
ioInfo: ReactIOInfo,
|
|
rootEnv: string,
|
|
value: mixed,
|
|
): void {
|
|
const startTime = ioInfo.start;
|
|
const endTime = ioInfo.end;
|
|
if (supportsUserTiming && endTime >= 0) {
|
|
const name = ioInfo.name;
|
|
const env = ioInfo.env;
|
|
const isPrimaryEnv = env === rootEnv;
|
|
const entryName =
|
|
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
|
const debugTask = ioInfo.debugTask;
|
|
const color = getIOColor(name);
|
|
if (__DEV__ && debugTask) {
|
|
const properties: Array<[string, string]> = [];
|
|
if (typeof value === 'object' && value !== null) {
|
|
addObjectToProperties(value, properties, 0);
|
|
} else if (value !== undefined) {
|
|
addValueToProperties('Resolved', value, properties, 0);
|
|
}
|
|
debugTask.run(
|
|
// $FlowFixMe[method-unbinding]
|
|
performance.measure.bind(performance, entryName, {
|
|
start: startTime < 0 ? 0 : startTime,
|
|
end: endTime,
|
|
detail: {
|
|
devtools: {
|
|
color: color,
|
|
track: IO_TRACK,
|
|
properties,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
} else {
|
|
console.timeStamp(
|
|
entryName,
|
|
startTime < 0 ? 0 : startTime,
|
|
endTime,
|
|
IO_TRACK,
|
|
undefined,
|
|
color,
|
|
);
|
|
}
|
|
}
|
|
}
|