diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
new file mode 100644
index 0000000000000..72310812482e4
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
@@ -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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ 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();
+ });
+
+ 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,
+ },
+ ],
+ ]);
+ });
+});
diff --git a/packages/shared/ReactPerformanceTrackProperties.js b/packages/shared/ReactPerformanceTrackProperties.js
index 160c839c3ed52..87231c20c49d8 100644
--- a/packages/shared/ReactPerformanceTrackProperties.js
+++ b/packages/shared/ReactPerformanceTrackProperties.js
@@ -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 = (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;
}