Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ typings/
.env

.idea
/dist/
/dist*
website/build
website/.docusaurus
.rts2*
Expand Down
35 changes: 35 additions & 0 deletions perf-testing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# CPU profiles
*.cpuprofile

# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
71 changes: 71 additions & 0 deletions perf-testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Immer Performance Testing

This directory contains performance testing tools for Immer, allowing you to benchmark different versions of Immer and analyze CPU profiles to identify performance bottlenecks.

## Setup

1. **Install dependencies** (in this directory):

```bash
yarn install
```

2. **Build Immer first**:

```bash
yarn build-immer
```

3. **Build the benchmark bundle**:
```bash
yarn build
```

Alternately, you can rebuild both Immer and the benchmarking script:

```bash
yarn build-with-latest
```

## Usage

### Running Benchmarks

To run the performance benchmarks:

```bash
# Run basic benchmarks, with relative version speed comparisons
yarn benchmark

# Run the benchmarks, but also generate a CPU profile
yarn profile
```

### Analyzing CPU Profiles

After running `yarn profile`, you'll get a `.cpuprofile` file. To analyze it:

```bash
# Analyze the most recent profile
yarn analyze-profile your-profile.cpuprofile
```

## What's Included

- **immutability-benchmarks.mjs**: Main benchmark script comparing different Immer versions
- **read-cpuprofile.js**: Advanced CPU profile analyzer with sourcemap support
- **rolldown.config.js**: Bundler configuration that eliminates `process.env` overhead

## Benchmark Versions

The benchmarks compare:

- **immer7-10**: Historical Immer versions
- **immer10Perf**: Current development version (references `../dist`)
- **vanilla**: Pure JavaScript implementations for baseline comparison

## Key Features

- **Sourcemap support**: CPU profile analysis includes original function names
- **Version-aware analysis**: Breaks down performance by Immer version
- **Production bundling**: Uses Rolldown to eliminate development overhead
245 changes: 245 additions & 0 deletions perf-testing/immutability-benchmarks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/* eslint-disable no-inner-declarations */
import "source-map-support/register"

import {produce as produce5, setAutoFreeze as setAutoFreeze5} from "immer5"
import {produce as produce6, setAutoFreeze as setAutoFreeze6} from "immer6"
import {produce as produce7, setAutoFreeze as setAutoFreeze7} from "immer7"
import {produce as produce8, setAutoFreeze as setAutoFreeze8} from "immer8"
import {produce as produce9, setAutoFreeze as setAutoFreeze9} from "immer9"
import {produce as produce10, setAutoFreeze as setAutoFreeze10} from "immer10"
import {
produce as produce10Perf,
setAutoFreeze as setAutoFreeze10Perf
// Uncomment when using a build of Immer that exposes this function,
// and enable the corresponding line in the setStrictIteration object below.
// setUseStrictIteration as setUseStrictIteration10Perf
} from "immer10Perf"
import {create as produceMutative} from "mutative"
import {
produce as produceMutativeCompat,
setAutoFreeze as setAutoFreezeMutativeCompat
} from "mutative-compat"
import {bench, run, group, summary} from "mitata"

function createInitialState() {
const initialState = {
largeArray: Array.from({length: 10000}, (_, i) => ({
id: i,
value: Math.random(),
nested: {key: `key-${i}`, data: Math.random()},
moreNested: {
items: Array.from({length: 100}, (_, i) => ({id: i, name: String(i)}))
}
})),
otherData: Array.from({length: 10000}, (_, i) => ({
id: i,
name: `name-${i}`,
isActive: i % 2 === 0
}))
}
return initialState
}

const MAX = 1

const add = index => ({
type: "test/addItem",
payload: {id: index, value: index, nested: {data: index}}
})
const remove = index => ({type: "test/removeItem", payload: index})
const filter = index => ({type: "test/filterItem", payload: index})
const update = index => ({
type: "test/updateItem",
payload: {id: index, value: index, nestedData: index}
})
const concat = index => ({
type: "test/concatArray",
payload: Array.from({length: 500}, (_, i) => ({id: i, value: index}))
})

const actions = {
add,
remove,
filter,
update,
concat
}

const immerProducers = {
// immer5: produce5,
// immer6: produce6,
immer7: produce7,
immer8: produce8,
immer9: produce9,
immer10: produce10,
immer10Perf: produce10Perf
// mutative: produceMutative,
// mutativeCompat: produceMutativeCompat
}

const noop = () => {}

