Skip to content

Commit

Permalink
[DevTools] add perf regression test page in shell (#25078)
Browse files Browse the repository at this point in the history
## 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.

<img width="561" alt="image"
src="https://user-images.githubusercontent.com/1001890/184059633-e4f0852c-8464-4d94-8064-1684eee626f4.png">

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:
<img width="452" alt="image"
src="https://user-images.githubusercontent.com/1001890/184059743-0968bc7d-4ce4-42cd-b04a-f6cbc078d4f4.png">

## 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
<img width="464" alt="image"
src="https://user-images.githubusercontent.com/1001890/184060228-990a4c75-f594-411a-9f85-fa5532ec8c37.png">
They are very close to the numbers reported by Chrome profiling:
<img width="456" alt="image"
src="https://user-images.githubusercontent.com/1001890/184060355-a15d1ec5-c296-4016-9c83-03e761f387e3.png">

<img width="354" alt="image"
src="https://user-images.githubusercontent.com/1001890/184060375-19029010-3aed-4a23-890e-397cdba86d9e.png">

`<Profiler>` is not able to catch this issue here.

If you are aware of a better way to do this, please kindly share with
me.
  • Loading branch information
mondaychen committed Jan 4, 2023
1 parent c2d6552 commit ff9f943
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/react-devtools-shell/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
<a href="/multi.html">multi DevTools</a>
|
<a href="/e2e.html">e2e tests</a>
|
<a href="/e2e-regression.html">e2e regression tests</a>
|
<a href="/perf-regression.html">perf regression tests</a>
</span>
</div>

Expand Down
45 changes: 45 additions & 0 deletions packages/react-devtools-shell/perf-regression.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>React DevTools</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
font-size: 12px;
line-height: 1.5;
}
#iframe {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 60vh;
}
#devtools {
position: absolute;
bottom: 0;
left: 0;
width: 100vw;
height: 40vh;
}
#load-devtools {
margin: 20px;
}
</style>
</head>
<body>
<iframe id="iframe"></iframe>
<div id="devtools">
<button id="load-devtools">Load DevTools</button>
</div>
<script src="dist/perf-regression-devtools.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions packages/react-devtools-shell/src/perf-regression/app.js
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}

mountApp();
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h2>Mount/Unmount a large subtree</h2>
<p>Click the button to toggle the state. Open console for results.</p>
<button onClick={toggleList}>toggle</button>
<ul>
<li key="dummy">dummy item</li>
{showList && arr.map((num, idx) => <li key={idx}>{num}</li>)}
</ul>
</div>
);
}
19 changes: 19 additions & 0 deletions packages/react-devtools-shell/src/perf-regression/apps/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<LargeSubtree />
</div>
);
}
55 changes: 55 additions & 0 deletions packages/react-devtools-shell/src/perf-regression/devtools.js
Original file line number Diff line number Diff line change
@@ -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(
<DevTools
hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
showTabBar={true}
/>,
);
activateBackend(contentWindow);
});
}

init(
'dist/perf-regression-app.js',
document.getElementById('iframe'),
document.getElementById('devtools'),
document.getElementById('load-devtools'),
);
2 changes: 2 additions & 0 deletions packages/react-devtools-shell/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down

0 comments on commit ff9f943

Please sign in to comment.