Skip to content

perf(array): improve array method perf #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b940b47
perf(array): improve array remove method perf
unadlib Jan 3, 2025
18c4c30
refactor(array): refactor array remove
unadlib Jan 4, 2025
36d624f
test(benchmark): update
unadlib Jan 4, 2025
542c07d
chore: update
unadlib Jan 4, 2025
61a76c2
refactor(array): update
unadlib Jan 4, 2025
39120e4
fix(array): fix issue
unadlib Jan 4, 2025
f2795dc
refactor(array): update
unadlib Jan 4, 2025
d63b463
fix(array): update
unadlib Jan 4, 2025
a19b152
test(test): fix testing
unadlib Jan 4, 2025
6103b6a
fix(array): fix array handler
unadlib Jan 12, 2025
51685cd
test(array): add array testing
unadlib Jan 13, 2025
aff7025
test(array): update
unadlib Jan 13, 2025
2cac9ec
fix(snapshots): update
unadlib Jan 13, 2025
0967533
refactor(array): update
unadlib Jan 13, 2025
252a441
refactor(array): refactor array method proxy
unadlib Mar 9, 2025
4236985
Revert "refactor(array): refactor array method proxy"
unadlib Mar 9, 2025
8eb94f5
test(array): add array testing
unadlib Mar 9, 2025
c3c88c5
test: remove testing
unadlib Mar 16, 2025
d42823a
test: update testing
unadlib Mar 16, 2025
6e442bd
refactor(array): imporve copyWithin
unadlib Mar 16, 2025
79277d4
fix(array): update
unadlib Mar 16, 2025
21a6746
feat(option): add enableOptimizedArray option
unadlib Mar 16, 2025
b77a884
test(array): add testing
unadlib Mar 16, 2025
ebdd311
test(benchamark): update
unadlib Mar 16, 2025
356fdd5
chore(type): fix import type
unadlib Mar 16, 2025
c14014a
docs(jsdoc): update
unadlib Mar 16, 2025
35d6452
fix(array): update
unadlib Mar 18, 2025
860714e
docs(array): update
unadlib Mar 23, 2025
be4400b
fix(patches): fix array patches
unadlib Mar 23, 2025
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: 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.34",
"prettier": "^3.3.3",
"quickchart-js": "^3.1.2",
"redux": "^5.0.1",
Expand Down
8 changes: 7 additions & 1 deletion src/apply.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Draft, Options, Patches, DraftType, Operation } from './interface';
import {
DraftType,
Operation,
type Draft,
type Options,
type Patches,
} from './interface';
import { deepClone, get, getType, isDraft, unescapePath } from './utils';
import { create } from './create';

Expand Down
2 changes: 1 addition & 1 deletion src/current.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Draft, DraftType, type ProxyDraft } from './interface';
import { DraftType, type Draft, type ProxyDraft } from './interface';
import {
forEach,
get,
Expand Down
86 changes: 73 additions & 13 deletions src/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
Finalities,
Patches,
ProxyDraft,
Options,
Operation,
DraftOptions,
} from './interface';
import { dataTypes, PROXY_DRAFT } from './constant';
import { mapHandler, mapHandlerKeys } from './map';
Expand All @@ -29,17 +29,22 @@ import {
finalizeSetValue,
markFinalization,
finalizePatches,
isDraft,
} from './utils';
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';

const draftsCache = new WeakSet<object>();
// The array methods that need to be handled by the draft.
// `sort` is not included, because array items may be modified by mutations in the sort function, it has to be drafted.
// `copyWithin` is not included, it would require implementing a complete check of array copy reference relationships,
// which might result in limited performance gains and increased maintenance complexity.
const proxyArrayMethods = ['splice', 'shift', 'unshift', 'reverse'];

