mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Include Component Props in Performance Track (#33655)
Similar to how we can include a Promise resolved value we can include Component Props. For now I left out props for Client Components for perf unless they error. I'll try it for Client Components in general in a separate PR. <img width="730" alt="Screenshot 2025-06-26 at 5 54 29 PM" src="https://github.com/user-attachments/assets/f0c86911-2899-4b5f-b45f-5326bdbc630f" /> <img width="762" alt="Screenshot 2025-06-26 at 5 54 12 PM" src="https://github.com/user-attachments/assets/97540d19-5950-4346-99e6-066af086040e" />
This commit is contained in:
parent
4db4b21c63
commit
d2a288febf
|
|
@ -100,7 +100,7 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack'
|
|||
|
||||
import {injectInternals} from './ReactFlightClientDevToolsHook';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
|
||||
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ import type {
|
|||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {OMITTED_PROP_ERROR} from './ReactFlightPropertyAccess';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
import {
|
||||
addValueToProperties,
|
||||
addObjectToProperties,
|
||||
} from 'shared/ReactPerformanceTrackProperties';
|
||||
|
||||
const supportsUserTiming =
|
||||
enableProfilerTimer &&
|
||||
|
|
@ -33,127 +33,6 @@ const supportsUserTiming =
|
|||
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
|
||||
|
|
@ -222,17 +101,27 @@ export function logComponentRender(
|
|||
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
|
||||
const debugTask = componentInfo.debugTask;
|
||||
if (__DEV__ && debugTask) {
|
||||
const properties: Array<[string, string]> = [];
|
||||
if (componentInfo.key != null) {
|
||||
addValueToProperties('key', componentInfo.key, properties, 0);
|
||||
}
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0);
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
console.timeStamp.bind(
|
||||
console,
|
||||
entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
COMPONENTS_TRACK,
|
||||
color,
|
||||
),
|
||||
performance.measure.bind(performance, entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
devtools: {
|
||||
color: color,
|
||||
track: trackNames[trackIdx],
|
||||
trackGroup: COMPONENTS_TRACK,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
|
@ -268,6 +157,12 @@ export function logComponentAborted(
|
|||
'The stream was aborted before this Component finished rendering.',
|
||||
],
|
||||
];
|
||||
if (componentInfo.key != null) {
|
||||
addValueToProperties('key', componentInfo.key, properties, 0);
|
||||
}
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0);
|
||||
}
|
||||
performance.measure(entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
|
|
@ -319,6 +214,12 @@ export function logComponentErrored(
|
|||
: // eslint-disable-next-line react-internal/safe-string-coercion
|
||||
String(error);
|
||||
const properties = [['Error', message]];
|
||||
if (componentInfo.key != null) {
|
||||
addValueToProperties('key', componentInfo.key, properties, 0);
|
||||
}
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0);
|
||||
}
|
||||
performance.measure(entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ import {
|
|||
includesOnlyHydrationOrOffscreenLanes,
|
||||
} from './ReactFiberLane';
|
||||
|
||||
import {
|
||||
addValueToProperties,
|
||||
addObjectToProperties,
|
||||
} from 'shared/ReactPerformanceTrackProperties';
|
||||
|
||||
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
|
||||
|
||||
const supportsUserTiming =
|
||||
|
|
@ -239,7 +244,7 @@ export function logComponentErrored(
|
|||
typeof performance.measure === 'function'
|
||||
) {
|
||||
let debugTask: ?ConsoleTask = null;
|
||||
const properties = [];
|
||||
const properties: Array<[string, string]> = [];
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
const capturedValue = errors[i];
|
||||
if (debugTask == null && capturedValue.source !== null) {
|
||||
|
|
@ -261,6 +266,12 @@ export function logComponentErrored(
|
|||
String(error);
|
||||
properties.push(['Error', message]);
|
||||
}
|
||||
if (fiber.key !== null) {
|
||||
addValueToProperties('key', fiber.key, properties, 0);
|
||||
}
|
||||
if (fiber.memoizedProps !== null) {
|
||||
addObjectToProperties(fiber.memoizedProps, properties, 0);
|
||||
}
|
||||
if (debugTask == null) {
|
||||
// If the captured values don't have a debug task, fallback to the
|
||||
// error boundary itself.
|
||||
|
|
@ -320,7 +331,7 @@ function logComponentEffectErrored(
|
|||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
const properties = [];
|
||||
const properties: Array<[string, string]> = [];
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
const capturedValue = errors[i];
|
||||
const error = capturedValue.value;
|
||||
|
|
@ -334,6 +345,12 @@ function logComponentEffectErrored(
|
|||
String(error);
|
||||
properties.push(['Error', message]);
|
||||
}
|
||||
if (fiber.key !== null) {
|
||||
addValueToProperties('key', fiber.key, properties, 0);
|
||||
}
|
||||
if (fiber.memoizedProps !== null) {
|
||||
addObjectToProperties(fiber.memoizedProps, properties, 0);
|
||||
}
|
||||
const options = {
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
|
|
@ -804,7 +821,7 @@ export function logRecoveredRenderPhase(
|
|||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
const properties = [];
|
||||
const properties: Array<[string, string]> = [];
|
||||
for (let i = 0; i < recoverableErrors.length; i++) {
|
||||
const capturedValue = recoverableErrors[i];
|
||||
const error = capturedValue.value;
|
||||
|
|
@ -928,7 +945,7 @@ export function logCommitErrored(
|
|||
// $FlowFixMe[method-unbinding]
|
||||
typeof performance.measure === 'function'
|
||||
) {
|
||||
const properties = [];
|
||||
const properties: Array<[string, string]> = [];
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
const capturedValue = errors[i];
|
||||
const error = capturedValue.value;
|
||||
|
|
|
|||
188
packages/shared/ReactPerformanceTrackProperties.js
Normal file
188
packages/shared/ReactPerformanceTrackProperties.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* 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 {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
|
||||
|
||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import isArray from 'shared/isArray';
|
||||
import {REACT_ELEMENT_TYPE} from './ReactSymbols';
|
||||
import getComponentNameFromType from './getComponentNameFromType';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export 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 {
|
||||
if (value.$$typeof === REACT_ELEMENT_TYPE) {
|
||||
// JSX
|
||||
const typeName = getComponentNameFromType(value.type) || '\u2026';
|
||||
const key = value.key;
|
||||
const props: any = value.props;
|
||||
const propsKeys = Object.keys(props);
|
||||
const propsLength = propsKeys.length;
|
||||
if (key == null && propsLength === 0) {
|
||||
desc = '<' + typeName + ' />';
|
||||
break;
|
||||
}
|
||||
if (
|
||||
indent < 3 ||
|
||||
(propsLength === 1 && propsKeys[0] === 'children' && key == null)
|
||||
) {
|
||||
desc = '<' + typeName + ' \u2026 />';
|
||||
break;
|
||||
}
|
||||
properties.push([
|
||||
'\xa0\xa0'.repeat(indent) + propertyName,
|
||||
'<' + typeName,
|
||||
]);
|
||||
if (key !== null) {
|
||||
addValueToProperties('key', key, properties, indent + 1);
|
||||
}
|
||||
let hasChildren = false;
|
||||
for (const propKey in props) {
|
||||
if (propKey === 'children') {
|
||||
if (
|
||||
props.children != null &&
|
||||
(!isArray(props.children) || props.children.length > 0)
|
||||
) {
|
||||
hasChildren = true;
|
||||
}
|
||||
} else if (
|
||||
hasOwnProperty.call(props, propKey) &&
|
||||
propKey[0] !== '_'
|
||||
) {
|
||||
addValueToProperties(
|
||||
propKey,
|
||||
props[propKey],
|
||||
properties,
|
||||
indent + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
properties.push([
|
||||
'',
|
||||
hasChildren ? '>\u2026</' + typeName + '>' : '/>',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// $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' ? (indent < 3 ? '' : '\u2026') : 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 = '\u2026'; // ellipsis
|
||||
} 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]);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user