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; }