Skip to content
Draft
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
127 changes: 127 additions & 0 deletions perf-testing/rtkq-testing.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {produce} from "../dist/immer.mjs"

function createInitialState(arraySize = BENCHMARK_CONFIG.arraySize) {
const initialState = {
largeArray: Array.from({length: arraySize}, (_, i) => ({
id: i,
value: Math.random(),
nested: {key: `key-${i}`, data: Math.random()},
moreNested: {
items: Array.from(
{length: BENCHMARK_CONFIG.nestedArraySize},
(_, i) => ({id: i, name: String(i)})
)
}
})),
otherData: Array.from({length: arraySize}, (_, i) => ({
id: i,
name: `name-${i}`,
isActive: i % 2 === 0
})),
api: {
queries: {},
provided: {
keys: {}
},
subscriptions: {}
}
}
return initialState
}

const MAX = 1

const BENCHMARK_CONFIG = {
iterations: 1,
arraySize: 100,
nestedArraySize: 10,
multiUpdateCount: 5,
reuseStateIterations: 10
}

// RTKQ-style action creators
const rtkqPending = index => ({
type: "rtkq/pending",
payload: {
cacheKey: `some("test-${index}-")`,
requestId: `req-${index}`,
id: `test-${index}-`
}
})

const rtkqResolved = index => ({
type: "rtkq/resolved",
payload: {
cacheKey: `some("test-${index}-")`,
requestId: `req-${index}`,
id: `test-${index}-`,
data: `test-${index}-1`
}
})

const createImmerReducer = produce => {
const immerReducer = (state = createInitialState(), action) =>
produce(state, draft => {
switch (action.type) {
case "rtkq/pending": {
// Simulate separate RTK slice reducers with combined reducer pattern
const cacheKey = action.payload.cacheKey
draft.api.queries[cacheKey] = {
id: action.payload.id,
status: "pending",
data: undefined
}
draft.api.provided.keys[cacheKey] = {}
draft.api.subscriptions[cacheKey] = {
[action.payload.requestId]: {
pollingInterval: 0,
skipPollingIfUnfocused: false
}
}
break
}
case "rtkq/resolved": {
const cacheKey = action.payload.cacheKey
draft.api.queries[cacheKey].status = "fulfilled"
draft.api.queries[cacheKey].data = action.payload.data
// provided and subscriptions don't change on resolved
break
}
}
})

return immerReducer
}

const immerReducer = createImmerReducer(produce)
const initialState = createInitialState()

const arraySizes = [10, 100, 250, 500, 1000, 1021, 1500, 2000, 3000]

for (const arraySize of arraySizes) {
console.log(`Running benchmark with array size: ${arraySize}`)

const start = performance.now()

let state = initialState

// Phase 1: Execute all pending actions
for (let i = 0; i < arraySize; i++) {
state = immerReducer(state, rtkqPending(i))
}

// Phase 2: Execute all resolved actions
for (let i = 0; i < arraySize; i++) {
state = immerReducer(state, rtkqResolved(i))
}

const end = performance.now()
const total = end - start
const avg = total / arraySize

console.log(
`Done in ${total.toFixed(1)} ms (items: ${arraySize}, avg: ${avg.toFixed(
3
)} ms / item)`
)
}
9 changes: 7 additions & 2 deletions src/core/immerClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ interface ProducersFns {
produceWithPatches: IProduceWithPatches
}

export type StrictMode = boolean | "class_only"
export type StrictMode =
| boolean
| "class_only"
| "strings_only"
| "with_symbols"
| "full"

export class Immer implements ProducersFns {
autoFreeze_: boolean = true
Expand All @@ -45,7 +50,7 @@ export class Immer implements ProducersFns {
}) {
if (typeof config?.autoFreeze === "boolean")
this.setAutoFreeze(config!.autoFreeze)
if (typeof config?.useStrictShallowCopy === "boolean")
if (typeof config?.useStrictShallowCopy !== "undefined")
this.setUseStrictShallowCopy(config!.useStrictShallowCopy)
if (typeof config?.useStrictIteration === "boolean")
this.setUseStrictIteration(config!.useStrictIteration)
Expand Down
25 changes: 24 additions & 1 deletion src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ export function shallowCopy(base: any, strict: StrictMode) {

const isPlain = isPlainObject(base)

if (strict === true || (strict === "class_only" && !isPlain)) {
if (
strict === true ||
strict === "full" ||
(strict === "class_only" && !isPlain)
) {
// Perform a strict copy
const descriptors = Object.getOwnPropertyDescriptors(base)
delete descriptors[DRAFT_STATE as any]
Expand Down Expand Up @@ -192,6 +196,25 @@ export function shallowCopy(base: any, strict: StrictMode) {
// perform a sloppy copy
const proto = getPrototypeOf(base)
if (proto !== null && isPlain) {
// v8 has a perf cliff at 1020 properties where it
// switches from "fast properties" to "dictionary mode" at 1020 keys:
// - https://github.com/v8/v8/blob/754e7ba956b06231c487e09178aab9baba1f46fe/src/objects/property-details.h#L242-L247
// - https://github.com/v8/v8/blob/754e7ba956b06231c487e09178aab9baba1f46fe/test/mjsunit/dictionary-prototypes.js
// At that size, object spread gets drastically slower,
// and an `Object.keys()` loop becomes _faster_.
// Immer currently expects that we also copy symbols. That would require either a `Reflect.ownKeys()`,
// or `.keys()` + `.getOwnPropertySymbols()`.
// For v10.x, we can keep object spread the default,
// and offer an option to switch to just strings to enable better perf
// with larger objects. For v11, we can flip those defaults.
if (strict === "strings_only") {
const copy: Record<string | symbol, any> = {}
Object.keys(base).forEach(key => {
copy[key] = base[key]
})
return copy
}

return {...base} // assumption: better inner class optimization than the assign below
}
const obj = Object.create(proto)
Expand Down
Loading