Skip to content

Commit

Permalink
perf(read): improve draft reading performance by caching the draft co…
Browse files Browse the repository at this point in the history
…py (#42)
  • Loading branch information
unadlib authored Jun 20, 2024
1 parent 37e332b commit 34b00c3
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ import {
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';

const draftsCache = new WeakSet<object>();

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)) {
return copy;
}
if (key === PROXY_DRAFT) return target;
let markResult: any;
if (target.options.mark) {
Expand Down Expand Up @@ -249,6 +256,7 @@ export function createDraft<T extends object>(createDraftOptions: {
proxyHandler
);
finalities.revoke.push(revoke);
draftsCache.add(proxy);
proxyDraft.proxy = proxy;
if (parentDraft) {
const target = parentDraft;
Expand Down
154 changes: 154 additions & 0 deletions test/performance/read-draft/index.ts
Original file line number Diff line number Diff line change
@@ -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<any, any> = 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)'
);
88 changes: 88 additions & 0 deletions test/performance/read-draft/mockPhysics.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 34b00c3

Please sign in to comment.