From ff9f943741671b6d83d732b2131d3f7e7d3c54c8 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Wed, 4 Jan 2023 17:31:13 -0500 Subject: [PATCH] [DevTools] add perf regression test page in shell (#25078) ## Summary This PR adds a "perf regression tests" page to react-devtools-shell. This page is meant to be used as a performance sanity check we will run whenever we release a new version or finish a major refactor. Similar to other pages in the shell, this page can load the inline version of devtools and a test react app on the same page. But this page does not load devtools automatically like other pages. Instead, it provides a button that allows us to load devtools on-demand, so that we can easily compare perf numbers without devtools against the numbers with devtools. image As a first step, this page currently only contain one test: mount/unmount a large subtree. This is to catch perf issues that devtools can cause on the react applications it's running on, which was once a bug fixed in #24863. In the future, we plan to add: - more test apps covering different scenarios - perf numbers within devtools (e.g. initial load) ## How did you test this change? In order to show this test app can actually catch the perf regression it's aiming at, I reverted #24863 locally. Here is the result: https://user-images.githubusercontent.com/1001890/184059214-9c9b308c-173b-4dd7-b815-46fbd7067073.mov As shown in the video, the time it takes to unmount the large subtree significantly increased after DevTools is loaded. For comparison, here is how it looks like before the fix was reverted: image ## about the `requestAnimationFrame` method For this test, I used `requestAnimationFrame` to catch the time when render and commit are done. It aligns very well with the numbers reported by Chrome DevTools performance profiling. For example, in one run, the numbers reported by my method are image They are very close to the numbers reported by Chrome profiling: image image `` is not able to catch this issue here. If you are aware of a better way to do this, please kindly share with me. --- packages/react-devtools-shell/index.html | 3 + .../react-devtools-shell/perf-regression.html | 45 +++++++++++++++ .../src/e2e-regression/devtools.js | 2 +- .../src/perf-regression/app.js | 22 ++++++++ .../src/perf-regression/apps/LargeSubtree.js | 44 +++++++++++++++ .../src/perf-regression/apps/index.js | 19 +++++++ .../src/perf-regression/devtools.js | 55 +++++++++++++++++++ .../react-devtools-shell/webpack.config.js | 2 + 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 packages/react-devtools-shell/perf-regression.html create mode 100644 packages/react-devtools-shell/src/perf-regression/app.js create mode 100644 packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js create mode 100644 packages/react-devtools-shell/src/perf-regression/apps/index.js create mode 100644 packages/react-devtools-shell/src/perf-regression/devtools.js diff --git a/packages/react-devtools-shell/index.html b/packages/react-devtools-shell/index.html index 81b9c2ce18ccc..4cde55278a9a9 100644 --- a/packages/react-devtools-shell/index.html +++ b/packages/react-devtools-shell/index.html @@ -51,7 +51,10 @@ multi DevTools | e2e tests + | e2e regression tests + | + perf regression tests diff --git a/packages/react-devtools-shell/perf-regression.html b/packages/react-devtools-shell/perf-regression.html new file mode 100644 index 0000000000000..27fd87f88dd8a --- /dev/null +++ b/packages/react-devtools-shell/perf-regression.html @@ -0,0 +1,45 @@ + + + + + React DevTools + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/packages/react-devtools-shell/src/e2e-regression/devtools.js b/packages/react-devtools-shell/src/e2e-regression/devtools.js index d8554eb558657..eb3a62622415f 100644 --- a/packages/react-devtools-shell/src/e2e-regression/devtools.js +++ b/packages/react-devtools-shell/src/e2e-regression/devtools.js @@ -9,7 +9,7 @@ import {initialize as createDevTools} from 'react-devtools-inline/frontend'; // This is a pretty gross hack to make the runtime loaded named-hooks-code work. // TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. -// $FlowFixMer +// $FlowFixMe __webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. diff --git a/packages/react-devtools-shell/src/perf-regression/app.js b/packages/react-devtools-shell/src/perf-regression/app.js new file mode 100644 index 0000000000000..88308e0fcf345 --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/app.js @@ -0,0 +1,22 @@ +/** @flow */ + +// This test harness mounts each test app as a separate root to test multi-root applications. + +import * as React from 'react'; +import {createRoot} from 'react-dom/client'; +import App from './apps/index'; + +function mountApp() { + const container = document.createElement('div'); + + ((document.body: any): HTMLBodyElement).appendChild(container); + + const root = createRoot(container); + root.render( + + + , + ); +} + +mountApp(); diff --git a/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js b/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js new file mode 100644 index 0000000000000..11da40cf2c111 --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 * as React from 'react'; + +function generateArray(size) { + return Array.from({length: size}, () => Math.floor(Math.random() * size)); +} + +const arr = generateArray(50000); + +export default function LargeSubtree() { + const [showList, setShowList] = React.useState(false); + const toggleList = () => { + const startTime = performance.now(); + setShowList(!showList); + // requestAnimationFrame should happen after render+commit is done + window.requestAnimationFrame(() => { + const afterRenderTime = performance.now(); + console.log( + `Time spent on ${ + showList ? 'unmounting' : 'mounting' + } the subtree: ${afterRenderTime - startTime}ms`, + ); + }); + }; + return ( +
+

Mount/Unmount a large subtree

+

Click the button to toggle the state. Open console for results.

+ +
    +
  • dummy item
  • + {showList && arr.map((num, idx) =>
  • {num}
  • )} +
+
+ ); +} diff --git a/packages/react-devtools-shell/src/perf-regression/apps/index.js b/packages/react-devtools-shell/src/perf-regression/apps/index.js new file mode 100644 index 0000000000000..2508dd40103ea --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/apps/index.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 * as React from 'react'; +import LargeSubtree from './LargeSubtree'; + +export default function Home() { + return ( +
+ +
+ ); +} diff --git a/packages/react-devtools-shell/src/perf-regression/devtools.js b/packages/react-devtools-shell/src/perf-regression/devtools.js new file mode 100644 index 0000000000000..ffbc8afd718ce --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/devtools.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import {createRoot} from 'react-dom/client'; +import { + activate as activateBackend, + initialize as initializeBackend, +} from 'react-devtools-inline/backend'; +import {initialize as createDevTools} from 'react-devtools-inline/frontend'; + +// This is a pretty gross hack to make the runtime loaded named-hooks-code work. +// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. +// $FlowFixMe +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef + +// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. +function hookNamesModuleLoaderFunction() { + return import('react-devtools-inline/hookNames'); +} + +function inject(contentDocument, sourcePath) { + const script = contentDocument.createElement('script'); + script.src = sourcePath; + + ((contentDocument.body: any): HTMLBodyElement).appendChild(script); +} + +function init( + appSource: string, + appIframe: HTMLIFrameElement, + devtoolsContainer: HTMLElement, + loadDevToolsButton: HTMLButtonElement, +) { + const {contentDocument, contentWindow} = appIframe; + + initializeBackend(contentWindow); + + inject(contentDocument, appSource); + + loadDevToolsButton.addEventListener('click', () => { + const DevTools = createDevTools(contentWindow); + createRoot(devtoolsContainer).render( + , + ); + activateBackend(contentWindow); + }); +} + +init( + 'dist/perf-regression-app.js', + document.getElementById('iframe'), + document.getElementById('devtools'), + document.getElementById('load-devtools'), +); diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 26d27a08a7cf6..e1a30f7334448 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -157,6 +157,8 @@ const app = makeConfig( 'multi-devtools': './src/multi/devtools.js', 'multi-right': './src/multi/right.js', 'e2e-regression': './src/e2e-regression/app.js', + 'perf-regression-app': './src/perf-regression/app.js', + 'perf-regression-devtools': './src/perf-regression/devtools.js', }, { react: resolve(builtModulesDir, 'react'),