From bb28bb0f1658e984957d0de0a365ee6d9770bd14 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 17:04:19 -0400 Subject: [PATCH 1/3] Move performance track property serialization to shared --- .../react-client/src/ReactFlightClient.js | 2 +- .../src/ReactFlightPerformanceTrack.js | 129 +---------------- .../ReactFlightPropertyAccess.js | 0 .../shared/ReactPerformanceTrackProperties.js | 134 ++++++++++++++++++ 4 files changed, 139 insertions(+), 126 deletions(-) rename packages/{react-client/src => shared}/ReactFlightPropertyAccess.js (100%) create mode 100644 packages/shared/ReactPerformanceTrackProperties.js diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index bf767d5854768..3f0035337c073 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -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'; diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index d07a86970462f..c4736090a032a 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -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 = (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 diff --git a/packages/react-client/src/ReactFlightPropertyAccess.js b/packages/shared/ReactFlightPropertyAccess.js similarity index 100% rename from packages/react-client/src/ReactFlightPropertyAccess.js rename to packages/shared/ReactFlightPropertyAccess.js diff --git a/packages/shared/ReactPerformanceTrackProperties.js b/packages/shared/ReactPerformanceTrackProperties.js new file mode 100644 index 0000000000000..43d461c60c5ff --- /dev/null +++ b/packages/shared/ReactPerformanceTrackProperties.js @@ -0,0 +1,134 @@ +/** + * 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'; + +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 { + // $FlowFixMe[method-unbinding] + const objectToString = Object.prototype.toString.call(value); + let objectName = objectToString.slice(8, objectToString.length - 1); + if (objectName === 'Array') { + const array: Array = (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]); +} From ce03fbf54f72c8672a6891eabfcc5872f611f5d4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 17:55:22 -0400 Subject: [PATCH 2/3] Support JSX in Performance Track Properties --- .../shared/ReactPerformanceTrackProperties.js | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/shared/ReactPerformanceTrackProperties.js b/packages/shared/ReactPerformanceTrackProperties.js index 43d461c60c5ff..8b5f980856b8e 100644 --- a/packages/shared/ReactPerformanceTrackProperties.js +++ b/packages/shared/ReactPerformanceTrackProperties.js @@ -11,6 +11,8 @@ 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; @@ -73,6 +75,58 @@ export function addValueToProperties( 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' : '/>', + ]); + return; + } // $FlowFixMe[method-unbinding] const objectToString = Object.prototype.toString.call(value); let objectName = objectToString.slice(8, objectToString.length - 1); @@ -99,7 +153,7 @@ export function addValueToProperties( } properties.push([ '\xa0\xa0'.repeat(indent) + propertyName, - objectName === 'Object' ? '' : objectName, + objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName, ]); if (indent < 3) { addObjectToProperties(value, properties, indent + 1); @@ -115,7 +169,7 @@ export function addValueToProperties( break; case 'string': if (value === OMITTED_PROP_ERROR) { - desc = '...'; + desc = '\u2026'; // ellipsis } else { desc = JSON.stringify(value); } From c5495b652a5e6e1b8abe5deb44e4d7a0489dbb43 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 26 Jun 2025 17:55:38 -0400 Subject: [PATCH 3/3] Log component props in Performance Track --- .../src/ReactFlightPerformanceTrack.js | 40 ++++++++++++++----- .../src/ReactFiberPerformanceTrack.js | 25 ++++++++++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index c4736090a032a..7e48709b9f0dc 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -101,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( @@ -147,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, @@ -198,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, diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index f94c481c8f02a..48a403a8b63bd 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -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;