Skip to content

Commit 32cccdb

Browse files
authored
enhance: Improve performance by using Map (#3390)
* enhance: Improve performance by using Map * enhance: Use maps in networkmanager * enhance: Map in RestHelpers * enhance: queryKeys to Map * enhance: Map in addEntities * internal: Changeset * docs: Update benchmarks for new performance results * enhance: Interceptors that have args specified will still work, and warn about args. * enhance: Remove NM changes for now as they are breaking
1 parent e746344 commit 32cccdb

File tree

20 files changed

+219
-157
lines changed

20 files changed

+219
-157
lines changed

.changeset/evil-beans-attack.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@data-client/normalizr': patch
3+
'@data-client/rest': patch
4+
'@data-client/test': patch
5+
---
6+
7+
Improve performance by using Map() instead of Object for unbounded keys

.changeset/loud-rice-fry.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@data-client/test': patch
3+
---
4+
5+
Interceptors that have args specified will still work, and warn about args.
6+
7+
Example:
8+
9+
```typescript
10+
{
11+
endpoint: TodoResource.getList.push,
12+
args: [{ userId: '5' }, {}],
13+
response({ userId }, body) {
14+
return { id: Math.random(), userId, ...ensurePojo(body) };
15+
},
16+
}
17+
```
18+
19+
This is clearly an interceptor, but args were accidentally specified. Before
20+
this would make it not register, and TypeScript couldn't detect the issue.
21+
22+
Now this is treated as an interceptor (args ignored); and there is a console warning

docs/core/concepts/normalization.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,16 +391,16 @@ and up to 20x performance even after [mutation operations](../getting-started/mu
391391
xychart-beta
392392
title "Denormalize Single Entity"
393393
x-axis [normalizr, "Data Client", "Data Client (cached)"]
394-
y-axis "Operations per second" 0 --> 4875500
395-
bar [218500, 1079500, 4875500]
394+
y-axis "Operations per second" 0 --> 5618500
395+
bar [212500, 1288500, 5618500]
396396
```
397397

398398
```mermaid
399399
xychart-beta
400400
title "Denormalize Large List"
401401
x-axis [normalizr, "Data Client", "Data Client (cached)"]
402402
y-axis "Operations per second" 0 --> 12962
403-
bar [1165, 1807, 13168]
403+
bar [1151, 1807, 13182]
404404
```
405405

406406
</Grid>

examples/benchmark/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ Performance compared to normalizr package (higher is better):
2323

2424
| | no cache | with cache |
2525
| ------------------- | -------- | ---------- |
26-
| normalize (long) | 121% | 121% |
27-
| denormalize (long) | 158% | 1,262% |
28-
| denormalize (short) | 676% | 2,367% |
26+
| normalize (long) | 120% | 120% |
27+
| denormalize (long) | 158% | 1,250% |
28+
| denormalize (short) | 676% | 2,650% |
2929

30-
[Comparison done on a Ryzen 7950x; Ubuntu; Node 20.10.0]
30+
[Comparison done on a Ryzen 7950x; Ubuntu; Node 22.14.0]
3131

3232
Not only is denormalize faster, but it is more feature-rich as well.

packages/normalizr/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,9 @@ Available from [@data-client/endpoint](https://www.npmjs.com/package/@data-clien
313313

314314
| | no cache | with cache |
315315
| ------------------- | -------- | ---------- |
316-
| normalize (long) | 121% | 121% |
317-
| denormalize (long) | 158% | 1,262% |
318-
| denormalize (short) | 676% | 2,367% |
316+
| normalize (long) | 120% | 120% |
317+
| denormalize (long) | 158% | 1,250% |
318+
| denormalize (short) | 676% | 2,650% |
319319

320320
[View benchmark](https://github.com/reactive/data-client/blob/master/examples/benchmark)
321321

packages/normalizr/src/denormalize/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default interface Cache {
66
pk: string,
77
schema: EntityInterface,
88
entity: any,
9-
computeValue: (localCacheKey: Record<string, any>) => void,
9+
computeValue: (localCacheKey: Map<string, any>) => void,
1010
): object | undefined | symbol;
1111
getResults(
1212
input: any,

packages/normalizr/src/denormalize/localCache.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ import type { EntityInterface } from '../interface.js';
33
import type { EntityPath } from '../types.js';
44

55
export default class LocalCache implements Cache {
6-
private localCache: Record<string, Record<string, any>> = {};
6+
private localCache = new Map<string, Map<string, any>>();
77

88
getEntity(
99
pk: string,
1010
schema: EntityInterface,
1111
entity: any,
12-
computeValue: (localCacheKey: Record<string, any>) => void,
12+
computeValue: (localCacheKey: Map<string, any>) => void,
1313
): object | undefined | symbol {
1414
const key = schema.key;
15-
if (!(key in this.localCache)) {
16-
this.localCache[key] = Object.create(null);
15+
if (!this.localCache.has(key)) {
16+
this.localCache.set(key, new Map<string, any>());
1717
}
18-
const localCacheKey = this.localCache[key];
18+
const localCacheKey = this.localCache.get(key) as Map<string, any>;
1919

20-
if (!localCacheKey[pk]) {
20+
if (!localCacheKey.get(pk)) {
2121
computeValue(localCacheKey);
2222
}
23-
return localCacheKey[pk];
23+
return localCacheKey.get(pk);
2424
}
2525

2626
getResults(

packages/normalizr/src/denormalize/unvisit.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function unvisitEntity(
3535
// we're actually using this call to ensure we update the cache if a nested schema changes from `undefined`
3636
// this is because cache.getEntity adds this key,pk as a dependency of anything it is nested under
3737
return cache.getEntity(entityOrId, schema, UNDEF, localCacheKey => {
38-
localCacheKey[entityOrId] = undefined;
38+
localCacheKey.set(entityOrId, undefined);
3939
});
4040
}
4141

@@ -71,33 +71,34 @@ function unvisitEntity(
7171
}
7272

7373
function noCacheGetEntity(
74-
computeValue: (localCacheKey: Record<string, any>) => void,
74+
computeValue: (localCacheKey: Map<string, any>) => void,
7575
): object | undefined | symbol {
76-
const localCacheKey = {};
76+
const localCacheKey = new Map<string, any>();
7777
computeValue(localCacheKey);
7878

79-
return localCacheKey[''];
79+
return localCacheKey.get('');
8080
}
8181

8282
function unvisitEntityObject(
8383
entity: object,
8484
schema: EntityInterface<any>,
8585
unvisit: (schema: any, input: any) => any,
8686
pk: string,
87-
localCacheKey: Record<string, any>,
87+
localCacheKey: Map<string, any>,
8888
args: readonly any[],
8989
): void {
90-
const entityCopy = (localCacheKey[pk] =
90+
const entityCopy =
9191
isImmutable(entity) ?
9292
schema.createIfValid(entity.toObject())
93-
: schema.createIfValid(entity));
93+
: schema.createIfValid(entity);
94+
localCacheKey.set(pk, entityCopy);
9495

9596
if (entityCopy === undefined) {
9697
// undefined indicates we should suspense (perhaps failed validation)
97-
localCacheKey[pk] = INVALID;
98+
localCacheKey.set(pk, INVALID);
9899
} else {
99100
if (typeof schema.denormalize === 'function') {
100-
localCacheKey[pk] = schema.denormalize(entityCopy, args, unvisit);
101+
localCacheKey.set(pk, schema.denormalize(entityCopy, args, unvisit));
101102
}
102103
}
103104
}

packages/normalizr/src/memo/MemoCache.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import type {
1717
/** Singleton to store the memoization cache for denormalization methods */
1818
export default class MemoCache {
1919
/** Cache for every entity based on its dependencies and its own input */
20-
protected entities: EntityCache = {};
20+
protected entities: EntityCache = new Map();
2121
/** Caches the final denormalized form based on input, entities */
2222
protected endpoints: EndpointsCache = new WeakDependencyMap<EntityPath>();
2323
/** Caches the queryKey based on schema, args, and any used entities or indexes */
24-
protected queryKeys: Record<string, WeakDependencyMap<QueryPath>> = {};
24+
protected queryKeys: Map<string, WeakDependencyMap<QueryPath>> = new Map();
2525

2626
/** Compute denormalized form maintaining referential equality for same inputs */
2727
denormalize<S extends Schema>(
@@ -107,10 +107,14 @@ export default class MemoCache {
107107
return schema as any;
108108

109109
// cache lookup: argsKey -> schema -> ...touched indexes or entities
110-
if (!this.queryKeys[argsKey]) {
111-
this.queryKeys[argsKey] = new WeakDependencyMap<QueryPath>();
110+
if (!this.queryKeys.get(argsKey)) {
111+
this.queryKeys.set(argsKey, new WeakDependencyMap<QueryPath>());
112112
}
113-
const queryCache = this.queryKeys[argsKey];
113+
const queryCache = this.queryKeys.get(argsKey) as WeakDependencyMap<
114+
QueryPath,
115+
object,
116+
any
117+
>;
114118
const getEntity = createGetEntity(entities);
115119
const getIndex = createGetIndex(indexes);
116120
// eslint-disable-next-line prefer-const

packages/normalizr/src/memo/globalCache.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type { EntityPath } from '../types.js';
77

88
export default class GlobalCache implements Cache {
99
private dependencies: Dep<EntityPath>[] = [];
10-
private cycleCache: Record<string, Record<string, number>> = {};
10+
private cycleCache: Map<string, Map<string, number>> = new Map();
1111
private cycleIndex = -1;
12-
private localCache: Record<string, Record<string, any>> = {};
12+
private localCache: Map<string, Map<string, any>> = new Map();
1313

1414
declare private getCache: (
1515
pk: string,
@@ -33,12 +33,12 @@ export default class GlobalCache implements Cache {
3333
pk: string,
3434
schema: EntityInterface,
3535
entity: any,
36-
computeValue: (localCacheKey: Record<string, any>) => void,
36+
computeValue: (localCacheKey: Map<string, any>) => void,
3737
): object | undefined | symbol {
3838
const key = schema.key;
3939
const { localCacheKey, cycleCacheKey } = this.getCacheKey(key);
4040

41-
if (!localCacheKey[pk]) {
41+
if (!localCacheKey.get(pk)) {
4242
const globalCache: WeakDependencyMap<
4343
EntityPath,
4444
object,
@@ -48,7 +48,7 @@ export default class GlobalCache implements Cache {
4848
// TODO: what if this just returned the deps - then we don't need to store them
4949

5050
if (cachePath) {
51-
localCacheKey[pk] = cacheValue.value;
51+
localCacheKey.set(pk, cacheValue.value);
5252
// TODO: can we store the cache values instead of tracking *all* their sources?
5353
// 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
5454
this.dependencies.push(...cacheValue.dependencies);
@@ -57,22 +57,22 @@ export default class GlobalCache implements Cache {
5757
// if we don't find in denormalize cache then do full denormalize
5858
else {
5959
const trackingIndex = this.dependencies.length;
60-
cycleCacheKey[pk] = trackingIndex;
60+
cycleCacheKey.set(pk, trackingIndex);
6161
this.dependencies.push({ entity, path: { key, pk } });
6262

6363
/** NON-GLOBAL_CACHE CODE */
6464
computeValue(localCacheKey);
6565
/** /END NON-GLOBAL_CACHE CODE */
6666

67-
delete cycleCacheKey[pk];
67+
cycleCacheKey.delete(pk);
6868
// if in cycle, use the start of the cycle to track all deps
6969
// otherwise, we use our own trackingIndex
7070
const localKey = this.dependencies.slice(
7171
this.cycleIndex === -1 ? trackingIndex : this.cycleIndex,
7272
);
7373
const cacheValue: EntityCacheValue = {
7474
dependencies: localKey,
75-
value: localCacheKey[pk],
75+
value: localCacheKey.get(pk),
7676
};
7777
globalCache.set(localKey, cacheValue);
7878

@@ -83,25 +83,25 @@ export default class GlobalCache implements Cache {
8383
}
8484
} else {
8585
// cycle detected
86-
if (pk in cycleCacheKey) {
87-
this.cycleIndex = cycleCacheKey[pk];
86+
if (cycleCacheKey.has(pk)) {
87+
this.cycleIndex = cycleCacheKey.get(pk)!;
8888
} else {
8989
// with no cycle, globalCacheEntry will have already been set
9090
this.dependencies.push({ entity, path: { key, pk } });
9191
}
9292
}
93-
return localCacheKey[pk];
93+
return localCacheKey.get(pk);
9494
}
9595

9696
private getCacheKey(key: string) {
97-
if (!(key in this.localCache)) {
98-
this.localCache[key] = Object.create(null);
97+
if (!this.localCache.has(key)) {
98+
this.localCache.set(key, new Map());
9999
}
100-
if (!(key in this.cycleCache)) {
101-
this.cycleCache[key] = Object.create(null);
100+
if (!this.cycleCache.has(key)) {
101+
this.cycleCache.set(key, new Map());
102102
}
103-
const localCacheKey = this.localCache[key];
104-
const cycleCacheKey = this.cycleCache[key];
103+
const localCacheKey = this.localCache.get(key)!;
104+
const cycleCacheKey = this.cycleCache.get(key)!;
105105
return { localCacheKey, cycleCacheKey };
106106
}
107107

@@ -150,22 +150,31 @@ const getEntityCaches = (entityCache: EntityCache) => {
150150
// TODO: this should be based on a public interface
151151
const entityInstance: EntityInterface = (schema.cacheWith as any) ?? schema;
152152

153-
if (!(key in entityCache)) {
154-
entityCache[key] = Object.create(null);
153+
if (!entityCache.has(key)) {
154+
entityCache.set(key, new Map());
155155
}
156-
const entityCacheKey = entityCache[key];
157-
if (!entityCacheKey[pk])
158-
entityCacheKey[pk] = new WeakMap<
159-
EntityInterface,
160-
WeakDependencyMap<EntityPath, object, any>
161-
>();
162-
163-
let wem: WeakDependencyMap<EntityPath, object, any> = entityCacheKey[
164-
pk
165-
].get(entityInstance) as any;
156+
const entityCacheKey = entityCache.get(key)!;
157+
if (!entityCacheKey.get(pk))
158+
entityCacheKey.set(
159+
pk,
160+
new WeakMap<
161+
EntityInterface,
162+
WeakDependencyMap<EntityPath, object, any>
163+
>(),
164+
);
165+
166+
const entityCachePk = entityCacheKey.get(pk) as WeakMap<
167+
EntityInterface<any>,
168+
WeakDependencyMap<EntityPath, object, any>
169+
>;
170+
let wem = entityCachePk.get(entityInstance) as WeakDependencyMap<
171+
EntityPath,
172+
object,
173+
any
174+
>;
166175
if (!wem) {
167176
wem = new WeakDependencyMap<EntityPath, object, any>();
168-
entityCacheKey[pk].set(entityInstance, wem);
177+
entityCachePk.set(entityInstance, wem);
169178
}
170179

171180
return wem;

0 commit comments

Comments
 (0)