Skip to content

Commit ad65328

Browse files
Juan Tejadafacebook-github-bot
authored andcommitted
Refactor notifying store subscriptions for better consistency update perf (under feature flag)
Reviewed By: josephsavona Differential Revision: D24966863 fbshipit-source-id: d22a614dcbc3e457e39bc79185e03803724223bf
1 parent d64b579 commit ad65328

File tree

6 files changed

+1028
-8
lines changed

6 files changed

+1028
-8
lines changed

packages/relay-runtime/store/RelayModernStore.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,20 @@
1313
'use strict';
1414

1515
const DataChecker = require('./DataChecker');
16+
const RelayFeatureFlags = require('../util/RelayFeatureFlags');
1617
const RelayModernRecord = require('./RelayModernRecord');
1718
const RelayOptimisticRecordSource = require('./RelayOptimisticRecordSource');
1819
const RelayProfiler = require('../util/RelayProfiler');
1920
const RelayReader = require('./RelayReader');
2021
const RelayReferenceMarker = require('./RelayReferenceMarker');
2122
const RelayStoreReactFlightUtils = require('./RelayStoreReactFlightUtils');
2223
const RelayStoreSubscriptions = require('./RelayStoreSubscriptions');
24+
const RelayStoreSubscriptionsUsingMapByID = require('./RelayStoreSubscriptionsUsingMapByID');
2325
const RelayStoreUtils = require('./RelayStoreUtils');
2426

2527
const deepFreeze = require('../util/deepFreeze');
2628
const defaultGetDataID = require('./defaultGetDataID');
27-
const hasOverlappingIDs = require('./hasOverlappingIDs');
2829
const invariant = require('invariant');
29-
const recycleNodesInto = require('../util/recycleNodesInto');
3030
const resolveImmediate = require('../util/resolveImmediate');
3131

3232
const {ROOT_ID, ROOT_TYPE} = require('./RelayStoreUtils');
@@ -47,6 +47,7 @@ import type {
4747
SingularReaderSelector,
4848
Snapshot,
4949
Store,
50+
StoreSubscriptions,
5051
UpdatedRecords,
5152
} from './RelayStoreTypes';
5253

@@ -100,7 +101,7 @@ class RelayModernStore implements Store {
100101
|},
101102
>;
102103
_shouldScheduleGC: boolean;
103-
_storeSubscriptions: RelayStoreSubscriptions;
104+
_storeSubscriptions: StoreSubscriptions;
104105
_updatedRecordIDs: UpdatedRecords;
105106