const proxyHandler: ProxyHandler<ProxyDraft> = {
get(target: ProxyDraft, key: string | number | symbol, receiver: any) {
const copy = target.copy?.[key];
// Improve draft reading performance by caching the draft copy.
if (copy && draftsCache.has(copy)) {
if (copy && target.finalities.draftsCache.has(copy)) {
return copy;
}
if (key === PROXY_DRAFT) return target;
Expand All @@ -61,6 +66,15 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
}
const source = latest(target);
const skipFinalization = target.options.skipFinalization;

if (
skipFinalization &&
source[key] &&
target.finalities.draftsCache.has(source[key])
) {
return source[key];
}

if (source instanceof Map && mapHandlerKeys.includes(key as any)) {
if (key === 'size') {
Expand Down Expand Up @@ -88,9 +102,35 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {

if (!has(source, key)) {
const desc = getDescriptor(source, key);
const value = desc?.value;
if (
target.type === DraftType.Array &&
skipFinalization &&
proxyArrayMethods.includes(key as string)
) {
return function (this: any, ...args: any[]) {
let returnValue: any;
target.finalities.arrayHandling = true;
try {
returnValue = value.apply(this, args);
if (isDraftable(returnValue, target.options)) {
returnValue = createDraft({
original: returnValue,
parentDraft: undefined,
key: undefined,
finalities: target.finalities,
options: target.options,
});
}
return returnValue;
} finally {
target.finalities.arrayHandling = false;
}
};
}
return desc
? `value` in desc
? desc.value
? value
: // !case: support for getter
desc.get?.call(target.proxy)
: undefined;
Expand All @@ -103,10 +143,17 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
return value;
}
// Ensure that the assigned values are not drafted
if (value === peek(target.original, key)) {
if (
!target.finalities.arrayHandling &&
(value === peek(target.original, key) || skipFinalization?.has(value))
) {
const shouldSkip = skipFinalization?.has(value);
if (shouldSkip) {
skipFinalization!.delete(value);
}
ensureShallowCopy(target);
target.copy![key] = createDraft({
original: target.original[key],
original: shouldSkip ? target.copy![key] : target.original[key],
parentDraft: target,
key: target.type === DraftType.Array ? Number(key) : key,
finalities: target.finalities,
Expand All @@ -122,6 +169,15 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
return target.copy![key];
}
if (
target.finalities.arrayHandling &&
skipFinalization &&
!isDraft(value) &&
isDraftable(value)
) {
// !case: handle the case of assigning the original array item via array methods(`splice`, `shift``, `unshift`, `reverse`)
skipFinalization.add(value);
}
return value;
},
set(target: ProxyDraft, key: string | number | symbol, value: any) {
Expand Down Expand Up @@ -227,7 +283,7 @@ export function createDraft<T extends object>(createDraftOptions: {
parentDraft?: ProxyDraft | null;
key?: string | number | symbol;
finalities: Finalities;
options: Options<any, any>;
options: DraftOptions;
}): T {
const { original, parentDraft, key, finalities, options } =
createDraftOptions;
Expand Down Expand Up @@ -256,7 +312,7 @@ export function createDraft<T extends object>(createDraftOptions: {
proxyHandler
);
finalities.revoke.push(revoke);
draftsCache.add(proxy);
finalities.draftsCache.add(proxy);
proxyDraft.proxy = proxy;
if (parentDraft) {
const target = parentDraft;
Expand All @@ -274,7 +330,11 @@ export function createDraft<T extends object>(createDraftOptions: {
}
finalizeSetValue(proxyDraft);
finalizePatches(proxyDraft, generatePatches, patches, inversePatches);
if (__DEV__ && target.options.enableAutoFreeze) {
if (
__DEV__ &&
target.options.enableAutoFreeze &&
typeof updatedValue === 'object'
) {
target.options.updatedValues =
target.options.updatedValues ?? new WeakMap();
target.options.updatedValues.set(updatedValue, proxyDraft.original);
Expand Down Expand Up @@ -319,10 +379,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
10 changes: 6 additions & 4 deletions src/draftify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
import type {
DraftOptions,
Finalities,
Options,
Patches,
PatchesOptions,
Result,
Expand All @@ -12,15 +12,17 @@ import { dataTypes } from './constant';
export function draftify<
T extends object,
O extends PatchesOptions = false,
F extends boolean = false
F extends boolean = false,
>(
baseState: T,
options: Options<O, F>
options: DraftOptions
): [T, (returnedValue: [T] | []) => Result<T, O, F>] {
const finalities: Finalities = {
draft: [],
revoke: [],
handledSet: new WeakSet<any>(),
draftsCache: new WeakSet<object>(),
arrayHandling: false,
};
let patches: Patches | undefined;
let inversePatches: Patches | undefined;
Expand Down
79 changes: 48 additions & 31 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface Finalities {
draft: ((patches?: Patches, inversePatches?: Patches) => void)[];
revoke: (() => void)[];
handledSet: WeakSet<any>;
draftsCache: WeakSet<object>;
arrayHandling: boolean;
}

export interface ProxyDraft<T = any> {
Expand All @@ -42,7 +44,7 @@ export interface ProxyDraft<T = any> {
copy: T | null;
proxy: T | null;
finalities: Finalities;
options: Options<any, any> & { updatedValues?: WeakMap<any, any> };
options: DraftOptions;
parent?: ProxyDraft | null;
key?: string | number | symbol;
setMap?: Map<any, ProxyDraft>;
Expand All @@ -62,30 +64,30 @@ export type Patch<P extends PatchesOptions = any> = P extends {
path: string;
}
: P extends true | object
? IPatch & {
path: (string | number)[];
}
: IPatch & {
path: string | (string | number)[];
};
? IPatch & {
path: (string | number)[];
}
: IPatch & {
path: string | (string | number)[];
};

export type Patches<P extends PatchesOptions = any> = Patch<P>[];

export type Result<
T extends any,
O extends PatchesOptions,
F extends boolean
F extends boolean,
> = O extends true | object
? [F extends true ? Immutable<T> : T, Patches<O>, Patches<O>]
: F extends true
? Immutable<T>
: T;
? Immutable<T>
: T;

export type CreateResult<
T extends any,
O extends PatchesOptions,
F extends boolean,
R extends void | Promise<void> | T | Promise<T>
R extends void | Promise<void> | T | Promise<T>,
> = R extends Promise<void> | Promise<T>
? Promise<Result<T, O, F>>
: Result<T, O, F>;
Expand All @@ -99,8 +101,8 @@ export type Mark<O extends PatchesOptions, F extends boolean> = (
) => O extends true | object
? BaseMark
: F extends true
? BaseMark
: MarkWithCopy;
? BaseMark
: MarkWithCopy;

export interface Options<O extends PatchesOptions, F extends boolean> {
/**
Expand All @@ -122,6 +124,17 @@ export interface Options<O extends PatchesOptions, F extends boolean> {
mark?: Mark<O, F>;
}

export type DraftOptions = Options<any, any> & {
/**
* a collection for circular reference check
*/
updatedValues?: WeakMap<any, any>;
/**
* a collection for array item skip deep check
*/
skipFinalization?: WeakSet<any>;
};

export interface ExternalOptions<O extends PatchesOptions, F extends boolean> {
/**
* In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value.
Expand All @@ -140,6 +153,10 @@ export interface ExternalOptions<O extends PatchesOptions, F extends boolean> {
* And it can also return a shallow copy function(AutoFreeze and Patches should both be disabled).
*/
mark?: Mark<O, F>[] | Mark<O, F>;
/**
* Enable optimized array for improving performance.
*/
enableOptimizedArray?: boolean;
}

// Exclude `symbol`
Expand All @@ -154,8 +171,8 @@ export type IfAvailable<T, Fallback = void> = true | false extends (
)
? Fallback
: keyof T extends never
? Fallback
: T;
? Fallback
: T;
type WeakReferences =
| IfAvailable<WeakMap<any, any>>
| IfAvailable<WeakSet<any>>;
Expand All @@ -164,14 +181,14 @@ type AtomicObject = Function | Promise<any> | Date | RegExp;
export type Immutable<T> = T extends Primitive | AtomicObject
? T
: T extends IfAvailable<ReadonlyMap<infer K, infer V>>
? ImmutableMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? ImmutableSet<V>
: T extends WeakReferences
? T
: T extends object
? ImmutableObject<T>
: T;
? ImmutableMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? ImmutableSet<V>
: T extends WeakReferences
? T
: T extends object
? ImmutableObject<T>
: T;

type DraftedMap<K, V> = Map<K, Draft<V>>;
type DraftedSet<T> = Set<Draft<T>>;
Expand All @@ -182,11 +199,11 @@ type DraftedObject<T> = {
export type Draft<T> = T extends Primitive | AtomicObject
? T
: T extends IfAvailable<ReadonlyMap<infer K, infer V>>
? DraftedMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? DraftedSet<V>
: T extends WeakReferences
? T
: T extends object
? DraftedObject<T>
: T;
? DraftedMap<K, V>
: T extends IfAvailable<ReadonlySet<infer V>>
? DraftedSet<V>
: T extends WeakReferences
? T
: T extends object
? DraftedObject<T>
: T;
Loading