Skip to content

Commit f57e925

Browse files
authored
enhance(normalizr,endpoint): Reduce allocations in hot cache paths (#3879)
* enhance(normalizr,endpoint): Reduce allocations in hot cache paths Inline getCacheKey in GlobalCache to avoid eagerly creating both localCache and cycleCache Maps per entity type. Replace push(...spread) with an indexed for-loop when copying cached entity dependencies. Pre-create _removeSchema in Collection.CreateMover so normalizeMove no longer calls Object.create on every invocation, eliminating hidden class polymorphism that caused V8 "wrong call target" deoptimizations. Made-with: Cursor * docs: Add optimization rationale comments Made-with: Cursor * refactor: Extract getOrCreateLocalCache for readability Made-with: Cursor
1 parent 98a7831 commit f57e925

2 files changed

Lines changed: 36 additions & 21 deletions

File tree

packages/endpoint/src/schemas/Collection.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ function CreateMover<C extends CollectionSchema<any, any>>(
396396
addMerge: (existing: any, incoming: any) => any,
397397
removeMerge: (existing: any, incoming: any) => any,
398398
) {
399-
return Object.create(
399+
const mover = Object.create(
400400
collection,
401401
derivedProperties(
402402
collection,
@@ -412,8 +412,7 @@ function CreateMover<C extends CollectionSchema<any, any>>(
412412
) {
413413
return normalizeMove.call(
414414
this,
415-
addMerge,
416-
removeMerge,
415+
(this as any)._removeSchema,
417416
input,
418417
parent,
419418
key,
@@ -424,12 +423,17 @@ function CreateMover<C extends CollectionSchema<any, any>>(
424423
},
425424
),
426425
);
426+
// Pre-create the remove schema once so normalizeMove avoids
427+
// per-call Object.create (which causes V8 hidden-class polymorphism).
428+
mover._removeSchema = Object.create(mover, {
429+
merge: { value: removeMerge },
430+
});
431+
return mover;
427432
}
428433

429434
function normalizeMove(
430435
this: CollectionSchema<any, any>,
431-
addMerge: (existing: any, incoming: any) => any,
432-
removeMergeFunc: (existing: any, incoming: any) => any,
436+
removeSchema: CollectionSchema<any, any>,
433437
input: any,
434438
parent: any,
435439
key: string,
@@ -470,10 +474,6 @@ function normalizeMove(
470474
);
471475

472476
const lastArg = args[args.length - 1];
473-
const addSchema = Object.create(this, { merge: { value: addMerge } });
474-
const removeSchema = Object.create(this, {
475-
merge: { value: removeMergeFunc },
476-
});
477477
const collections = delegate.getEntities(this.key);
478478

479479
// Process each entity's collection membership individually
@@ -507,7 +507,8 @@ function normalizeMove(
507507
if (shouldRemove && !shouldAdd) {
508508
delegate.mergeEntity(removeSchema, collectionKey, value);
509509
} else if (shouldAdd && !shouldRemove) {
510-
delegate.mergeEntity(addSchema, collectionKey, value);
510+
// this.merge is already the addMerge function
511+
delegate.mergeEntity(this, collectionKey, value);
511512
}
512513
}
513514
}

packages/normalizr/src/memo/globalCache.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export default class GlobalCache implements Cache {
3939
computeValue: (localCacheKey: Map<string, any>) => void,
4040
): object | undefined | typeof INVALID {
4141
const key = schema.key;
42-
const { localCacheKey, cycleCacheKey } = this.getCacheKey(key);
42+
// cycleCache is deferred to the branch that actually needs it
43+
// to avoid unnecessary allocations.
44+
const localCacheKey = this.getOrCreateLocalCache(key);
4345

4446
if (!localCacheKey.get(pk)) {
4547
const globalCache: WeakDependencyMap<
@@ -54,12 +56,17 @@ export default class GlobalCache implements Cache {
5456
localCacheKey.set(pk, cacheValue.value);
5557
// TODO: can we store the cache values instead of tracking *all* their sources?
5658
// this is only used for setting endpoints cache correctly. if we got this far we will def need to set as we would have already tried getting it
57-
this.dependencies.push(...cacheValue.dependencies);
59+
// Indexed loop avoids spread-into-push overhead for large dep arrays
60+
const cdeps = cacheValue.dependencies;
61+
for (let i = 0; i < cdeps.length; i++) {
62+
this.dependencies.push(cdeps[i]);
63+
}
5864
return cacheValue.value;
5965
}
6066
// if we don't find in denormalize cache then do full denormalize
6167
else {
6268
const trackingIndex = this.dependencies.length;
69+
const cycleCacheKey = this.getOrCreateCycleCache(key);
6370
cycleCacheKey.set(pk, trackingIndex);
6471
this.dependencies.push({ path: { key, pk }, entity });
6572

@@ -85,8 +92,9 @@ export default class GlobalCache implements Cache {
8592
}
8693
}
8794
} else {
95+
const cycleCacheKey = this.cycleCache.get(key);
8896
// cycle detected
89-
if (cycleCacheKey.has(pk)) {
97+
if (cycleCacheKey?.has(pk)) {
9098
this.cycleIndex = cycleCacheKey.get(pk)!;
9199
} else {
92100
// with no cycle, globalCacheEntry will have already been set
@@ -96,16 +104,22 @@ export default class GlobalCache implements Cache {
96104
return localCacheKey.get(pk);
97105
}
98106

99-
private getCacheKey(key: string) {
100-
if (!this.localCache.has(key)) {
101-
this.localCache.set(key, new Map());
107+
private getOrCreateLocalCache(key: string): Map<string, any> {
108+
let localCacheKey = this.localCache.get(key);
109+
if (!localCacheKey) {
110+
localCacheKey = new Map();
111+
this.localCache.set(key, localCacheKey);
102112
}
103-
if (!this.cycleCache.has(key)) {
104-
this.cycleCache.set(key, new Map());
113+
return localCacheKey;
114+
}
115+
116+
private getOrCreateCycleCache(key: string): Map<string, number> {
117+
let cycleCacheKey = this.cycleCache.get(key);
118+
if (!cycleCacheKey) {
119+
cycleCacheKey = new Map();
120+
this.cycleCache.set(key, cycleCacheKey);
105121
}
106-
const localCacheKey = this.localCache.get(key)!;
107-
const cycleCacheKey = this.cycleCache.get(key)!;
108-
return { localCacheKey, cycleCacheKey };
122+
return cycleCacheKey;
109123
}
110124

111125
/** Cache varies based on input (=== aka reference) */

0 commit comments

Comments
 (0)