Skip to content

Commit 3e04ebe

Browse files
grumdmarkerikson
andauthored
Add benchmarks for autotracking with many components/slices (#31)
Co-authored-by: Mark Erikson <[email protected]>
1 parent e4acb03 commit 3e04ebe

File tree

20 files changed

+425
-922
lines changed

20 files changed

+425
-922
lines changed

README.md

+21-53
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,73 @@
11
# react-redux-benchmarks
2+
23
Performance benchmark harness for React-Redux
34

45
This repo expects that you are using Yarn for package management.
56

67
# Running benchmarks
8+
79
```bash
8-
yarn initialize
10+
yarn build
911
yarn start
1012
```
1113

12-
After benchmarks have been initialized, you can run with simply:
14+
After benchmarks have been built, you can run with simply:
1315

1416
```bash
1517
yarn start
1618
```
1719

18-
## Running specific versions
20+
You'll need to rebuild the benchmarks after every code change.
21+
22+
## Running specific versions of react-redux
1923

2024
To specify a single version:
2125

2226
```bash
23-
REDUX=5.0.7 yarn start
27+
yarn start --versions 8.1.1
28+
yarn start -v 8.1.1
2429
```
2530

2631
To specify running against multiple versions:
2732

2833
```bash
29-
REDUX=5.0.7:4.4.9 yarn start
34+
yarn start -v 8.1.1 7.2.5
3035
```
3136

32-
To run a specific benchmark:
37+
## To run a specific benchmark:
3338

3439
```bash
35-
BENCHMARKS=stockticker yarn start
40+
yarn start --scenarios deeptree
41+
yarn start -s deeptree
3642
```
3743

3844
or specific benchmarks:
3945

4046
```bash
41-
BENCHMARKS=stockticker:another yarn start
47+
yarn start -s deeptree forms
4248
```
4349

4450
## Setting run length
4551

4652
By default, benchmarks run for 30 seconds. To change this, use
4753

4854
```bash
49-
SECONDS=10 yarn start
55+
yarn start --length 5
56+
yarn start -l 5
5057
```
5158

52-
5359
# Adding a benchmark
5460

55-
Benchmarks live in the `sources/` directory. Each benchmark must insert this
56-
code into `index.js`:
57-
58-
```js
59-
import 'fps-emit'
60-
```
61-
62-
In addition, a `config-overrides.js` must be created with these contents:
61+
Benchmarks live in the `src/scenarios` directory. Each benchmark must render a React component like this:
6362

6463
```js
65-
module.exports = function override(config, env) {
66-
//do stuff with the webpack config...
67-
console.log(`Environment: ${env}`)
68-
69-
if(env === "production") {
70-
config.externals = {
71-
"react" : "React",
72-
"redux" : "Redux",
73-
"react-redux" : "ReactRedux",
74-
}
75-
}
76-
77-
78-
return config;
79-
}
80-
```
81-
82-
and the scripts section of `package.json` should be changed to:
83-
84-
```json
85-
"scripts": {
86-
"start": "react-app-rewired start",
87-
"build": "react-app-rewired build",
88-
"test": "react-app-rewired --env=jsdom",
89-
...
90-
}
91-
```
92-
93-
Also, `index.html` must be modified to include these lines:
64+
import { renderApp } from '../../common'
9465

95-
```html
96-
<script type="text/javascript" src="redux.min.js"></script>
97-
<script type="text/javascript" src="react.production.min.js"></script>
98-
<script type="text/javascript" src="react-dom.production.min.js"></script>
99-
<script type="text/javascript" src="react-redux.min.js"></script>
66+
renderApp(<App />, store)
10067
```
10168

69+
Where `App` is your benchmark component, and `store` is your redux store.
10270

10371
If you need to make changes to the `fps-emit` package, bump the version number in its `package.json`,
10472
then update each benchmark to use the newest version using `yarn upgrade-interactive` and selecting `fps-emit`
105-
for an update. Then rebuild all the benchmarks using `yarn initialize`
73+
for an update. Then rebuild all the benchmarks using `yarn build`

package.json

+2-4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@replayio/playwright": "^1.0.3",
2929
"@testing-library/dom": "^8.5.0",
3030
"@testing-library/user-event": "^13.2.1",
31+
"chalk": "4.1.2",
3132
"chance": "^1.1.8",
3233
"cli-table2": "^0.2.0",
3334
"cross-spawn": "^6.0.5",
@@ -37,15 +38,14 @@
3738
"glob": "^7.1.3",
3839
"performance-mark-metadata": "^1.0.3",
3940
"playwright": "^1.35.1",
40-
"puppeteer": "latest",
4141
"react": "^18.2.0",
4242
"react-dom": "^18.2.0",
4343
"react-redux": "^8.1.0",
4444
"react-redux-5.1.2": "npm:[email protected]",
4545
"react-redux-6.0.1": "npm:[email protected]",
4646
"react-redux-7.2.5": "npm:[email protected]",
47-
"react-redux-8.1.0": "npm:[email protected]",
4847
"react-redux-8.1.0-autotracking": "file:.yalc/react-redux",
48+
"react-redux-8.1.1": "npm:[email protected]",
4949
"recursive-copy": "^2.0.9",
5050
"redux": "^4.1.1",
5151
"reselect": "^4.0.0",
@@ -60,10 +60,8 @@
6060
"@types/fs-extra": "^9.0.12",
6161
"@types/glob": "^8.1.0",
6262
"@types/lodash": "^4.14.195",
63-
"@types/puppeteer": "^7.0.4",
6463
"@types/react": "^17.0.21",
6564
"@types/react-dom": "^17.0.9",
66-
"@types/yargs": "^17.0.24",
6765
"esbuild": "^0.17",
6866
"esbuild-plugin-alias": "^0.1.2",
6967
"fs-extra": "^10.0.0",

runBenchmarks.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Table from 'cli-table2'
99
import _ from 'lodash'
1010
import glob from 'glob'
1111
import yargs from 'yargs/yargs'
12+
import chalk from 'chalk'
1213

1314
import {
1415
capturePageStats,
@@ -53,6 +54,11 @@ const args = yargs(process.argv.slice(2))
5354
type: 'boolean',
5455
default: false,
5556
})
57+
.option('headless', {
58+
describe: 'Run Chrome in headless mode (default: true)',
59+
type: 'boolean',
60+
default: true,
61+
})
5662
.help('h')
5763
.alias('h', 'help')
5864

