Skip to content

[pull] main from facebook:main #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 2 additions & 64 deletions packages/react-client/src/ReactFlightPerformanceTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
addObjectToProperties,
} from 'shared/ReactPerformanceTrackProperties';

import {getIODescription} from 'shared/ReactIODescription';

const supportsUserTiming =
enableProfilerTimer &&
typeof console !== 'undefined' &&
Expand Down Expand Up @@ -300,70 +302,6 @@ function getIOColor(
}
}

function getIODescription(value: any): string {
if (!__DEV__) {
return '';
}
try {
switch (typeof value) {
case 'object':
// Test the object for a bunch of common property names that are useful identifiers.
// While we only have the return value here, it should ideally be a name that
// describes the arguments requested.
if (value === null) {
return '';
} else if (value instanceof Error) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
typeof value.request === 'object' &&
typeof value.request.url === 'string'
) {
return value.request.url;
} else if (
typeof value.response === 'object' &&
typeof value.response.url === 'string'
) {
return value.response.url;
} else if (
typeof value.id === 'string' ||
typeof value.id === 'number' ||
typeof value.id === 'bigint'
) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.id);
} else if (typeof value.name === 'string') {
return value.name;
} else {
const str = value.toString();
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
// This is probably not a useful description.
return '';
}
return str;
}
case 'string':
if (value.length < 5 || value.length > 500) {
return '';
}
return value;
case 'number':
case 'bigint':
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
default:
// Not useful descriptors.
return '';
}
} catch (x) {
return '';
}
}

function getIOLongName(
ioInfo: ReactIOInfo,
description: string,
Expand Down
19 changes: 19 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';

import {getIODescription} from 'shared/ReactIODescription';

import {
getStackByFiberInDevAndProd,
getOwnerStackByFiberInDev,
Expand Down Expand Up @@ -4116,9 +4118,26 @@ export function attach(
parentInstance,
asyncInfo.owner,
);
const value: any = ioInfo.value;
let resolvedValue = undefined;
if (
typeof value === 'object' &&
value !== null &&
typeof value.then === 'function'
) {
switch (value.status) {
case 'fulfilled':
resolvedValue = value.value;
break;
case 'rejected':
resolvedValue = value.reason;
break;
}
}
return {
awaited: {
name: ioInfo.name,
description: getIODescription(resolvedValue),
start: ioInfo.start,
end: ioInfo.end,
value: ioInfo.value == null ? null : ioInfo.value,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export type PathMatch = {
// Serialized version of ReactIOInfo
export type SerializedIOInfo = {
name: string,
description: string,
start: number,
end: number,
value: null | Promise<mixed>,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/backendAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ function backendToFrontendSerializedAsyncInfo(
return {
awaited: {
name: ioInfo.name,
description: ioInfo.description,
start: ioInfo.start,
end: ioInfo.end,
value: ioInfo.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,25 @@
color: var(--color-expand-collapse-toggle);
}

.CollapsableHeaderTitle {
flex: 1 1 auto;
.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
text-align: left;
white-space: nowrap;
}
.CollapsableHeaderTitle {
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}

.CollapsableHeaderSeparator {
flex: 0 0 auto;
white-space: pre;
}

.CollapsableHeaderFiller {
flex: 1 0 0;
}

.CollapsableContent {
Expand Down Expand Up @@ -108,4 +122,4 @@

.TimeBarSpanErrored {
background-color: var(--color-timespan-background-errored);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,37 @@ type RowProps = {
maxTime: number,
};

function getShortDescription(name: string, description: string): string {
const descMaxLength = 30 - name.length;
if (descMaxLength > 1) {
const l = description.length;
if (l > 0 && l <= descMaxLength) {
// We can fit the full description
return description;
} else if (
description.startsWith('http://') ||
description.startsWith('https://') ||
description.startsWith('/')
) {
// Looks like a URL. Let's see if we can extract something shorter.
// We don't have to do a full parse so let's try something cheaper.
let queryIdx = description.indexOf('?');
if (queryIdx === -1) {
queryIdx = description.length;
}
if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) {
// Ends with slash. Look before that.
queryIdx--;
}
const slashIdx = description.lastIndexOf('/', queryIdx - 1);
// This may now be either the file name or the host.
// Include the slash to make it more obvious what we trimmed.
return '…' + description.slice(slashIdx, queryIdx);
}
}
return '';
}

function SuspendedByRow({
bridge,
element,
Expand All @@ -50,6 +81,9 @@ function SuspendedByRow({
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const name = asyncInfo.awaited.name;
const description = asyncInfo.awaited.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
let stack;
let owner;
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
Expand Down Expand Up @@ -83,12 +117,22 @@ function SuspendedByRow({
<Button
className={styles.CollapsableHeader}
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
title={name + ' — ' + (end - start).toFixed(2) + ' ms'}>
title={longName + ' — ' + (end - start).toFixed(2) + ' ms'}>
<ButtonIcon
className={styles.CollapsableHeaderIcon}
type={isOpen ? 'expanded' : 'collapsed'}
/>
<span className={styles.CollapsableHeaderTitle}>{name}</span>
{shortDescription === '' ? null : (
<>
<span className={styles.CollapsableHeaderSeparator}>{' ('}</span>
<span className={styles.CollapsableHeaderTitle}>
{shortDescription}
</span>
<span className={styles.CollapsableHeaderSeparator}>{') '}</span>
</>
)}
<div className={styles.CollapsableHeaderFiller} />
<div className={styles.TimeBarContainer}>
<div
className={
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/frontend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export type Element = {
// Serialized version of ReactIOInfo
export type SerializedIOInfo = {
name: string,
description: string,
start: number,
end: number,
value: null | Promise<mixed>,
Expand Down
72 changes: 72 additions & 0 deletions packages/shared/ReactIODescription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* 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
*/

export function getIODescription(value: any): string {
if (!__DEV__) {
return '';
}
try {
switch (typeof value) {
case 'object':
// Test the object for a bunch of common property names that are useful identifiers.
// While we only have the return value here, it should ideally be a name that
// describes the arguments requested.
if (value === null) {
return '';
} else if (value instanceof Error) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
typeof value.request === 'object' &&
typeof value.request.url === 'string'
) {
return value.request.url;
} else if (
typeof value.response === 'object' &&
typeof value.response.url === 'string'
) {
return value.response.url;
} else if (
typeof value.id === 'string' ||
typeof value.id === 'number' ||
typeof value.id === 'bigint'
) {
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value.id);
} else if (typeof value.name === 'string') {
return value.name;
} else {
const str = value.toString();
if (str.startWith('[object ') || str.length < 5 || str.length > 500) {
// This is probably not a useful description.
return '';
}
return str;
}
case 'string':
if (value.length < 5 || value.length > 500) {
return '';
}
return value;
case 'number':
case 'bigint':
// eslint-disable-next-line react-internal/safe-string-coercion
return String(value);
default:
// Not useful descriptors.
return '';
}
} catch (x) {
return '';
}
}
Loading