Skip to content

Commit dca63d5

Browse files
Optimize getter performance by caching memoization symbols
- Pre-compute and cache memoization symbols during FastGetBuilder construction - Eliminates repeated Symbol.for() calls identified as bottleneck in CPU profiles - Reduces symbol lookup overhead in hot path for read-only instance getters - Add getter-optimization benchmark for targeted performance validation - All existing tests pass, maintaining API compatibility Co-Authored-By: Harry Brundage <[email protected]>
1 parent 3f58722 commit dca63d5

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import findRoot from "find-root";
2+
import fs from "fs";
3+
import { FruitAisle } from "../spec/fixtures/FruitAisle";
4+
import { benchmarker } from "./benchmark";
5+
6+
const root = findRoot(__dirname);
7+
const fruitBasket = JSON.parse(fs.readFileSync(root + "/spec/fixtures/fruit-aisle-snapshot.json", "utf8"));
8+
9+
export default benchmarker(async (suite) => {
10+
const instances = Array.from({ length: 1000 }, () => FruitAisle.createReadOnly(fruitBasket));
11+
12+
return suite
13+
.add("symbol lookup overhead test", function () {
14+
for (const instance of instances) {
15+
instance.binCount;
16+
instance.firstBinType;
17+
instance.firstBinCount;
18+
}
19+
})
20+
.add("memoization cache hit test", function () {
21+
const shared = instances[0];
22+
for (let i = 0; i < 1000; i++) {
23+
shared.binCount;
24+
shared.firstBinType;
25+
shared.firstBinCount;
26+
}
27+
});
28+
});

src/fast-getter.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { $notYetMemoized, $readOnly } from "./symbols";
77
/** Assemble a function for getting the value of a readonly instance very quickly with static dispatch to properties */
88
export class FastGetBuilder {
99
memoizableProperties: string[];
10+
private memoSymbols: Map<string, symbol>;
11+
private snapshotSymbols: Map<string, symbol>;
1012

1113
constructor(
1214
metadatas: PropertyMetadata[],
@@ -23,16 +25,31 @@ export class FastGetBuilder {
2325
return descriptor.get !== undefined;
2426
})
2527
.map((metadata) => metadata.property);
28+
29+
this.memoSymbols = new Map();
30+
this.snapshotSymbols = new Map();
31+
for (const property of this.memoizableProperties) {
32+
this.memoSymbols.set(property, Symbol.for(this.memoSymbolName(property)));
33+
this.snapshotSymbols.set(property, Symbol.for(this.snapshottedViewInputSymbolName(property)));
34+
}
2635
}
2736

2837
memoSymbolName(property: string) {
2938
return `mqt/${property}-memo`;
3039
}
3140

41+
getMemoSymbol(property: string) {
42+
return this.memoSymbols.get(property)!;
43+
}
44+
3245
snapshottedViewInputSymbolName(property: string) {
3346
return `mqt/${property}-svi-memo`;
3447
}
3548

49+
getSnapshotSymbol(property: string) {
50+
return this.snapshotSymbols.get(property)!;
51+
}
52+
3653
outerClosureStatements(className: string) {
3754
return this.memoizableProperties
3855
.map(
@@ -46,13 +63,13 @@ export class FastGetBuilder {
4663

4764
buildViewGetter(metadata: ViewMetadata | SnapshottedViewMetadata, descriptor: PropertyDescriptor) {
4865
const property = metadata.property;
49-
const $memo = Symbol.for(this.memoSymbolName(property));
66+
const $memo = this.getMemoSymbol(property);
5067

5168
let source;
5269
let args;
5370

5471
if (metadata.type === "snapshotted-view" && metadata.options.createReadOnly) {
55-
const $snapshotValue = Symbol.for(this.snapshottedViewInputSymbolName(property));
72+
const $snapshotValue = this.getSnapshotSymbol(property);
5673

5774
// this snapshotted view has a hydrator, so we need a special view function for readonly instances that lazily hydrates the snapshotted value
5875
source = `

0 commit comments

Comments
 (0)