Skip to content

Commit 14dffc7

Browse files
author
Brian Vaughn
committed
Overhalled continuations to work with React's scheduler
1 parent 974d25e commit 14dffc7

File tree

6 files changed

+469
-208
lines changed

6 files changed

+469
-208
lines changed

packages/interaction-tracking/src/InteractionEmitter.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
* @flow
88
*/
99

10-
import type {Interaction} from './InteractionTracking';
11-
12-
type Interactions = Array<Interaction>;
10+
import type {Interactions} from './InteractionTracking';
1311

1412
export type InteractionObserver = {
1513
onInteractionsScheduled: (

packages/interaction-tracking/src/InteractionTracking.js

Lines changed: 103 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import invariant from 'shared/invariant';
10+
import warning from 'shared/warning';
1111
import {
1212
__onInteractionsScheduled,
1313
__onInteractionsStarting,
@@ -16,20 +16,17 @@ import {
1616

1717
export {registerInteractionObserver} from './InteractionEmitter';
1818

19-
type Interactions = Array<Interaction>;
20-
19+
// Maps execution ID to Interactions for all scheduled continuations.
20+
// We key off of ID because Interactions may be scheduled for multiple continuations.
21+
// For example, an Interaction may schedule work with React at multiple priorities.
22+
// Each priority would reserve a continuation (since they may be processed separately).
23+
export type Continuations = Map<number, Interaction>;
2124
export type Interaction = {|
2225
id: number,
2326
name: string,
2427
timestamp: number,
2528
|};
26-
27-
export type Continuation = {
28-
__hasBeenRun: boolean,
29-
__id: number,
30-
__interactions: Interactions,
31-
__prevInteractions: Interactions | null,
32-
};
29+
export type Interactions = Set<Interaction>;
3330

3431
// Normally we would use the current renderer HostConfig's "now" method,
3532
// But since interaction-tracking will be a separate package,
@@ -43,10 +40,33 @@ if (typeof performance === 'object' && typeof performance.now === 'function') {
4340
now = () => localDate.now();
4441
}
4542

46-
let currentContinuation: Continuation | null = null;
47-
let globalExecutionID: number = 0;
48-
let globalInteractionID: number = 0;
49-
let interactions: Interactions | null = null;
43+
// Counters used to generate unique IDs for all interactions,
44+
// And InteractionObserver executions (including continuations).
45+
let executionIDCounter: number = 0;
46+
let interactionIDCounter: number = 0;
47+
48+
// Set of currently tracked interactions.
49+
// Interactions "stack"–
50+
// Meaning that newly tracked interactions are appended to the previously active set.
51+
// When an interaction goes out of scope, the previous set (if any) is restored.
52+
let interactions: Interactions = new Set();
53+
54+
// Temporarily holds the Set of interactions masked by an active "continuation".
55+
// This is necessary since continuations are started/stopped externally.
56+
// This value enables previous interactions to be restored when the continuation ends.
57+
// This implementation supports only one active continuation at any time.
58+
// We could change this to a stack structure in the future if this requirement changed.
59+
let interactionsMaskedByContinuation: Interactions = interactions;
60+
61+
// Map continuation (execution) UIDs to interaction UIDs.
62+
// These Maps are only used for DEV mode validation.
63+
// Ideally we can replace these Maps with WeakMaps at some point.
64+
let scheduledContinuations: Continuations | null = null;
65+
let startedContinuations: Continuations | null = null;
66+
if (__DEV__) {
67+
scheduledContinuations = new Map();
68+
startedContinuations = new Map();
69+
}
5070

5171
export function getCurrent(): Interactions | null {
5272
if (!__PROFILE__) {
@@ -56,77 +76,99 @@ export function getCurrent(): Interactions | null {
5676
}
5777
}
5878

59-
export function reserveContinuation(): Continuation | null {
79+
// A "continuation" signifies that an interaction is not completed when its callback finish running.
80+
// This is useful for React, since scheduled work may be batched and processed asynchronously.
81+
// Continuations have a unique execution ID which must later be used to restore the interaction.
82+
// This is done by calling startContinuations() before processing the delayed work,
83+
// And stopContinuations() to indicate that the work has been completed.
84+
export function reserveContinuation(interaction: Interaction): number {
6085
if (!__PROFILE__) {
61-
return null;
86+
return 0;
6287
}
6388

64-
if (interactions !== null) {
65-
const executionID = globalExecutionID++;
89+
const executionID = executionIDCounter++;
6690

67-
__onInteractionsScheduled(interactions, executionID);
91+
__onInteractionsScheduled(new Set([interaction]), executionID);
6892

69-
return {
70-
__hasBeenRun: false,
71-
__id: executionID,
72-
__interactions: interactions,
73-
__prevInteractions: null,
74-
};
75-
} else {
76-
return null;
93+
if (__DEV__) {
94+
((scheduledContinuations: any): Continuations).set(
95+
executionID,
96+
interaction,
97+
);
7798
}
99+
100+
return executionID;
78101
}
79102

80-
export function startContinuation(continuation: Continuation | null): void {
103+
export function startContinuations(continuations: Continuations): void {
81104
if (!__PROFILE__) {
82105
return;
83106
}
84107

85-
invariant(
86-
currentContinuation === null,
87-
'Cannot start a continuation when one is already active.',
88-
);
89-
90-
if (continuation === null) {
91-
return;
108+
if (__DEV__) {
109+
warning(
110+
((startedContinuations: any): Continuations).size === 0,
111+
'Only one batch of continuations can be active at a time.',
112+
);
92113
}
93114

94-
invariant(
95-
!continuation.__hasBeenRun,
96-
'A continuation can only be started once',
97-
);
115+
const continuationInteractions = new Set();
116+
117+
const entries = Array.from(continuations);
118+
for (let index = 0; index < entries.length; index++) {
119+
const [executionID: number, interaction: Interaction] = entries[index];
98120

99-
continuation.__hasBeenRun = true;
100-
currentContinuation = continuation;
121+
if (__DEV__) {
122+
warning(
123+
((scheduledContinuations: any): Continuations).get(executionID) ===
124+
interaction,
125+
'Cannot run an unscheduled continuation.',
126+
);
127+
128+
((scheduledContinuations: any): Continuations).delete(executionID);
129+
((startedContinuations: any): Continuations).set(
130+
executionID,
131+
interaction,
132+
);
133+
}
134+
135+
__onInteractionsStarting(new Set([interaction]), executionID);
136+
137+
continuationInteractions.add(interaction);
138+
}
101139

102140
// Continuations should mask (rather than extend) any current interactions.
103141
// Upon completion of a continuation, previous interactions will be restored.
104-
continuation.__prevInteractions = interactions;
105-
interactions = continuation.__interactions;
106-
107-
__onInteractionsStarting(interactions, continuation.__id);
142+
interactionsMaskedByContinuation = interactions;
143+
interactions = continuationInteractions;
108144
}
109145

110-
export function stopContinuation(continuation: Continuation): void {
146+
export function stopContinuations(continuations: Continuations): void {
111147
if (!__PROFILE__) {
112148
return;
113149
}
114150

115-
invariant(
116-
currentContinuation === continuation,
117-
'Cannot stop a continuation that is not active.',
118-
);
151+
// Stop interactions in the reverse order they were started.
152+
const entries = Array.from(continuations);
153+
for (let index = entries.length - 1; index >= 0; index--) {
154+
const [executionID: number, interaction: Interaction] = entries[index];
119155

120-
if (continuation === null) {
121-
return;
122-
}
156+
if (__DEV__) {
157+
warning(
158+
((startedContinuations: any): Continuations).get(executionID) ===
159+
interaction,
160+
'Cannot stop an inactive continuation.',
161+
);
123162

124-
__onInteractionsEnded(continuation.__interactions, continuation.__id);
163+
((startedContinuations: any): Continuations).delete(executionID);
164+
}
125165

126-
currentContinuation = null;
166+
__onInteractionsEnded(new Set([interaction]), executionID);
167+
}
127168

128169
// Restore previous interactions.
129-
interactions = continuation.__prevInteractions;
170+
interactions = interactionsMaskedByContinuation;
171+
interactionsMaskedByContinuation = interactions;
130172
}
131173

132174
export function track(name: string, callback: Function): void {
@@ -136,19 +178,19 @@ export function track(name: string, callback: Function): void {
136178
}
137179

138180
const interaction: Interaction = {
139-
id: globalInteractionID++,
181+
id: interactionIDCounter++,
140182
name,
141183
timestamp: now(),
142184
};
143185

144-
const executionID = globalExecutionID++;
186+
const executionID = executionIDCounter++;
145187
const prevInteractions = interactions;
146188

147189
// Tracked interactions should stack/accumulate.
148190
// To do that, clone the current interactions array.
149191
// The previous interactions array will be restored upon completion.
150-
interactions =
151-
interactions === null ? [interaction] : interactions.concat(interaction);
192+
interactions = new Set(interactions);
193+
interactions.add(interaction);
152194

153195
try {
154196
__onInteractionsScheduled(interactions, executionID);
@@ -171,7 +213,7 @@ export function wrap(callback: Function): Function {
171213
return callback;
172214
}
173215

174-
const executionID = globalExecutionID++;
216+
const executionID = executionIDCounter++;
175217
const wrappedInteractions = interactions;
176218

177219
__onInteractionsScheduled(wrappedInteractions, executionID);

0 commit comments

Comments
 (0)