106107
constructor(
@@ -143,7 +144,10 @@ class RelayModernStore implements Store {
143144
this._releaseBuffer = [];
144145
this._roots = new Map();
145146
this._shouldScheduleGC = false;
146-
this._storeSubscriptions = new RelayStoreSubscriptions();
147+
this._storeSubscriptions =
148+
RelayFeatureFlags.ENABLE_STORE_SUBSCRIPTIONS_REFACTOR === true
149+
? new RelayStoreSubscriptionsUsingMapByID()
150+
: new RelayStoreSubscriptions();
147151
this._updatedRecordIDs = {};
148152

149153
initializeRecordSource(this._recordSource);

packages/relay-runtime/store/RelayStoreSubscriptions.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,18 @@ import type {
2424
RecordSource,
2525
RequestDescriptor,
2626
Snapshot,
27+
StoreSubscriptions,
2728
UpdatedRecords,
2829
} from './RelayStoreTypes';
2930

30-
export type Subscription = {|
31+
type Subscription = {|
3132
callback: (snapshot: Snapshot) => void,
3233
snapshot: Snapshot,
3334
stale: boolean,
3435
backup: ?Snapshot,
3536
|};
3637

37-
class RelayStoreSubscriptions {
38+
class RelayStoreSubscriptions implements StoreSubscriptions {
3839
_subscriptions: Set<Subscription>;
3940

4041
constructor() {
@@ -119,8 +120,14 @@ class RelayStoreSubscriptions {
119120
});
120121
}
121122

122-
// Returns the owner (RequestDescriptor) if the subscription was affected by the
123-
// latest update, or null if it was not affected.
123+
/**
124+
* Notifies the callback for the subscription if the data for the associated
125+
* snapshot has changed.
126+
* Additionally, updates the subscription snapshot with the latest snapshot,
127+
* and marks it as not stale.
128+
* Returns the owner (RequestDescriptor) if the subscription was affected by the
129+
* latest update, or null if it was not affected.
130+
*/
124131
_updateSubscription(
125132
source: RecordSource,
126133
subscription: Subscription,
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
// flowlint ambiguous-object-type:error
12+
13+
'use strict';
14+
15+
const RelayReader = require('./RelayReader');
16+
17+
const deepFreeze = require('../util/deepFreeze');
18+
const recycleNodesInto = require('../util/recycleNodesInto');
19+
20+
import type {DataID, Disposable} from '../util/RelayRuntimeTypes';
21+
import type {
22+
RecordMap,
23+
RecordSource,
24+
RequestDescriptor,
25+
Snapshot,
26+
StoreSubscriptions,
27+
UpdatedRecords,
28+
} from './RelayStoreTypes';
29+
30+
type Subscription = {|
31+
backup: ?Snapshot,
32+
callback: (snapshot: Snapshot) => void,
33+
notifiedRevision: number,
34+
snapshot: Snapshot,
35+
snapshotRevision: number,
36+
|};
37+
38+
class RelayStoreSubscriptionsUsingMapByID implements StoreSubscriptions {
39+
_notifiedRevision: number;
40+
_snapshotRevision: number;
41+
_subscriptionsByDataId: Map<DataID, Set<Subscription>>;
42+
_staleSubscriptions: Set<Subscription>;
43+
44+
constructor() {
45+
this._notifiedRevision = 0;
46+
this._snapshotRevision = 0;
47+
this._subscriptionsByDataId = new Map();
48+
this._staleSubscriptions = new Set();
49+
}
50+
51+
subscribe(
52+
snapshot: Snapshot,
53+
callback: (snapshot: Snapshot) => void,
54+
): Disposable {
55+
const subscription = {
56+
backup: null,
57+
callback,
58+
notifiedRevision: this._notifiedRevision,
59+
snapshotRevision: this._snapshotRevision,
60+
snapshot,
61+
};
62+
const dispose = () => {
63+
for (const dataId in snapshot.seenRecords) {
64+
const subscriptionsForDataId = this._subscriptionsByDataId.get(dataId);
65+
if (subscriptionsForDataId != null) {
66+
subscriptionsForDataId.delete(subscription);
67+
if (subscriptionsForDataId.size === 0) {
68+
this._subscriptionsByDataId.delete(dataId);
69+
}
70+
}
71+
}
72+
};
73+
74+
for (const dataId in snapshot.seenRecords) {
75+
const subscriptionsForDataId = this._subscriptionsByDataId.get(dataId);
76+
if (subscriptionsForDataId != null) {
77+
subscriptionsForDataId.add(subscription);
78+
} else {
79+
this._subscriptionsByDataId.set(dataId, new Set([subscription]));
80+
}
81+
}
82+
83+
return {dispose};
84+
}
85+
86+
snapshotSubscriptions(source: RecordSource) {
87+
this._snapshotRevision++;
88+
this._subscriptionsByDataId.forEach(subscriptions => {
89+
subscriptions.forEach(subscription => {
90+
if (subscription.snapshotRevision === this._snapshotRevision) {
91+
return;
92+
}
93+
subscription.snapshotRevision = this._snapshotRevision;
94+
95+
// Backup occurs after writing a new "final" payload(s) and before (re)applying
96+
// optimistic changes. Each subscription's `snapshot` represents what was *last
97+
// published to the subscriber*, which notably may include previous optimistic
98+
// updates. Therefore a subscription can be in any of the following states:
99+
// - stale=true: This subscription was restored to a different value than
100+
// `snapshot`. That means this subscription has changes relative to its base,
101+
// but its base has changed (we just applied a final payload): recompute
102+
// a backup so that we can later restore to the state the subscription
103+
// should be in.
104+
// - stale=false: This subscription was restored to the same value than
105+
// `snapshot`. That means this subscription does *not* have changes relative
106+
// to its base, so the current `snapshot` is valid to use as a backup.
107+
if (!this._staleSubscriptions.has(subscription)) {
108+
subscription.backup = subscription.snapshot;
109+
return;
110+
}
111+
const snapshot = subscription.snapshot;
112+
const backup = RelayReader.read(source, snapshot.selector);
113+
const nextData = recycleNodesInto(snapshot.data, backup.data);
114+
(backup: $FlowFixMe).data = nextData; // backup owns the snapshot and can safely mutate
115+
subscription.backup = backup;
116+
});
117+
});
118+
}
119+
120+
restoreSubscriptions() {
121+
this._snapshotRevision++;
122+
this._subscriptionsByDataId.forEach(subscriptions => {
123+
subscriptions.forEach(subscription => {
124+
if (subscription.snapshotRevision === this._snapshotRevision) {
125+
return;
126+
}
127+
subscription.snapshotRevision = this._snapshotRevision;
128+
129+
const backup = subscription.backup;
130+
subscription.backup = null;
131+
if (backup) {
132+
if (backup.data !== subscription.snapshot.data) {
133+
this._staleSubscriptions.add(subscription);
134+
}
135+
const prevSeenRecords = subscription.snapshot.seenRecords;
136+
subscription.snapshot = {
137+
data: subscription.snapshot.data,
138+
isMissingData: backup.isMissingData,
139+
seenRecords: backup.seenRecords,
140+
selector: backup.selector,
141+
missingRequiredFields: backup.missingRequiredFields,
142+
};
143+
this._updateSubscriptionsMap(subscription, prevSeenRecords);
144+
} else {
145+
this._staleSubscriptions.add(subscription);
146+
}
147+
});
148+
});
149+
}
150+
151+
updateSubscriptions(
152+
source: RecordSource,
153+
updatedRecordIDs: UpdatedRecords,
154+
updatedOwners: Array<RequestDescriptor>,
155+
) {
156+
this._notifiedRevision++;
157+
Object.keys(updatedRecordIDs).forEach(updatedRecordId => {
158+
const subcriptionsForDataId = this._subscriptionsByDataId.get(
159+
updatedRecordId,
160+
);
161+
if (subcriptionsForDataId == null) {
162+
return;
163+
}
164+
subcriptionsForDataId.forEach(subscription => {
165+
if (subscription.notifiedRevision === this._notifiedRevision) {
166+
return;
167+
}
168+
const owner = this._updateSubscription(source, subscription, false);
169+
if (owner != null) {
170+
updatedOwners.push(owner);
171+
}
172+
});
173+
});
174+
this._staleSubscriptions.forEach(subscription => {
175+
if (subscription.notifiedRevision === this._notifiedRevision) {
176+
return;
177+
}
178+
const owner = this._updateSubscription(source, subscription, true);
179+
if (owner != null) {
180+
updatedOwners.push(owner);
181+
}
182+
});
183+
this._staleSubscriptions.clear();
184+
}
185+
186+
/**
187+
* Notifies the callback for the subscription if the data for the associated
188+
* snapshot has changed.
189+
* Additionally, updates the subscription snapshot with the latest snapshot,
190+
* amarks it as not stale, and updates the subscription tracking for any
191+
* any new ids observed in the latest data snapshot.
192+
* Returns the owner (RequestDescriptor) if the subscription was affected by the
193+
* latest update, or null if it was not affected.
194+
*/
195+
_updateSubscription(
196+
source: RecordSource,
197+
subscription: Subscription,
198+
stale: boolean,
199+
): ?RequestDescriptor {
200+
const {backup, callback, snapshot} = subscription;
201+
let nextSnapshot: Snapshot =
202+
stale && backup != null
203+
? backup
204+
: RelayReader.read(source, snapshot.selector);
205+
const nextData = recycleNodesInto(snapshot.data, nextSnapshot.data);
206+
nextSnapshot = ({
207+
data: nextData,
208+
isMissingData: nextSnapshot.isMissingData,
209+
seenRecords: nextSnapshot.seenRecords,
210+
selector: nextSnapshot.selector,
211+
missingRequiredFields: nextSnapshot.missingRequiredFields,
212+
}: Snapshot);
213+
if (__DEV__) {
214+
deepFreeze(nextSnapshot);
215+
}
216+
217+
const prevSeenRecords = subscription.snapshot.seenRecords;
218+
subscription.snapshot = nextSnapshot;
219+
subscription.notifiedRevision = this._notifiedRevision;
220+
this._updateSubscriptionsMap(subscription, prevSeenRecords);
221+
222+
if (nextSnapshot.data !== snapshot.data) {
223+
callback(nextSnapshot);
224+
return snapshot.selector.owner;
225+
}
226+
}
227+
228+
/**
229+
* Updates the Map that tracks subscriptions by id.
230+
* Given an updated subscription and the records that where seen
231+
* on the previous subscription snapshot, updates our tracking
232+
* to track the subscription for the newly and no longer seen ids.
233+
*/
234+
_updateSubscriptionsMap(
235+
subscription: Subscription,
236+
prevSeenRecords: RecordMap,
237+
) {
238+
for (const dataId in prevSeenRecords) {
239+
const subscriptionsForDataId = this._subscriptionsByDataId.get(dataId);
240+
if (subscriptionsForDataId != null) {
241+
subscriptionsForDataId.delete(subscription);
242+
if (subscriptionsForDataId.size === 0) {
243+
this._subscriptionsByDataId.delete(dataId);
244+
}
245+
}
246+
}
247+
248+
for (const dataId in subscription.snapshot.seenRecords) {
249+
const subscriptionsForDataId = this._subscriptionsByDataId.get(dataId);
250+
if (subscriptionsForDataId != null) {
251+
subscriptionsForDataId.add(subscription);
252+
} else {
253+
this._subscriptionsByDataId.set(dataId, new Set([subscription]));
254+
}
255+
}
256+
}
257+
}
258+
259+
module.exports = RelayStoreSubscriptionsUsingMapByID;

packages/relay-runtime/store/RelayStoreTypes.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,40 @@ export interface Store {
333333
): Disposable;
334334
}
335335

336+
export interface StoreSubscriptions {
337+
/**
338+
* Subscribe to changes to the results of a selector. The callback is called
339+
* when `updateSubscriptions()` is called *and* records have been published that affect the
340+
* selector results relative to the last update.
341+
*/
342+
subscribe(
343+
snapshot: Snapshot,
344+
callback: (snapshot: Snapshot) => void,
345+
): Disposable;
346+
347+
/**
348+
* Record a backup/snapshot of the current state of the subscriptions.
349+
* This state can be restored with restore().
350+
*/
351+
snapshotSubscriptions(source: RecordSource): void;
352+
353+
/**
354+
* Reset the state of the subscriptions to the point that snapshot() was last called.
355+
*/
356+
restoreSubscriptions(): void;
357+
358+
/**
359+
* Notifies each subscription if the snapshot for the subscription selector has changed.
360+
* Mutates the updatedOwners array with any owners (RequestDescriptors) associated
361+
* with the subscriptions that were notifed; i.e. the owners affected by the changes.
362+
*/
363+
updateSubscriptions(
364+
source: RecordSource,
365+
updatedRecordIDs: UpdatedRecords,
366+
updatedOwners: Array<RequestDescriptor>,
367+
): void;
368+
}
369+
336370
/**
337371
* A type that accepts a callback and schedules it to run at some future time.
338372
* By convention, implementations should not execute the callback immediately.

0 commit comments

Comments
 (0)