Skip to content

Commit dbe4c43

Browse files
committed
chore: reduce simple Map/Set ops
1 parent d84c5af commit dbe4c43

File tree

7 files changed

+182
-46
lines changed

7 files changed

+182
-46
lines changed

packages/build-config/src/debugging.ts

+12
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,15 @@ export const LOG_METRIC_COUNTS: boolean = false;
119119
* @public
120120
*/
121121
export const DEBUG_RELATIONSHIP_NOTIFICATIONS: boolean = false;
122+
123+
/**
124+
* A private flag to enable logging of the native Map/Set
125+
* constructor and method calls.
126+
*
127+
* EXTREMELY MALPERFORMANT
128+
*
129+
* LOG_METRIC_COUNTS must also be enabled.
130+
*
131+
* @typedoc
132+
*/
133+
export const __INTERNAL_LOG_NATIVE_MAP_SET_COUNTS: boolean = false;

packages/graph/src/-private/graph.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -781,19 +781,22 @@ function addPending(
781781
definition: UpgradedMeta,
782782
op: RemoteRelationshipOperation & { field: string }
783783
): void {
784-
const lc = (cache[definition.kind as 'hasMany' | 'belongsTo'] =
784+
const cacheForKind = (cache[definition.kind as 'hasMany' | 'belongsTo'] =
785785
cache[definition.kind as 'hasMany' | 'belongsTo'] || new Map<string, Map<string, RemoteRelationshipOperation[]>>());
786-
let lc2 = lc.get(definition.inverseType);
787-
if (!lc2) {
788-
lc2 = new Map<string, RemoteRelationshipOperation[]>();
789-
lc.set(definition.inverseType, lc2);
786+
787+
let cacheForType = cacheForKind.get(definition.inverseType);
788+
if (!cacheForType) {
789+
cacheForType = new Map<string, RemoteRelationshipOperation[]>();
790+
cacheForKind.set(definition.inverseType, cacheForType);
790791
}
791-
let arr = lc2.get(op.field);
792-
if (!arr) {
793-
arr = [];
794-
lc2.set(op.field, arr);
792+
793+
let cacheForField = cacheForType.get(op.field);
794+
if (!cacheForField) {
795+
cacheForField = [];
796+
cacheForType.set(op.field, cacheForField);
795797
}
796-
arr.push(op);
798+
799+
cacheForField.push(op);
797800
}
798801

799802
function isReordered(relationship: CollectionEdge): boolean {

packages/json-api/src/-private/cache.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1992,9 +1992,9 @@ function setupRelationships(
19921992
}
19931993
}
19941994

1995-
const RelationshipKinds = new Set(['hasMany', 'belongsTo', 'resource', 'collection']);
19961995
function isRelationship(field: FieldSchema): field is LegacyRelationshipSchema | CollectionField | ResourceField {
1997-
return RelationshipKinds.has(field.kind);
1996+
const { kind } = field;
1997+
return kind === 'hasMany' || kind === 'belongsTo' || kind === 'resource' || kind === 'collection';
19981998
}
19991999

20002000
function patchLocalAttributes(cached: CachedResource, changedRemoteKeys?: Set<string>): boolean {

packages/store/src/-private/managers/notification-manager.ts

+43-30
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import { _backburner } from '@ember/runloop';
66

77
import { LOG_METRIC_COUNTS, LOG_NOTIFICATIONS } from '@warp-drive/build-config/debugging';
8-
import { DEBUG } from '@warp-drive/build-config/env';
98
import { assert } from '@warp-drive/build-config/macros';
109
import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier';
1110

@@ -14,14 +13,14 @@ import { log } from '../debug/utils';
1413
import type { Store } from '../store-service';
1514

1615
export type UnsubscribeToken = object;
17-
let tokenId = 0;
1816

19-
const CacheOperations = new Set(['added', 'removed', 'state', 'updated', 'invalidated']);
2017
export type CacheOperation = 'added' | 'removed' | 'updated' | 'state';
2118
export type DocumentCacheOperation = 'invalidated' | 'added' | 'removed' | 'updated' | 'state';
2219

2320
function isCacheOperationValue(value: NotificationType | DocumentCacheOperation): value is DocumentCacheOperation {
24-
return CacheOperations.has(value);
21+
return (
22+
value === 'added' || value === 'state' || value === 'updated' || value === 'removed' || value === 'invalidated'
23+
);
2524
}
2625

2726
function runLoopIsFlushing(): boolean {
@@ -54,25 +53,40 @@ function count(label: string) {
5453
globalThis.__WarpDriveMetricCountData[label] = (globalThis.__WarpDriveMetricCountData[label] || 0) + 1;
5554
}
5655

56+
function asInternalToken(token: unknown): asserts token is {
57+
for: StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document';
58+
} & (NotificationCallback | ResourceOperationCallback | DocumentOperationCallback) {
59+
assert(`Expected a token with a 'for' property`, token && typeof token === 'function' && 'for' in token);
60+
}
61+
5762
function _unsubscribe(
58-
tokens: Map<UnsubscribeToken, StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document'>,
5963
token: UnsubscribeToken,
6064
cache: Map<
6165
'resource' | 'document' | StableDocumentIdentifier | StableRecordIdentifier,
62-
Map<UnsubscribeToken, NotificationCallback | ResourceOperationCallback | DocumentOperationCallback>
66+
Array<NotificationCallback | ResourceOperationCallback | DocumentOperationCallback>
6367
>
6468
) {
65-
const identifier = tokens.get(token);
69+
asInternalToken(token);
70+
const identifier = token.for;
6671
if (LOG_NOTIFICATIONS) {
6772
if (!identifier) {
6873
// eslint-disable-next-line no-console
6974
console.log('Passed unknown unsubscribe token to unsubscribe', identifier);
7075
}
7176
}
7277
if (identifier) {
73-
tokens.delete(token);
74-
const map = cache.get(identifier);
75-
map?.delete(token);
78+
const callbacks = cache.get(identifier);
79+
if (!callbacks) {
80+
return;
81+
}
82+
83+
const index = callbacks.indexOf(token);
84+
if (index === -1) {
85+
assert(`Cannot unsubscribe a token that is not subscribed`, index !== -1);
86+
return;
87+
}
88+
89+
callbacks.splice(index, 1);
7690
}
7791
}
7892

@@ -92,9 +106,8 @@ export default class NotificationManager {
92106
declare _buffered: Map<StableDocumentIdentifier | StableRecordIdentifier, [string, string | undefined][]>;
93107
declare _cache: Map<
94108
StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document',
95-
Map<UnsubscribeToken, NotificationCallback | ResourceOperationCallback | DocumentOperationCallback>
109+
Array<NotificationCallback | ResourceOperationCallback | DocumentOperationCallback>
96110
>;
97-
declare _tokens: Map<UnsubscribeToken, StableDocumentIdentifier | StableRecordIdentifier | 'resource' | 'document'>;
98111
declare _hasFlush: boolean;
99112
declare _onFlushCB?: () => void;
100113

@@ -104,7 +117,6 @@ export default class NotificationManager {
104117
this._buffered = new Map();
105118
this._hasFlush = false;
106119
this._cache = new Map();
107-
this._tokens = new Map();
108120
}
109121

110122
/**
@@ -148,17 +160,20 @@ export default class NotificationManager {
148160
isStableIdentifier(identifier) ||
149161
isDocumentIdentifier(identifier)
150162
);
151-
let map = this._cache.get(identifier);
152-
153-
if (!map) {
154-
map = new Map();
155-
this._cache.set(identifier, map);
163+
let callbacks = this._cache.get(identifier);
164+
assert(`expected to receive a valid callback`, typeof callback === 'function');
165+
assert(`cannot subscribe with the same callback twice`, !callbacks || !callbacks.includes(callback));
166+
// we use the callback as the cancellation token
167+
//@ts-expect-error
168+
callback.for = identifier;
169+
170+
if (!callbacks) {
171+
callbacks = [];
172+
this._cache.set(identifier, callbacks);
156173
}
157174

158-
const unsubToken = DEBUG ? { _tokenRef: tokenId++ } : {};
159-
map.set(unsubToken, callback);
160-
this._tokens.set(unsubToken, identifier);
161-
return unsubToken;
175+
callbacks.push(callback);
176+
return callback;
162177
}
163178

164179
/**
@@ -170,7 +185,7 @@ export default class NotificationManager {
170185
*/
171186
unsubscribe(token: UnsubscribeToken) {
172187
if (!this.isDestroyed) {
173-
_unsubscribe(this._tokens, token, this._cache);
188+
_unsubscribe(token, this._cache);
174189
}
175190
}
176191

@@ -210,7 +225,7 @@ export default class NotificationManager {
210225
return false;
211226
}
212227

213-
const hasSubscribers = Boolean(this._cache.get(identifier)?.size);
228+
const hasSubscribers = Boolean(this._cache.get(identifier)?.length);
214229

215230
if (isCacheOperationValue(value) || hasSubscribers) {
216231
let buffer = this._buffered.get(identifier);
@@ -302,8 +317,7 @@ export default class NotificationManager {
302317

303318
// TODO for documents this will need to switch based on Identifier kind
304319
if (isCacheOperationValue(value)) {
305-
const callbackMap = this._cache.get(isDocumentIdentifier(identifier) ? 'document' : 'resource') as Map<
306-
UnsubscribeToken,
320+
const callbackMap = this._cache.get(isDocumentIdentifier(identifier) ? 'document' : 'resource') as Array<
307321
ResourceOperationCallback | DocumentOperationCallback
308322
>;
309323

@@ -314,11 +328,11 @@ export default class NotificationManager {
314328
}
315329
}
316330

317-
const callbackMap = this._cache.get(identifier);
318-
if (!callbackMap || !callbackMap.size) {
331+
const callbacks = this._cache.get(identifier);
332+
if (!callbacks || !callbacks.length) {
319333
return false;
320334
}
321-
callbackMap.forEach((cb) => {
335+
callbacks.forEach((cb) => {
322336
// @ts-expect-error overload doesn't narrow within body
323337
cb(identifier, value, key);
324338
});
@@ -327,7 +341,6 @@ export default class NotificationManager {
327341

328342
destroy() {
329343
this.isDestroyed = true;
330-
this._tokens.clear();
331344
this._cache.clear();
332345
}
333346
}

packages/store/src/-private/store-service.ts

+107-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { dependencySatisfies, importSync, macroCondition } from '@embroider/macr
88

99
import type RequestManager from '@ember-data/request';
1010
import type { Future } from '@ember-data/request';
11-
import { LOG_METRIC_COUNTS, LOG_PAYLOADS, LOG_REQUESTS } from '@warp-drive/build-config/debugging';
11+
import {
12+
__INTERNAL_LOG_NATIVE_MAP_SET_COUNTS,
13+
LOG_METRIC_COUNTS,
14+
LOG_PAYLOADS,
15+
LOG_REQUESTS,
16+
} from '@warp-drive/build-config/debugging';
1217
import {
1318
DEPRECATE_STORE_EXTENDS_EMBER_OBJECT,
1419
ENABLE_LEGACY_SCHEMA_SERVICE,
@@ -70,16 +75,114 @@ globalThis.setWarpDriveLogging = setLogging;
7075
globalThis.getWarpDriveRuntimeConfig = getRuntimeConfig;
7176

7277
if (LOG_METRIC_COUNTS) {
73-
// @ts-expect-error
78+
// @ts-expect-error adding to globalThis
7479
// eslint-disable-next-line
7580
globalThis.__WarpDriveMetricCountData = globalThis.__WarpDriveMetricCountData || {};
7681

77-
// @ts-expect-error
82+
// @ts-expect-error adding to globalThis
7883
globalThis.getWarpDriveMetricCounts = () => {
7984
// @ts-expect-error
8085
// eslint-disable-next-line
81-
return globalThis.__WarpDriveMetricCountData;
86+
return structuredClone(globalThis.__WarpDriveMetricCountData);
87+
};
88+
89+
// @ts-expect-error adding to globalThis
90+
globalThis.resetWarpDriveMetricCounts = () => {
91+
// @ts-expect-error
92+
globalThis.__WarpDriveMetricCountData = {};
8293
};
94+
95+
if (__INTERNAL_LOG_NATIVE_MAP_SET_COUNTS) {
96+
// @ts-expect-error adding to globalThis
97+
globalThis.__primitiveInstanceId = 0;
98+
99+
function interceptAndLog(
100+
klassName: 'Set' | 'Map' | 'WeakSet' | 'WeakMap',
101+
methodName: 'add' | 'set' | 'delete' | 'has' | 'get' | 'constructor'
102+
) {
103+
const klass = globalThis[klassName];
104+
105+
if (methodName === 'constructor') {
106+
const instantiationLabel = `new ${klassName}()`;
107+
// @ts-expect-error
108+
globalThis[klassName] = class extends klass {
109+
// @ts-expect-error
110+
constructor(...args) {
111+
// eslint-disable-next-line
112+
super(...args);
113+
// @ts-expect-error
114+
115+
const instanceId = globalThis.__primitiveInstanceId++;
116+
// @ts-expect-error
117+
// eslint-disable-next-line
118+
globalThis.__WarpDriveMetricCountData[instantiationLabel] =
119+
// @ts-expect-error
120+
// eslint-disable-next-line
121+
(globalThis.__WarpDriveMetricCountData[instantiationLabel] || 0) + 1;
122+
// @ts-expect-error
123+
this.instanceName = `${klassName}:${instanceId} - ${new Error().stack?.split('\n')[2]}`;
124+
}
125+
};
126+
} else {
127+
// @ts-expect-error
128+
// eslint-disable-next-line
129+
const original = klass.prototype[methodName];
130+
const logName = `${klassName}.${methodName}`;
131+
132+
// @ts-expect-error
133+
klass.prototype[methodName] = function (...args) {
134+
// @ts-expect-error
135+
// eslint-disable-next-line
136+
globalThis.__WarpDriveMetricCountData[logName] = (globalThis.__WarpDriveMetricCountData[logName] || 0) + 1;
137+
// @ts-expect-error
138+
const { instanceName } = this;
139+
if (!instanceName) {
140+
// @ts-expect-error
141+
const instanceId = globalThis.__primitiveInstanceId++;
142+
// @ts-expect-error
143+
this.instanceName = `${klassName}.${methodName}:${instanceId} - ${new Error().stack?.split('\n')[2]}`;
144+
}
145+
const instanceLogName = `${logName} (${instanceName})`;
146+
// @ts-expect-error
147+
// eslint-disable-next-line
148+
globalThis.__WarpDriveMetricCountData[instanceLogName] =
149+
// @ts-expect-error
150+
// eslint-disable-next-line
151+
(globalThis.__WarpDriveMetricCountData[instanceLogName] || 0) + 1;
152+
// eslint-disable-next-line
153+
return original.apply(this, args);
154+
};
155+
}
156+
}
157+
158+
interceptAndLog('Set', 'constructor');
159+
interceptAndLog('Set', 'add');
160+
interceptAndLog('Set', 'delete');
161+
interceptAndLog('Set', 'has');
162+
interceptAndLog('Set', 'set');
163+
interceptAndLog('Set', 'get');
164+
165+
interceptAndLog('Map', 'constructor');
166+
interceptAndLog('Map', 'set');
167+
interceptAndLog('Map', 'delete');
168+
interceptAndLog('Map', 'has');
169+
interceptAndLog('Map', 'add');
170+
interceptAndLog('Map', 'get');
171+
172+
interceptAndLog('WeakSet', 'constructor');
173+
interceptAndLog('WeakSet', 'add');
174+
interceptAndLog('WeakSet', 'delete');
175+
interceptAndLog('WeakSet', 'has');
176+
interceptAndLog('WeakSet', 'set');
177+
interceptAndLog('WeakSet', 'get');
178+
179+
interceptAndLog('WeakMap', 'constructor');
180+
interceptAndLog('WeakMap', 'set');
181+
interceptAndLog('WeakMap', 'delete');
182+
interceptAndLog('WeakMap', 'has');
183+
interceptAndLog('WeakMap', 'add');
184+
interceptAndLog('WeakMap', 'get');
185+
}
83186
}
84187

85188
export { storeFor };

tests/performance/app/app.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// uncomment this to install Map/Set instrumentation
2+
// prior to app boot
3+
// import './services/store';
4+
15
import Application from '@ember/application';
26

37
import compatModules from '@embroider/virtual/compat-modules';

tests/performance/ember-cli-build.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = async function (defaults) {
1414
// LOG_NOTIFICATIONS: true,
1515
// LOG_INSTANCE_CACHE: true,
1616
// LOG_METRIC_COUNTS: true,
17+
// __INTERNAL_LOG_NATIVE_MAP_SET_COUNTS: true,
1718
// DEBUG_RELATIONSHIP_NOTIFICATIONS: true,
1819
},
1920
});

0 commit comments

Comments
 (0)