@@ -110,7 +116,7 @@ function printBenchmarkResults(benchmark, versionPerfEntries, trace) {
110116
table.push([
111117
version,
112118
fps.weightedFPS.toFixed(2),
113-
`${mountTime.toFixed(1)}, ${averageUpdateTime.toFixed(1)}`,
119+
`${mountTime?.toFixed(1)}, ${averageUpdateTime?.toFixed(1)}`,
114120
...traceResults,
115121
fpsNumbers.toString(),
116122
])
@@ -179,11 +185,19 @@ function calculateBenchmarkStats(
179185

180186
const [mountEntry, ...updateEntries] = reactTimingEntries
181187

182-
const mountTime = mountEntry.actualTime
188+
if (!mountEntry) {
189+
console.error(
190+
chalk.red(
191+
'Error during component mounting, run the benchmark with "--headless false" to inspect the console for React errors'
192+
)
193+
)
194+
}
195+
196+
const mountTime = mountEntry?.actualTime
183197

184198
const averageUpdateTime =
185-
updateEntries.reduce((sum, entry) => sum + entry.actualTime, 0) /
186-
updateEntries.length || 1
199+
updateEntries?.reduce((sum, entry) => sum + entry.actualTime, 0) /
200+
updateEntries?.length || 1
187201

188202
return { fps, profile: { categories }, mountTime, averageUpdateTime }
189203
}
@@ -193,11 +207,13 @@ async function runBenchmarks({
193207
versions,
194208
length,
195209
trace,
210+
headless,
196211
}: {
197212
scenarios: string[]
198213
versions: string[]
199214
length: number
200215
trace: boolean
216+
headless: boolean
201217
}) {
202218
console.log('Scenarios: ', scenarios)
203219
const distFolder = path.resolve('dist')
@@ -211,7 +227,7 @@ async function runBenchmarks({
211227
for (let version of versions) {
212228
console.log(` React-Redux version: ${version}`)
213229
const browser = await playwright.chromium.launch({
214-
headless: true,
230+
headless,
215231
})
216232

217233
const folderPath = path.join(distFolder, version, scenario)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react'
2+
import { useSelector } from 'react-redux'
3+
4+
import { NUMBER_OF_COMPONENTS } from './constants'
5+
6+
import { getSliceName } from './state'
7+
import type { RootState } from './state'
8+
9+
const createComponent = (index: number) => {
10+
const sliceName = getSliceName(index)
11+
12+
return function Component() {
13+
// NOTE: "Bugged" selector used on purpose to force a rerender on every state change
14+
// Returning a new object every time causes a rerender
15+
const { counter } = useSelector((state: RootState) => {
16+
return {
17+
counter: state[sliceName].counter,
18+
}
19+
})
20+
21+
return <div>{counter}</div>
22+
}
23+
}
24+
25+
function App() {
26+
return (
27+
<div>
28+
{Array.from({ length: NUMBER_OF_COMPONENTS }).map((_, index) => {
29+
// App is rendered only once, so components are not re-created
30+
const Component = createComponent(index)
31+
return <Component key={index} />
32+
})}
33+
</div>
34+
)
35+
}
36+
37+
export default App
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const NUMBER_OF_COMPONENTS = 5000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useLayoutEffect } from 'react'
2+
import { configureStore } from '@reduxjs/toolkit'
3+
4+
import { renderApp } from '../../common'
5+
import { rootReducer, incrementActions } from './state'
6+
7+
import App from './App'
8+
9+
const store = configureStore({
10+
reducer: rootReducer,
11+
middleware: (gdm) =>
12+
gdm({
13+
immutabilityCheck: false,
14+
serializableCheck: false,
15+
}),
16+
})
17+
18+
const incrementRandom = () => {
19+
const randomIndex = Math.floor(Math.random() * incrementActions.length)
20+
store.dispatch(incrementActions[randomIndex]())
21+
}
22+
23+
const RootApp = () => {
24+
useLayoutEffect(() => {
25+
setInterval(incrementRandom, 13)
26+
}, [])
27+
28+
return <App />
29+
}
30+
31+
// @ts-ignore
32+
renderApp(RootApp, store)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { combineReducers, createSlice } from '@reduxjs/toolkit'
2+
import { NUMBER_OF_COMPONENTS } from './constants'
3+
4+
export const getSliceName = (index: number) => `counter_${index}`
5+
6+
const createStateSlice = (index: number) => {
7+
return createSlice({
8+
name: getSliceName(index),
9+
initialState: {
10+
counter: 0,
11+
},
12+
reducers: {
13+
increment(state) {
14+
state.counter += 1
15+
},
16+
},
17+
})
18+
}
19+
20+
const slices = Array.from({
21+
length: NUMBER_OF_COMPONENTS,
22+
}).map((_, i) => createStateSlice(i))
23+
24+
export const rootReducer = combineReducers(
25+
slices.reduce(
26+
(acc: Record<string, typeof slices[number]['reducer']>, slice) => {
27+
acc[slice.name] = slice.reducer
28+
return acc
29+
},
30+
{}
31+
)
32+
)
33+
34+
export const incrementActions = slices.map((slice) => slice.actions.increment)
35+
36+
export type RootState = ReturnType<typeof rootReducer>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import { useSelector } from 'react-redux'
3+
4+
import { NUMBER_OF_COMPONENTS } from './constants'
5+
6+
import { getSliceName } from './state'
7+
import type { RootState } from './state'
8+
9+
const createComponent = (index: number) => {
10+
const sliceName = getSliceName(index)
11+
12+
return function Component() {
13+
const counter = useSelector((state: RootState) => state[sliceName].counter)
14+
15+
return <div>{counter}</div>
16+
}
17+
}
18+
19+
function App() {
20+
return (
21+
<div>
22+
{Array.from({ length: NUMBER_OF_COMPONENTS }).map((_, index) => {
23+
// App is rendered only once, so components are not re-created
24+
const Component = createComponent(index)
25+
return <Component key={index} />
26+
})}
27+
</div>
28+
)
29+
}
30+
31+
export default App
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const NUMBER_OF_COMPONENTS = 5000

0 commit comments

Comments
 (0)