From ba4e5bc43ee76dbb4b03f4b0a48af22fccd86c19 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sat, 4 Jan 2025 01:36:49 +0800 Subject: [PATCH] perf(array): improve array remove method perf --- package.json | 2 + src/draft.ts | 39 ++++- src/utils/finalize.ts | 5 +- test/performance/benchmark-reducer.ts | 5 +- test/performance/benchmark-reducer1.mjs | 181 ++++++++++++++++++++++++ yarn.lock | 5 + 6 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 test/performance/benchmark-reducer1.mjs diff --git a/package.json b/package.json index b39b25f2..7b9a010a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "benchmark": "yarn build && yarn benchmark:base && yarn benchmark:object && yarn benchmark:array && yarn benchmark:class", "all-benchmark": "yarn build && NODE_ENV='production' ts-node test/benchmark/index.ts", "benchmark:reducer": "NODE_ENV='production' ts-node test/performance/benchmark-reducer.ts", + "benchmark:reducer1": "NODE_ENV='production' node test/performance/benchmark-reducer1.mjs", "benchmark:base": "NODE_ENV='production' ts-node test/performance/benchmark.ts", "benchmark:object": "NODE_ENV='production' ts-node test/performance/benchmark-object.ts", "benchmark:array": "NODE_ENV='production' ts-node test/performance/benchmark-array.ts", @@ -117,6 +118,7 @@ "json2csv": "^5.0.7", "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", + "mitata": "^1.0.25", "prettier": "^3.3.3", "quickchart-js": "^3.1.2", "redux": "^5.0.1", diff --git a/src/draft.ts b/src/draft.ts index 53904482..06d1d546 100644 --- a/src/draft.ts +++ b/src/draft.ts @@ -29,12 +29,16 @@ import { finalizeSetValue, markFinalization, finalizePatches, + isDraft, + skipFinalization, } from './utils'; import { checkReadable } from './unsafe'; import { generatePatches } from './patch'; const draftsCache = new WeakSet(); +let arrayHandling = false; + const proxyHandler: ProxyHandler = { get(target: ProxyDraft, key: string | number | symbol, receiver: any) { const copy = target.copy?.[key]; @@ -88,6 +92,26 @@ const proxyHandler: ProxyHandler = { if (!has(source, key)) { const desc = getDescriptor(source, key); + if (target.type === DraftType.Array) { + if ( + [ + 'splice', + 'push', + 'pop', + 'shift', + 'unshift', + 'sort', + 'reverse', + ].includes(key as string) + ) { + return function (this: any, ...args: any[]) { + arrayHandling = true; + const result = desc!.value.apply(this, args); + arrayHandling = false; + return result; + }; + } + } return desc ? `value` in desc ? desc.value @@ -103,7 +127,7 @@ const proxyHandler: ProxyHandler = { return value; } // Ensure that the assigned values are not drafted - if (value === peek(target.original, key)) { + if (value === peek(target.original, key) && !arrayHandling) { ensureShallowCopy(target); target.copy![key] = createDraft({ original: target.original[key], @@ -122,6 +146,11 @@ const proxyHandler: ProxyHandler = { } return target.copy![key]; } + if (arrayHandling && !isDraft(value)) { + skipFinalization.add(value); + } else if (skipFinalization.has(value)) { + skipFinalization.delete(value); + } return value; }, set(target: ProxyDraft, key: string | number | symbol, value: any) { @@ -319,10 +348,10 @@ export function finalizeDraft( const state = hasReturnedValue ? returnedValue[0] : proxyDraft - ? proxyDraft.operated - ? proxyDraft.copy - : proxyDraft.original - : result; + ? proxyDraft.operated + ? proxyDraft.copy + : proxyDraft.original + : result; if (proxyDraft) revokeProxy(proxyDraft); if (enableAutoFreeze) { deepFreeze(state, state, proxyDraft?.options.updatedValues); diff --git a/src/utils/finalize.ts b/src/utils/finalize.ts index f545c62d..984407ec 100644 --- a/src/utils/finalize.ts +++ b/src/utils/finalize.ts @@ -12,6 +12,8 @@ import { } from './draft'; import { forEach } from './forEach'; +export const skipFinalization = new WeakSet(); + export function handleValue( target: any, handledSet: WeakSet, @@ -21,7 +23,8 @@ export function handleValue( isDraft(target) || !isDraftable(target, options) || handledSet.has(target) || - Object.isFrozen(target) + Object.isFrozen(target) || + skipFinalization.has(target) ) return; const isSet = target instanceof Set; diff --git a/test/performance/benchmark-reducer.ts b/test/performance/benchmark-reducer.ts index dba6656e..0499bd2f 100644 --- a/test/performance/benchmark-reducer.ts +++ b/test/performance/benchmark-reducer.ts @@ -194,6 +194,7 @@ const MAX_ITERATIONS = 100; } console.timeEnd('immer:autoFreeze:nextAction'); } +console.log('---------------------------------'); { setAutoFreeze(false); const initialState = createInitialState(); @@ -207,7 +208,7 @@ const MAX_ITERATIONS = 100; } console.timeEnd('immer:nextAction'); } - +console.log('---------------------------------'); { const initialState = createInitialState(); console.time('mutative:autoFreeze'); @@ -220,7 +221,7 @@ const MAX_ITERATIONS = 100; } console.timeEnd('mutative:autoFreeze:nextAction'); } - +console.log('---------------------------------'); { const initialState = createInitialState(); console.time('vanilla'); diff --git a/test/performance/benchmark-reducer1.mjs b/test/performance/benchmark-reducer1.mjs new file mode 100644 index 00000000..700d744a --- /dev/null +++ b/test/performance/benchmark-reducer1.mjs @@ -0,0 +1,181 @@ +import { produce as produce10, setAutoFreeze as setAutoFreeze10 } from 'immer'; +import { create as produceMutative } from '../../dist/mutative.esm.mjs'; +import { bench, run, 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 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, + update, + concat, +}; + +const immerProducers = { + immer10: produce10, + mutative: produceMutative, +}; + +const setAutoFreezes = { + vanilla: () => {}, + immer10: setAutoFreeze10, + mutative: () => {}, +}; + +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/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/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); + 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(); +} + +main(); diff --git a/yarn.lock b/yarn.lock index 61d849ca..722f9262 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5412,6 +5412,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +mitata@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/mitata/-/mitata-1.0.25.tgz#918d0d04d2be0aeae7152cc7d8373b3a727b1b4f" + integrity sha512-0v5qZtVW5vwj9FDvYfraR31BMDcRLkhSFWPTLaxx/Z3/EvScfVtAAWtMI2ArIbBcwh7P86dXh0lQWKiXQPlwYA== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"