Skip to content

Commit

Permalink
perf(array): improve array remove method perf
Browse files Browse the repository at this point in the history
  • Loading branch information
unadlib committed Jan 3, 2025
1 parent 99fac77 commit ba4e5bc
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 8 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
39 changes: 34 additions & 5 deletions src/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ import {
finalizeSetValue,
markFinalization,
finalizePatches,
isDraft,
skipFinalization,
} from './utils';
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';

const draftsCache = new WeakSet<object>();

let arrayHandling = false;

const proxyHandler: ProxyHandler<ProxyDraft> = {
get(target: ProxyDraft, key: string | number | symbol, receiver: any) {
const copy = target.copy?.[key];
Expand Down Expand Up @@ -88,6 +92,26 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {

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
Expand All @@ -103,7 +127,7 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
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],
Expand All @@ -122,6 +146,11 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
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) {
Expand Down Expand Up @@ -319,10 +348,10 @@ export function finalizeDraft<T>(
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);
Expand Down
5 changes: 4 additions & 1 deletion src/utils/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
} from './draft';
import { forEach } from './forEach';

export const skipFinalization = new WeakSet();

export function handleValue(
target: any,
handledSet: WeakSet<any>,
Expand All @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions test/performance/benchmark-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const MAX_ITERATIONS = 100;
}
console.timeEnd('immer:autoFreeze:nextAction');
}
console.log('---------------------------------');
{
setAutoFreeze(false);
const initialState = createInitialState();
Expand All @@ -207,7 +208,7 @@ const MAX_ITERATIONS = 100;
}
console.timeEnd('immer:nextAction');
}

console.log('---------------------------------');
{
const initialState = createInitialState();
console.time('mutative:autoFreeze');
Expand All @@ -220,7 +221,7 @@ const MAX_ITERATIONS = 100;
}
console.timeEnd('mutative:autoFreeze:nextAction');
}

console.log('---------------------------------');
{
const initialState = createInitialState();
console.time('vanilla');
Expand Down
181 changes: 181 additions & 0 deletions test/performance/benchmark-reducer1.mjs
Original file line number Diff line number Diff line change
@@ -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();
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==

[email protected]:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
Expand Down

0 comments on commit ba4e5bc

Please sign in to comment.