[Fiber] Bail out of diffing wide objects and arrays (#34742)

This commit is contained in:
Sebastian "Sebbie" Silbermann 2025-10-06 01:13:22 +02:00 committed by GitHub
parent 3b2a398106
commit 1be3ce9996
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 395 additions and 3 deletions

View File

@ -0,0 +1,327 @@
/**
* 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.
*
* @emails react-core
* @jest-environment node
*/
let React;
let ReactNoop;
let Scheduler;
let act;
let useEffect;
describe('ReactPerformanceTracks', () => {
beforeEach(() => {
Object.defineProperty(performance, 'measure', {
value: jest.fn(),
configurable: true,
});
console.timeStamp = () => {};
jest.spyOn(console, 'timeStamp').mockImplementation(() => {});
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
useEffect = React.useEffect;
});
// @gate __DEV__ && enableComponentPerformanceTrack
it('shows a hint if an update is triggered by a deeply equal object', async () => {
const App = function App({items}) {
Scheduler.unstable_advanceTime(10);
useEffect(() => {}, [items]);
};
Scheduler.unstable_advanceTime(1);
const items = ['one', 'two'];
await act(() => {
ReactNoop.render(<App items={items} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'Mount',
{
detail: {
devtools: {
color: 'warning',
properties: null,
tooltipText: 'Mount',
track: 'Components ⚛',
},
},
end: 11,
start: 1,
},
],
]);
performance.measure.mockClear();
Scheduler.unstable_advanceTime(10);
await act(() => {
ReactNoop.render(<App items={items.concat('4')} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'App',
{
detail: {
devtools: {
color: 'primary-dark',
properties: [
['Changed Props', ''],
[' items', 'Array'],
['+   2', '…'],
],
tooltipText: 'App',
track: 'Components ⚛',
},
},
end: 31,
start: 21,
},
],
]);
});
// @gate __DEV__ && enableComponentPerformanceTrack
it('bails out of diffing wide arrays', async () => {
const App = function App({items}) {
Scheduler.unstable_advanceTime(10);
React.useEffect(() => {}, [items]);
};
Scheduler.unstable_advanceTime(1);
const items = Array.from({length: 1000}, (_, i) => i);
await act(() => {
ReactNoop.render(<App items={items} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'Mount',
{
detail: {
devtools: {
color: 'warning',
properties: null,
tooltipText: 'Mount',
track: 'Components ⚛',
},
},
end: 11,
start: 1,
},
],
]);
performance.measure.mockClear();
Scheduler.unstable_advanceTime(10);
await act(() => {
ReactNoop.render(<App items={items.concat('-1')} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'App',
{
detail: {
devtools: {
color: 'primary-dark',
properties: [
['Changed Props', ''],
[' items', 'Array'],
[
'Previous object has more than 100 properties. React will not attempt to diff objects with too many properties.',
'',
],
[
'Next object has more than 100 properties. React will not attempt to diff objects with too many properties.',
'',
],
],
tooltipText: 'App',
track: 'Components ⚛',
},
},
end: 31,
start: 21,
},
],
]);
});
// @gate __DEV__ && enableComponentPerformanceTrack
it('does not show all properties of wide objects', async () => {
const App = function App({items}) {
Scheduler.unstable_advanceTime(10);
React.useEffect(() => {}, [items]);
};
Scheduler.unstable_advanceTime(1);
await act(() => {
ReactNoop.render(<App data={{buffer: null}} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'Mount',
{
detail: {
devtools: {
color: 'warning',
properties: null,
tooltipText: 'Mount',
track: 'Components ⚛',
},
},
end: 11,
start: 1,
},
],
]);
performance.measure.mockClear();
Scheduler.unstable_advanceTime(10);
const bigData = new Uint8Array(1000);
await act(() => {
ReactNoop.render(<App data={{buffer: bigData}} />);
});
expect(performance.measure.mock.calls).toEqual([
[
'App',
{
detail: {
devtools: {
color: 'primary-dark',
properties: [
['Changed Props', ''],
[' data', ''],
['   buffer', 'null'],
['+   buffer', 'Uint8Array'],
['+     0', '0'],
['+     1', '0'],
['+     2', '0'],
['+     3', '0'],
['+     4', '0'],
['+     5', '0'],
['+     6', '0'],
['+     7', '0'],
['+     8', '0'],
['+     9', '0'],
['+     10', '0'],
['+     11', '0'],
['+     12', '0'],
['+     13', '0'],
['+     14', '0'],
['+     15', '0'],
['+     16', '0'],
['+     17', '0'],
['+     18', '0'],
['+     19', '0'],
['+     20', '0'],
['+     21', '0'],
['+     22', '0'],
['+     23', '0'],
['+     24', '0'],
['+     25', '0'],
['+     26', '0'],
['+     27', '0'],
['+     28', '0'],
['+     29', '0'],
['+     30', '0'],
['+     31', '0'],
['+     32', '0'],
['+     33', '0'],
['+     34', '0'],
['+     35', '0'],
['+     36', '0'],
['+     37', '0'],
['+     38', '0'],
['+     39', '0'],
['+     40', '0'],
['+     41', '0'],
['+     42', '0'],
['+     43', '0'],
['+     44', '0'],
['+     45', '0'],
['+     46', '0'],
['+     47', '0'],
['+     48', '0'],
['+     49', '0'],
['+     50', '0'],
['+     51', '0'],
['+     52', '0'],
['+     53', '0'],
['+     54', '0'],
['+     55', '0'],
['+     56', '0'],
['+     57', '0'],
['+     58', '0'],
['+     59', '0'],
['+     60', '0'],
['+     61', '0'],
['+     62', '0'],
['+     63', '0'],
['+     64', '0'],
['+     65', '0'],
['+     66', '0'],
['+     67', '0'],
['+     68', '0'],
['+     69', '0'],
['+     70', '0'],
['+     71', '0'],
['+     72', '0'],
['+     73', '0'],
['+     74', '0'],
['+     75', '0'],
['+     76', '0'],
['+     77', '0'],
['+     78', '0'],
['+     79', '0'],
['+     80', '0'],
['+     81', '0'],
['+     82', '0'],
['+     83', '0'],
['+     84', '0'],
['+     85', '0'],
['+     86', '0'],
['+     87', '0'],
['+     88', '0'],
['+     89', '0'],
['+     90', '0'],
['+     91', '0'],
['+     92', '0'],
['+     93', '0'],
['+     94', '0'],
['+     95', '0'],
['+     96', '0'],
['+     97', '0'],
['+     98', '0'],
['+     99', '0'],
[
'+     Only 100 properties are shown. React will not log more properties of this object.',
'',
],
],
tooltipText: 'App',
track: 'Components ⚛',
},
},
end: 31,
start: 21,
},
],
]);
});
});

View File

@ -18,9 +18,13 @@ 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)
// Showing wider objects in the devtools is not useful.
const OBJECT_WIDTH_LIMIT = 100;
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
let kind: 0 | 1 | 2 | 3 = EMPTY_ARRAY;
for (let i = 0; i < array.length; i++) {
for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
const value = array[i];
if (typeof value === 'object' && value !== null) {
if (
@ -55,10 +59,23 @@ export function addObjectToProperties(
indent: number,
prefix: string,
): void {
let addedProperties = 0;
for (const key in object) {
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
addedProperties++;
const value = object[key];
addValueToProperties(key, value, properties, indent, prefix);
if (addedProperties >= OBJECT_WIDTH_LIMIT) {
properties.push([
prefix +
'\xa0\xa0'.repeat(indent) +
'Only ' +
OBJECT_WIDTH_LIMIT +
' properties are shown. React will not log more properties of this object.',
'',
]);
break;
}
}
}
}
@ -103,7 +120,9 @@ export function addValueToProperties(
addValueToProperties('key', key, properties, indent + 1, prefix);
}
let hasChildren = false;
let addedProperties = 0;
for (const propKey in props) {
addedProperties++;
if (propKey === 'children') {
if (
props.children != null &&
@ -123,6 +142,10 @@ export function addValueToProperties(
prefix,
);
}
if (addedProperties >= OBJECT_WIDTH_LIMIT) {
break;
}
}
properties.push([
'',
@ -135,16 +158,21 @@ export function addValueToProperties(
let objectName = objectToString.slice(8, objectToString.length - 1);
if (objectName === 'Array') {
const array: Array<any> = (value: any);
const didTruncate = array.length > OBJECT_WIDTH_LIMIT;
const kind = getArrayKind(array);
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
desc = JSON.stringify(array);
desc = JSON.stringify(
didTruncate
? array.slice(0, OBJECT_WIDTH_LIMIT).concat('…')
: array,
);
break;
} else if (kind === ENTRIES_ARRAY) {
properties.push([
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
'',
]);
for (let i = 0; i < array.length; i++) {
for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
const entry = array[i];
addValueToProperties(
entry[0],
@ -154,6 +182,15 @@ export function addValueToProperties(
prefix,
);
}
if (didTruncate) {
addValueToProperties(
OBJECT_WIDTH_LIMIT.toString(),
'…',
properties,
indent + 1,
prefix,
);
}
return;
}
}
@ -254,13 +291,39 @@ export function addObjectDiffToProperties(
// If a property is added or removed, we just emit the property name and omit the value it had.
// Mainly for performance. We need to minimize to only relevant information.
let isDeeplyEqual = true;
let prevPropertiesChecked = 0;
for (const key in prev) {
if (prevPropertiesChecked > OBJECT_WIDTH_LIMIT) {
properties.push([
'Previous object has more than ' +
OBJECT_WIDTH_LIMIT +
' properties. React will not attempt to diff objects with too many properties.',
'',
]);
isDeeplyEqual = false;
break;
}
if (!(key in next)) {
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
isDeeplyEqual = false;
}
prevPropertiesChecked++;
}
let nextPropertiesChecked = 0;
for (const key in next) {
if (nextPropertiesChecked > OBJECT_WIDTH_LIMIT) {
properties.push([
'Next object has more than ' +
OBJECT_WIDTH_LIMIT +
' properties. React will not attempt to diff objects with too many properties.',
'',
]);
isDeeplyEqual = false;
break;
}
if (key in prev) {
const prevValue = prev[key];
const nextValue = next[key];
@ -368,6 +431,8 @@ export function addObjectDiffToProperties(
properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
isDeeplyEqual = false;
}
nextPropertiesChecked++;
}
return isDeeplyEqual;
}