From 34b00c375430e33def13d563aad4c42933ea845c Mon Sep 17 00:00:00 2001 From: Michael Lin Date: Fri, 21 Jun 2024 02:43:35 +0800 Subject: [PATCH] perf(read): improve draft reading performance by caching the draft copy (#42) --- package.json | 1 + src/draft.ts | 8 ++ test/performance/read-draft/index.ts | 154 +++++++++++++++++++++ test/performance/read-draft/mockPhysics.ts | 88 ++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 test/performance/read-draft/index.ts create mode 100644 test/performance/read-draft/mockPhysics.ts diff --git a/package.json b/package.json index 564bc38..6a5992c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "benchmark:object": "NODE_ENV='production' ts-node test/performance/benchmark-object.ts", "benchmark:array": "NODE_ENV='production' ts-node test/performance/benchmark-array.ts", "benchmark:class": "NODE_ENV='production' ts-node test/performance/benchmark-class.ts", + "performance:read-only": "yarn build && NODE_ENV='production' ts-node test/performance/read-draft/index.ts", "performance:immer": "cd test/__immer_performance_tests__ && NODE_ENV='production' ts-node add-data.ts && NODE_ENV='production' ts-node todo.ts && NODE_ENV='production' ts-node incremental.ts", "performance:basic": "cd test/performance && NODE_ENV='production' ts-node index.ts", "performance:set-map": "cd test/performance && NODE_ENV='production' ts-node set-map.ts", diff --git a/src/draft.ts b/src/draft.ts index 6562d74..5390448 100644 --- a/src/draft.ts +++ b/src/draft.ts @@ -33,8 +33,15 @@ import { import { checkReadable } from './unsafe'; import { generatePatches } from './patch'; +const draftsCache = new WeakSet(); + const proxyHandler: ProxyHandler = { 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)) { + return copy; + } if (key === PROXY_DRAFT) return target; let markResult: any; if (target.options.mark) { @@ -249,6 +256,7 @@ export function createDraft(createDraftOptions: { proxyHandler ); finalities.revoke.push(revoke); + draftsCache.add(proxy); proxyDraft.proxy = proxy; if (parentDraft) { const target = parentDraft; diff --git a/test/performance/read-draft/index.ts b/test/performance/read-draft/index.ts new file mode 100644 index 0000000..c039be1 --- /dev/null +++ b/test/performance/read-draft/index.ts @@ -0,0 +1,154 @@ +/* eslint-disable prefer-template */ +// @ts-nocheck +import { produce } from 'immer'; +import { create } from '../../..'; +import { createTable, updateTable } from './mockPhysics'; + +const iterations = 5000; +const ballCount = 30; +const readOnly = true; + +function deepProxy(target, handler) { + const wrap = (target) => { + if (typeof target !== 'object' || target === null) { + return target; + } + return new Proxy(target, handler); + }; + return wrap(target); +} +let k = 0; +const x: WeakMap = new WeakMap(); + +// example usage +const handler = { + get(target, prop, receiver) { + k++; + const value = target[prop]; + if (typeof value !== 'object' || value === null) { + return value; + } + const f = x.get(target); + if (!f) { + x.set(target, new Map()); + } else { + const k = f.get(prop); + if (k) { + return k; + } + } + const g = new Proxy(value, handler); + x.get(target).set(prop, g); + return g; + }, +}; + +console.log(new Date()); +console.log( + 'Iterations=' + iterations + ' Balls=' + ballCount + ' ReadOnly=' + readOnly +); +console.log(); +let rawTable = createTable(ballCount); +let before = Date.now(); +for (let i = 0; i < iterations; i++) { + updateTable(rawTable, readOnly); +} +let after = Date.now(); +console.log( + 'RAW : ' + + iterations + + ' iterations @' + + (after - before) + + 'ms (' + + (after - before) / iterations + + ' per loop)' +); + +let rawCopyTable = createTable(ballCount); +before = Date.now(); +for (let i = 0; i < iterations; i++) { + rawCopyTable = JSON.parse( + JSON.stringify(updateTable(rawCopyTable, readOnly)) + ); +} +after = Date.now(); +console.log( + 'RAW+COPY: ' + + iterations + + ' iterations @' + + (after - before) + + 'ms (' + + (after - before) / iterations + + ' per loop)' +); + +let mutativeTable = createTable(ballCount); +before = Date.now(); +for (let i = 0; i < iterations; i++) { + const beforeTable = mutativeTable; + mutativeTable = create(mutativeTable, (draft) => { + updateTable(draft, readOnly); + }); + // if (beforeTable !== mutativeTable && readOnly) { + // console.log('ERROR - change detected'); + // // @ts-ignore + // process.exit(0); + // } +} +after = Date.now(); +console.log( + 'MUTATIVE: ' + + iterations + + ' iterations @' + + (after - before) + + 'ms (' + + (after - before) / iterations + + ' per loop)' +); + +let immerTable1 = createTable(ballCount); +before = Date.now(); +for (let i = 0; i < iterations; i++) { + const beforeTable = immerTable1; + // immerTable = produce(immerTable, (draft) => {}); + const deepProxiedObject = deepProxy(immerTable1, handler); + updateTable(deepProxiedObject, readOnly); + if (beforeTable !== immerTable1 && readOnly) { + console.log('ERROR - change detected'); + // @ts-ignore + process.exit(0); + } +} +after = Date.now(); +console.log( + 'RAW+PROXY: ' + + iterations + + ' iterations @' + + (after - before) + + 'ms (' + + (after - before) / iterations + + ' per loop)' +); + +let immerTable = createTable(ballCount); +before = Date.now(); +for (let i = 0; i < iterations; i++) { + const beforeTable = immerTable; + immerTable = produce(immerTable, (draft) => { + updateTable(draft, readOnly); + }); + if (beforeTable !== immerTable && readOnly) { + console.log('ERROR - change detected'); + process.exit(0); + } +} +after = Date.now(); +console.log( + 'IMMER : ' + + iterations + + ' iterations @' + + (after - before) + + 'ms (' + + (after - before) / iterations + + ' per loop)' +); diff --git a/test/performance/read-draft/mockPhysics.ts b/test/performance/read-draft/mockPhysics.ts new file mode 100644 index 0000000..73652aa --- /dev/null +++ b/test/performance/read-draft/mockPhysics.ts @@ -0,0 +1,88 @@ +// our pretend physics engine +interface Vec2 { + x: number; + y: number; +} + +interface Ball { + position: Vec2; + velocity: Vec2; + radius: number; + mass: number; + data?: any; +} + +interface Table { + balls: Ball[]; +} + +function createVector(x: number, y: number): Vec2 { + return { x, y }; +} + +function createBall(): Ball { + return { + position: createVector(Math.random() * 100, Math.random() * 100), + velocity: createVector(-1 + Math.random() * 2, -1 + Math.random() * 2), + radius: 10, + mass: 10, + data: null, + }; +} + +export function createTable(ballCount: number): Table { + const table: Table = { + balls: [], + }; + for (let i = 0; i < ballCount; i++) { + table.balls.push(createBall()); + } + + return table; +} + +export function updateTable(table: Table, readsOnly: boolean): Table { + // let i = 0; + for (const ball of table.balls) { + // i++; + if (readsOnly) { + // @ts-ignore + const result1 = ball.velocity.x + ball.velocity.y; + // @ts-ignore + const result2 = ball.position.x + ball.position.y; + } else { + ball.position.x += ball.velocity.x; + ball.position.y += ball.velocity.y; + ball.velocity.x *= 0.999; + ball.velocity.x *= 0.999; + } + } + + // @ts-ignore + for (const ballA of table.balls) { + // @ts-ignore + for (const ballB of table.balls) { + // i++; + if (ballA !== ballB) { + if (readsOnly) { + // @ts-ignore + const result1 = ballA.velocity.x + ballB.velocity.y; + // @ts-ignore + const result2 = ballA.position.x + ballB.position.y; + } else { + // perform some collision or other + const dx = ballB.position.x - ballA.position.y; + const dy = ballB.position.y - ballA.position.y; + const len = Math.sqrt(dx * dx + dy * dy); + if (len < ballB.radius + ballA.radius) { + // collision! + } + } + } + } + } + + // console.log('i', i); + + return table; +}