const setAutoFreezes = {
vanilla: noop,
immer5: setAutoFreeze5,
immer6: setAutoFreeze6,
immer7: setAutoFreeze7,
immer8: setAutoFreeze8,
immer9: setAutoFreeze9,
immer10: setAutoFreeze10,
immer10Perf: setAutoFreeze10Perf,
mutative: noop,
mutativeCompat: setAutoFreezeMutativeCompat
}

const setStrictIteration = {
vanilla: noop,
immer5: noop,
immer6: noop,
immer7: noop,
immer8: noop,
immer9: noop,
immer10: noop,
immer10Perf: noop, // setUseStrictIteration10Perf,
mutative: noop,
mutativeCompat: noop
}

const vanillaReducer = (state = createInitialState(), action) => {
switch (action.type) {
case "test/addItem":
return {
...state,
largeArray: [...state.largeArray, action.payload]
}
case "test/removeItem": {
const newArray = state.largeArray.slice()
newArray.splice(action.payload, 1)
return {
...state,
largeArray: newArray
}
}
case "test/filterItem": {
const newArray = state.largeArray.filter(
(item, i) => i !== action.payload
)
return {
...state,
largeArray: newArray
}
}
case "test/updateItem": {
return {
...state,
largeArray: state.largeArray.map(item =>
item.id === action.payload.id
? {
...item,
value: action.payload.value,
nested: {...item.nested, data: action.payload.nestedData}
}
: item
)
}
}
case "test/concatArray": {
const length = state.largeArray.length
const newArray = action.payload.concat(state.largeArray)
newArray.length = length
return {
...state,
largeArray: newArray
}
}
default:
return state
}
}

const createImmerReducer = produce => {
const immerReducer = (state = createInitialState(), action) =>
produce(state, draft => {
switch (action.type) {
case "test/addItem":
draft.largeArray.push(action.payload)
break
case "test/removeItem":
draft.largeArray.splice(action.payload, 1)
break
case "test/filterItem": {
draft.largeArray = draft.largeArray.filter(
(item, i) => i !== action.payload
)
break
}
case "test/updateItem": {
const item = draft.largeArray.find(
item => item.id === action.payload.id
)
item.value = action.payload.value
item.nested.data = action.payload.nestedData
break
}
case "test/concatArray": {
const length = state.largeArray.length
const newArray = action.payload.concat(state.largeArray)
newArray.length = length
draft.largeArray = newArray
break
}
}
})

return immerReducer
}

function mapValues(obj, fn) {
const result = {}
for (const key in obj) {
result[key] = fn(obj[key])
}
return result
}

const reducers = {
vanilla: vanillaReducer,
...mapValues(immerProducers, createImmerReducer)
}

function createBenchmarks() {
for (const action in actions) {
summary(function() {
bench(`$action: $version (freeze: $freeze)`, function*(args) {
const version = args.get("version")
const freeze = args.get("freeze")
const action = args.get("action")

const initialState = createInitialState()

function benchMethod() {
setAutoFreezes[version](freeze)
setStrictIteration[version](false)
for (let i = 0; i < MAX; i++) {
reducers[version](initialState, actions[action](i))
}
setAutoFreezes[version](false)
}

yield benchMethod
}).args({
version: Object.keys(reducers),
freeze: [false, true],
action: [action]
})
})
}
}

async function main() {
createBenchmarks()
await run()
process.exit(0)
}

main()
34 changes: 34 additions & 0 deletions perf-testing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "immer-perf-testing",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"benchmark": "cross-env NO_COLOR=true node --expose-gc --enable-source-maps dist/immutability-benchmarks.js",
"build": "rolldown -c rolldown.config.js",
"profile": "node --cpu-prof --expose-gc dist/immutability-benchmarks.js",
"analyze-profile": "node read-cpuprofile.js",
"build-immer": "cd .. && yarn build",
"build-with-latest": "yarn build-immer && yarn build",
"build-and-benchmark": "yarn build-with-latest && yarn benchmark",
"build-and-profile": "yarn build-with-latest && yarn profile"
},
"dependencies": {
"cross-env": "^7.0.3",
"immer5": "npm:immer@5",
"immer6": "npm:immer@6",
"immer7": "npm:immer@7",
"immer8": "npm:immer@8",
"immer9": "npm:immer@9",
"immer10": "npm:immer@10",
"mitata": "^1.0.34",
"mutative": "^1.1.0",
"mutative-compat": "^0.1.2",
"pprof-format": "^2.2.1",
"source-map": "^0.7.4",
"source-map-support": "^0.5.21"
},
"devDependencies": {
"rolldown": "1.0.0-beta.23"
}
}
Loading
Loading