Skip to content
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

perf(array): improve array method perf #75

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
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
67 changes: 57 additions & 10 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,12 +29,17 @@ import {
finalizeSetValue,
markFinalization,
finalizePatches,
isDraft,
} from './utils';
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';

const draftsCache = new WeakSet<object>();

let arrayHandling = false;

const proxyArrayMethods = ['splice', 'shift', 'unshift', 'reverse'];

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

if (!has(source, key)) {
const desc = getDescriptor(source, key);
const value = desc?.value;
if (
target.type === DraftType.Array &&
proxyArrayMethods.includes(key as string)
) {
return function (this: any, ...args: any[]) {
let returnValue: any;
arrayHandling = true;
try {
returnValue = value.apply(this, args);
if (isDraftable(returnValue)) {
returnValue = createDraft({
original: returnValue,
parentDraft: target,
key: undefined,
finalities: target.finalities,
options: target.options,
});
// TODO: support for custom shallow copy function;
}
return returnValue;
} finally {
arrayHandling = false;
}
};
}
return desc
? `value` in desc
? desc.value
? value
: // !case: support for getter
desc.get?.call(target.proxy)
: undefined;
Expand All @@ -103,10 +134,18 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
return value;
}
// Ensure that the assigned values are not drafted
if (value === peek(target.original, key)) {
if (
!arrayHandling &&
(value === peek(target.original, key) ||
target.options.skipFinalization.has(value))
) {
const shouldSkip = target.options.skipFinalization.has(value);
if (shouldSkip) {
target.options.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 +161,10 @@ const proxyHandler: ProxyHandler<ProxyDraft> = {
}
return target.copy![key];
}
if (arrayHandling && !isDraft(value) && isDraftable(value)) {
// !case: handle the case of assigning the original array item via array methods(`splice`, `shift``, `unshift`, `reverse`)
target.options.skipFinalization.add(value);
}
return value;
},
set(target: ProxyDraft, key: string | number | symbol, value: any) {
Expand Down Expand Up @@ -227,7 +270,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 @@ -274,7 +317,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 +366,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
6 changes: 3 additions & 3 deletions src/draftify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
DraftOptions,
Finalities,
Options,
Patches,
PatchesOptions,
Result,
Expand All @@ -12,10 +12,10 @@ 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: [],
Expand Down
73 changes: 42 additions & 31 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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 +62,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 +99,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 +122,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 Down Expand Up @@ -154,8 +165,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 +175,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 +193,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;
12 changes: 7 additions & 5 deletions src/makeCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExternalOptions,
PatchesOptions,
Result,
DraftOptions,
} from './interface';
import { draftify } from './draftify';
import {
Expand All @@ -20,15 +21,15 @@ import { RAW_RETURN_SYMBOL, dataTypes } from './constant';

type MakeCreator = <
_F extends boolean = false,
_O extends PatchesOptions = false
_O extends PatchesOptions = false,
>(
options?: ExternalOptions<_O, _F>
) => {
<
T extends any,
F extends boolean = _F,
O extends PatchesOptions = _O,
R extends void | Promise<void> | T | Promise<T> = void
R extends void | Promise<void> | T | Promise<T> = void,
>(
base: T,
mutate: (draft: Draft<T>) => R,
Expand All @@ -38,7 +39,7 @@ type MakeCreator = <
T extends any,
F extends boolean = _F,
O extends PatchesOptions = _O,
R extends void | Promise<void> = void
R extends void | Promise<void> = void,
>(
base: T,
mutate: (draft: T) => R,
Expand All @@ -49,7 +50,7 @@ type MakeCreator = <
P extends any[] = [],
F extends boolean = _F,
O extends PatchesOptions = _O,
R extends void | Promise<void> = void
R extends void | Promise<void> = void,
>(
mutate: (draft: Draft<T>, ...args: P) => R,
options?: ExternalOptions<O, F>
Expand Down Expand Up @@ -143,11 +144,12 @@ export const makeCreator: MakeCreator = (arg) => {
const enablePatches = options.enablePatches ?? false;
const strict = options.strict ?? false;
const enableAutoFreeze = options.enableAutoFreeze ?? false;
const _options: Options<any, any> = {
const _options: DraftOptions = {
enableAutoFreeze,
mark,
strict,
enablePatches,
skipFinalization: new WeakSet(),
};
if (
!isDraftable(state, _options) &&
Expand Down
5 changes: 4 additions & 1 deletion src/utils/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export function handleValue(
isDraft(target) ||
!isDraftable(target, options) ||
handledSet.has(target) ||
Object.isFrozen(target)
Object.isFrozen(target) ||
options!.skipFinalization!.has(target)
// It should skip the finalization process
// This can avoid unnecessary deep traversal, as these objects are non-draft and do not contain draft in their deep object.
)
return;
const isSet = target instanceof Set;
Expand Down
Loading
Loading