Skip to content

Commit efa396d

Browse files
authored
test: Implement a perf testing benchmarking system (#1163)
1 parent cb1d687 commit efa396d

File tree

8 files changed

+1242
-1
lines changed

8 files changed

+1242
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ typings/
5858
.env
5959

6060
.idea
61-
/dist/
61+
/dist*
6262
website/build
6363
website/.docusaurus
6464
.rts2*

perf-testing/.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
25+
26+
# CPU profiles
27+
*.cpuprofile
28+
29+
# Yarn
30+
.yarn/*
31+
!.yarn/patches
32+
!.yarn/plugins
33+
!.yarn/releases
34+
!.yarn/sdks
35+
!.yarn/versions

perf-testing/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Immer Performance Testing
2+
3+
This directory contains performance testing tools for Immer, allowing you to benchmark different versions of Immer and analyze CPU profiles to identify performance bottlenecks.
4+
5+
## Setup
6+
7+
1. **Install dependencies** (in this directory):
8+
9+
```bash
10+
yarn install
11+
```
12+
13+
2. **Build Immer first**:
14+
15+
```bash
16+
yarn build-immer
17+
```
18+
19+
3. **Build the benchmark bundle**:
20+
```bash
21+
yarn build
22+
```
23+
24+
Alternately, you can rebuild both Immer and the benchmarking script:
25+
26+
```bash
27+
yarn build-with-latest
28+
```
29+
30+
## Usage
31+
32+
### Running Benchmarks
33+
34+
To run the performance benchmarks:
35+
36+
```bash
37+
# Run basic benchmarks, with relative version speed comparisons
38+
yarn benchmark
39+
40+
# Run the benchmarks, but also generate a CPU profile
41+
yarn profile
42+
```
43+
44+
### Analyzing CPU Profiles
45+
46+
After running `yarn profile`, you'll get a `.cpuprofile` file. To analyze it:
47+
48+
```bash
49+
# Analyze the most recent profile
50+
yarn analyze-profile your-profile.cpuprofile
51+
```
52+
53+
## What's Included
54+
55+
- **immutability-benchmarks.mjs**: Main benchmark script comparing different Immer versions
56+
- **read-cpuprofile.js**: Advanced CPU profile analyzer with sourcemap support
57+
- **rolldown.config.js**: Bundler configuration that eliminates `process.env` overhead
58+
59+
## Benchmark Versions
60+
61+
The benchmarks compare:
62+
63+
- **immer7-10**: Historical Immer versions
64+
- **immer10Perf**: Current development version (references `../dist`)
65+
- **vanilla**: Pure JavaScript implementations for baseline comparison
66+
67+
## Key Features
68+
69+
- **Sourcemap support**: CPU profile analysis includes original function names
70+
- **Version-aware analysis**: Breaks down performance by Immer version
71+
- **Production bundling**: Uses Rolldown to eliminate development overhead
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/* eslint-disable no-inner-declarations */
2+
import "source-map-support/register"
3+
4+
import {produce as produce5, setAutoFreeze as setAutoFreeze5} from "immer5"
5+
import {produce as produce6, setAutoFreeze as setAutoFreeze6} from "immer6"
6+
import {produce as produce7, setAutoFreeze as setAutoFreeze7} from "immer7"
7+
import {produce as produce8, setAutoFreeze as setAutoFreeze8} from "immer8"
8+
import {produce as produce9, setAutoFreeze as setAutoFreeze9} from "immer9"
9+
import {produce as produce10, setAutoFreeze as setAutoFreeze10} from "immer10"
10+
import {
11+
produce as produce10Perf,
12+
setAutoFreeze as setAutoFreeze10Perf
13+
// Uncomment when using a build of Immer that exposes this function,
14+
// and enable the corresponding line in the setStrictIteration object below.
15+
// setUseStrictIteration as setUseStrictIteration10Perf
16+
} from "immer10Perf"
17+
import {create as produceMutative} from "mutative"
18+
import {
19+
produce as produceMutativeCompat,
20+
setAutoFreeze as setAutoFreezeMutativeCompat
21+
} from "mutative-compat"
22+
import {bench, run, group, summary} from "mitata"
23+
24+
function createInitialState() {
25+
const initialState = {
26+
largeArray: Array.from({length: 10000}, (_, i) => ({
27+
id: i,
28+
value: Math.random(),
29+
nested: {key: `key-${i}`, data: Math.random()},
30+
moreNested: {
31+
items: Array.from({length: 100}, (_, i) => ({id: i, name: String(i)}))
32+
}
33+
})),
34+
otherData: Array.from({length: 10000}, (_, i) => ({
35+
id: i,
36+
name: `name-${i}`,
37+
isActive: i % 2 === 0
38+
}))
39+
}
40+
return initialState
41+
}
42+
43+
const MAX = 1
44+
45+
const add = index => ({
46+
type: "test/addItem",
47+
payload: {id: index, value: index, nested: {data: index}}
48+
})
49+
const remove = index => ({type: "test/removeItem", payload: index})
50+
const filter = index => ({type: "test/filterItem", payload: index})
51+
const update = index => ({
52+
type: "test/updateItem",
53+
payload: {id: index, value: index, nestedData: index}
54+
})
55+
const concat = index => ({
56+
type: "test/concatArray",
57+
payload: Array.from({length: 500}, (_, i) => ({id: i, value: index}))
58+
})
59+
60+
const actions = {
61+
add,
62+
remove,
63+
filter,
64+
update,
65+
concat
66+
}
67+
68+
const immerProducers = {
69+
// immer5: produce5,
70+
// immer6: produce6,
71+
immer7: produce7,
72+
immer8: produce8,
73+
immer9: produce9,
74+
immer10: produce10,
75+
immer10Perf: produce10Perf
76+
// mutative: produceMutative,
77+
// mutativeCompat: produceMutativeCompat
78+
}
79+
80+
const noop = () => {}
81+
82+
const setAutoFreezes = {
83+
vanilla: noop,
84+
immer5: setAutoFreeze5,
85+
immer6: setAutoFreeze6,
86+
immer7: setAutoFreeze7,
87+
immer8: setAutoFreeze8,
88+
immer9: setAutoFreeze9,
89+
immer10: setAutoFreeze10,
90+
immer10Perf: setAutoFreeze10Perf,
91+
mutative: noop,
92+
mutativeCompat: setAutoFreezeMutativeCompat
93+
}
94+
95+
const setStrictIteration = {
96+
vanilla: noop,
97+
immer5: noop,
98+
immer6: noop,
99+
immer7: noop,
100+
immer8: noop,
101+
immer9: noop,
102+
immer10: noop,
103+
immer10Perf: noop, // setUseStrictIteration10Perf,
104+
mutative: noop,
105+
mutativeCompat: noop
106+
}
107+
108+
const vanillaReducer = (state = createInitialState(), action) => {
109+
switch (action.type) {
110+
case "test/addItem":
111+
return {
112+
...state,
113+
largeArray: [...state.largeArray, action.payload]
114+
}
115+
case "test/removeItem": {
116+
const newArray = state.largeArray.slice()
117+
newArray.splice(action.payload, 1)
118+
return {
119+
...state,
120+
largeArray: newArray
121+
}
122+
}
123+
case "test/filterItem": {
124+
const newArray = state.largeArray.filter(
125+
(item, i) => i !== action.payload
126+
)
127+
return {
128+
...state,
129+
largeArray: newArray
130+
}
131+
}
132+
case "test/updateItem": {
133+
return {
134+
...state,
135+
largeArray: state.largeArray.map(item =>
136+
item.id === action.payload.id
137+
? {
138+
...item,
139+
value: action.payload.value,
140+
nested: {...item.nested, data: action.payload.nestedData}
141+
}
142+
: item
143+
)
144+
}
145+
}
146+
case "test/concatArray": {
147+
const length = state.largeArray.length
148+
const newArray = action.payload.concat(state.largeArray)
149+
newArray.length = length
150+
return {
151+
...state,
152+
largeArray: newArray
153+
}
154+
}
155+
default:
156+
return state
157+
}
158+
}
159+
160+
const createImmerReducer = produce => {
161+
const immerReducer = (state = createInitialState(), action) =>
162+
produce(state, draft => {
163+
switch (action.type) {
164+
case "test/addItem":
165+
draft.largeArray.push(action.payload)
166+
break
167+
case "test/removeItem":
168+
draft.largeArray.splice(action.payload, 1)
169+
break
170+
case "test/filterItem": {
171+
draft.largeArray = draft.largeArray.filter(
172+
(item, i) => i !== action.payload
173+
)
174+
break
175+
}
176+
case "test/updateItem": {
177+
const item = draft.largeArray.find(
178+
item => item.id === action.payload.id
179+
)
180+
item.value = action.payload.value
181+
item.nested.data = action.payload.nestedData
182+
break
183+
}
184+
case "test/concatArray": {
185+
const length = state.largeArray.length
186+
const newArray = action.payload.concat(state.largeArray)
187+
newArray.length = length
188+
draft.largeArray = newArray
189+
break
190+
}
191+
}
192+
})
193+
194+
return immerReducer
195+
}
196+
197+
function mapValues(obj, fn) {
198+
const result = {}
199+
for (const key in obj) {
200+
result[key] = fn(obj[key])
201+
}
202+
return result
203+
}
204+
205+
const reducers = {
206+
vanilla: vanillaReducer,
207+
...mapValues(immerProducers, createImmerReducer)
208+
}
209+
210+
function createBenchmarks() {
211+
for (const action in actions) {
212+
summary(function() {
213+
bench(`$action: $version (freeze: $freeze)`, function*(args) {
214+
const version = args.get("version")
215+
const freeze = args.get("freeze")
216+
const action = args.get("action")
217+
218+
const initialState = createInitialState()
219+
220+
function benchMethod() {
221+
setAutoFreezes[version](freeze)
222+
setStrictIteration[version](false)
223+
for (let i = 0; i < MAX; i++) {
224+
reducers[version](initialState, actions[action](i))
225+
}
226+
setAutoFreezes[version](false)
227+
}
228+
229+
yield benchMethod
230+
}).args({
231+
version: Object.keys(reducers),
232+
freeze: [false, true],
233+
action: [action]
234+
})
235+
})
236+
}
237+
}
238+
239+
async function main() {
240+
createBenchmarks()
241+
await run()
242+
process.exit(0)
243+
}
244+
245+
main()

perf-testing/package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "immer-perf-testing",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"benchmark": "cross-env NO_COLOR=true node --expose-gc --enable-source-maps dist/immutability-benchmarks.js",
8+
"build": "rolldown -c rolldown.config.js",
9+
"profile": "node --cpu-prof --expose-gc dist/immutability-benchmarks.js",
10+
"analyze-profile": "node read-cpuprofile.js",
11+
"build-immer": "cd .. && yarn build",
12+
"build-with-latest": "yarn build-immer && yarn build",
13+
"build-and-benchmark": "yarn build-with-latest && yarn benchmark",
14+
"build-and-profile": "yarn build-with-latest && yarn profile"
15+
},
16+
"dependencies": {
17+
"cross-env": "^7.0.3",
18+
"immer5": "npm:immer@5",
19+
"immer6": "npm:immer@6",
20+
"immer7": "npm:immer@7",
21+
"immer8": "npm:immer@8",
22+
"immer9": "npm:immer@9",
23+
"immer10": "npm:immer@10",
24+
"mitata": "^1.0.34",
25+
"mutative": "^1.1.0",
26+
"mutative-compat": "^0.1.2",
27+
"pprof-format": "^2.2.1",
28+
"source-map": "^0.7.4",
29+
"source-map-support": "^0.5.21"
30+
},
31+
"devDependencies": {
32+
"rolldown": "1.0.0-beta.23"
33+
}
34+
}

0 commit comments

Comments
 (0)