@samitouri / QOS-React / commits / b0726e9947

Support sharing context objects between concurrent renderers (#12779)

* Support concurrent primary and secondary renderers. As a workaround to support multiple concurrent renderers, we categorize some renderers as primary and others as secondary. We only expect there to be two concurrent renderers at most: React Native (primary) and Fabric (secondary); React DOM (primary) and React ART (secondary). Secondary renderers store their context values on separate fields. * Add back concurrent renderer warning Only warn for two concurrent primary or two concurrent secondary renderers. * Change "_secondary" suffix to "2" #EveryBitCounts

Andrew Clark committed May 10, 2018 at 18:34 UTC b0726e99476ea67c7558cbf268685998a38ade7c
13 files changed +146 -24
packages/react-art/src/ReactART.js
+3
@@ -478,6 +478,9 @@ const ARTRenderer = ReactFiberReconciler({
478
479 now: ReactScheduler.now,
480
481 + // The ART renderer is secondary to the React DOM renderer.
482 + isPrimaryRenderer: false,
483 +
484 mutation: {
485 appendChild(parentInstance, child) {
486 if (child.parentNode === parentInstance) {
packages/react-art/src/__tests__/ReactART-test.js
+54
@@ -339,6 +339,60 @@ describe('ReactART', () => {
339 doClick(instance);
340 expect(onClick2).toBeCalled();
341 });
342 +
343 + it('can concurrently render with a "primary" renderer while sharing context', () => {
344 + const CurrentRendererContext = React.createContext(null);
345 +
346 + function Yield(props) {
347 + testRenderer.unstable_yield(props.value);
348 + return null;
349 + }
350 +
351 + let ops = [];
352 + function LogCurrentRenderer() {
353 + return (
354 + <CurrentRendererContext.Consumer>
355 + {currentRenderer => {
356 + ops.push(currentRenderer);
357 + return null;
358 + }}
359 + </CurrentRendererContext.Consumer>
360 + );
361 + }
362 +
363 + // Using test renderer instead of the DOM renderer here because async
364 + // testing APIs for the DOM renderer don't exist.
365 + const testRenderer = renderer.create(
366 + <CurrentRendererContext.Provider value="Test">
367 + <Yield value="A" />
368 + <Yield value="B" />
369 + <LogCurrentRenderer />
370 + <Yield value="C" />
371 + </CurrentRendererContext.Provider>,
372 + {
373 + unstable_isAsync: true,
374 + },
375 + );
376 +
377 + testRenderer.unstable_flushThrough(['A']);
378 +
379 + ReactDOM.render(
380 + <Surface>
381 + <LogCurrentRenderer />
382 + <CurrentRendererContext.Provider value="ART">
383 + <LogCurrentRenderer />
384 + </CurrentRendererContext.Provider>
385 + </Surface>,
386 + container,
387 + );
388 +
389 + expect(ops).toEqual([null, 'ART']);
390 +
391 + ops = [];
392 + expect(testRenderer.unstable_flushAll()).toEqual(['B', 'C']);
393 +
394 + expect(ops).toEqual(['Test']);
395 + });
396 });
397
398 describe('ReactARTComponents', () => {
packages/react-dom/src/client/ReactDOM.js
+2
@@ -690,6 +690,8 @@ const DOMRenderer = ReactFiberReconciler({
690
691 now: ReactScheduler.now,
692
693 + isPrimaryRenderer: true,
694 +
695 mutation: {
696 commitMount(
697 domElement: Instance,
packages/react-native-renderer/src/ReactFabricRenderer.js
+3
@@ -217,6 +217,9 @@ const ReactFabricRenderer = ReactFiberReconciler({
217
218 now: ReactNativeFrameScheduling.now,
219
220 + // The Fabric renderer is secondary to the existing React Native renderer.
221 + isPrimaryRenderer: false,
222 +
223 prepareForCommit(): void {
224 // Noop
225 },
packages/react-native-renderer/src/ReactNativeFiberRenderer.js
+2
@@ -169,6 +169,8 @@ const NativeRenderer = ReactFiberReconciler({
169
170 now: ReactNativeFrameScheduling.now,
171
172 + isPrimaryRenderer: true,
173 +
174 prepareForCommit(): void {
175 // Noop
176 },
packages/react-noop-renderer/src/ReactNoop.js
+2
@@ -185,6 +185,8 @@ let SharedHostConfig = {
185 now(): number {
186 return elapsedTimeInMs;
187 },
188 +
189 + isPrimaryRenderer: true,
190 };
191
192 const NoopRenderer = ReactFiberReconciler({
packages/react-reconciler/src/ReactFiberBeginWork.js
+7 -3
@@ -104,7 +104,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
104
105 const {pushHostContext, pushHostContainer} = hostContext;
106
107 - const {pushProvider} = newContext;
107 + const {
108 + pushProvider,
109 + getContextCurrentValue,
110 + getContextChangedBits,
111 + } = newContext;
112
113 const {
114 markActualRenderTimeStarted,
@@ -1048,8 +1052,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1052 const newProps = workInProgress.pendingProps;
1053 const oldProps = workInProgress.memoizedProps;
1054
1051 - const newValue = context._currentValue;
1052 - const changedBits = context._changedBits;
1055 + const newValue = getContextCurrentValue(context);
1056 + const changedBits = getContextChangedBits(context);
1057
1058 if (hasLegacyContextChanged()) {
1059 // Normally we can bail out on props equality but if context has changed
packages/react-reconciler/src/ReactFiberNewContext.js
+54 -20
@@ -11,14 +11,16 @@ import type {Fiber} from './ReactFiber';
11 import type {ReactContext} from 'shared/ReactTypes';
12 import type {StackCursor, Stack} from './ReactFiberStack';
13
14 -import warning from 'fbjs/lib/warning';
15 -
14 export type NewContext = {
15 pushProvider(providerFiber: Fiber): void,
16 popProvider(providerFiber: Fiber): void,
17 + getContextCurrentValue(context: ReactContext<any>): any,
18 + getContextChangedBits(context: ReactContext<any>): number,
19 };
20
21 -export default function(stack: Stack) {
21 +import warning from 'fbjs/lib/warning';
22 +
23 +export default function(stack: Stack, isPrimaryRenderer: boolean) {
24 const {createCursor, push, pop} = stack;
25
26 const providerCursor: StackCursor<Fiber | null> = createCursor(null);
@@ -34,21 +36,38 @@ export default function(stack: Stack) {
36 function pushProvider(providerFiber: Fiber): void {
37 const context: ReactContext<any> = providerFiber.type._context;
38
37 - push(changedBitsCursor, context._changedBits, providerFiber);
38 - push(valueCursor, context._currentValue, providerFiber);
39 - push(providerCursor, providerFiber, providerFiber);
40 -
41 - context._currentValue = providerFiber.pendingProps.value;
42 - context._changedBits = providerFiber.stateNode;
43 -
44 - if (__DEV__) {
45 - warning(
46 - context._currentRenderer === null ||
47 - context._currentRenderer === rendererSigil,
48 - 'Detected multiple renderers concurrently rendering the ' +
49 - 'same context provider. This is currently unsupported.',
50 - );
51 - context._currentRenderer = rendererSigil;
39 + if (isPrimaryRenderer) {
40 + push(changedBitsCursor, context._changedBits, providerFiber);
41 + push(valueCursor, context._currentValue, providerFiber);
42 + push(providerCursor, providerFiber, providerFiber);
43 +
44 + context._currentValue = providerFiber.pendingProps.value;
45 + context._changedBits = providerFiber.stateNode;
46 + if (__DEV__) {
47 + warning(
48 + context._currentRenderer === null ||
49 + context._currentRenderer === rendererSigil,
50 + 'Detected multiple renderers concurrently rendering the ' +
51 + 'same context provider. This is currently unsupported.',
52 + );
53 + context._currentRenderer = rendererSigil;
54 + }
55 + } else {
56 + push(changedBitsCursor, context._changedBits2, providerFiber);
57 + push(valueCursor, context._currentValue2, providerFiber);
58 + push(providerCursor, providerFiber, providerFiber);
59 +
60 + context._currentValue2 = providerFiber.pendingProps.value;
61 + context._changedBits2 = providerFiber.stateNode;
62 + if (__DEV__) {
63 + warning(
64 + context._currentRenderer2 === null ||
65 + context._currentRenderer2 === rendererSigil,
66 + 'Detected multiple renderers concurrently rendering the ' +
67 + 'same context provider. This is currently unsupported.',
68 + );
69 + context._currentRenderer2 = rendererSigil;
70 + }
71 }
72 }
73
@@ -61,12 +80,27 @@ export default function(stack: Stack) {
80 pop(changedBitsCursor, providerFiber);
81
82 const context: ReactContext<any> = providerFiber.type._context;
64 - context._currentValue = currentValue;
65 - context._changedBits = changedBits;
83 + if (isPrimaryRenderer) {
84 + context._currentValue = currentValue;
85 + context._changedBits = changedBits;
86 + } else {
87 + context._currentValue2 = currentValue;
88 + context._changedBits2 = changedBits;
89 + }
90 + }
91 +
92 + function getContextCurrentValue(context: ReactContext<any>): any {
93 + return isPrimaryRenderer ? context._currentValue : context._currentValue2;
94 + }
95 +
96 + function getContextChangedBits(context: ReactContext<any>): number {
97 + return isPrimaryRenderer ? context._changedBits : context._changedBits2;
98 }
99
100 return {
101 pushProvider,
102 popProvider,
103 + getContextCurrentValue,
104 + getContextChangedBits,
105 };
106 }
packages/react-reconciler/src/ReactFiberReconciler.js
+5
@@ -95,6 +95,11 @@ export type HostConfig<T, P, I, TI, HI, PI, C, CC, CX, PL> = {
95
96 now(): number,
97
98 + // Temporary workaround for scenario where multiple renderers concurrently
99 + // render using the same context objects. E.g. React DOM and React ART on the
100 + // same page. DOM is the primary renderer; ART is the secondary renderer.
101 + isPrimaryRenderer: boolean,
102 +
103 +hydration?: HydrationHostConfig<T, P, I, TI, HI, C, CX, PL>,
104
105 +mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,
packages/react-reconciler/src/ReactFiberScheduler.js
+1 -1
@@ -181,7 +181,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
181 const stack = ReactFiberStack();
182 const hostContext = ReactFiberHostContext(config, stack);
183 const legacyContext = ReactFiberLegacyContext(stack);
184 - const newContext = ReactFiberNewContext(stack);
184 + const newContext = ReactFiberNewContext(stack, config.isPrimaryRenderer);
185 const profilerTimer = createProfilerTimer(now);
186 const {popHostContext, popHostContainer} = hostContext;
187 const {
packages/react-test-renderer/src/ReactTestRenderer.js
+2
@@ -226,6 +226,8 @@ const TestRenderer = ReactFiberReconciler({
226 // Even after the reconciler has initialized and read host config values.
227 now: () => nowImplementation(),
228
229 + isPrimaryRenderer: true,
230 +
231 mutation: {
232 commitUpdate(
233 instance: Instance,
packages/react/src/ReactContext.js
+8
@@ -36,7 +36,14 @@ export function createContext<T>(
36 _calculateChangedBits: calculateChangedBits,
37 _defaultValue: defaultValue,
38 _currentValue: defaultValue,
39 + // As a workaround to support multiple concurrent renderers, we categorize
40 + // some renderers as primary and others as secondary. We only expect
41 + // there to be two concurrent renderers at most: React Native (primary) and
42 + // Fabric (secondary); React DOM (primary) and React ART (secondary).
43 + // Secondary renderers store their context values on separate fields.
44 + _currentValue2: defaultValue,
45 _changedBits: 0,
46 + _changedBits2: 0,
47 // These are circular
48 Provider: (null: any),
49 Consumer: (null: any),
@@ -50,6 +57,7 @@ export function createContext<T>(
57
58 if (__DEV__) {
59 context._currentRenderer = null;
60 + context._currentRenderer2 = null;
61 }
62
63 return context;
packages/shared/ReactTypes.js
+3
@@ -85,10 +85,13 @@ export type ReactContext<T> = {
85 _defaultValue: T,
86
87 _currentValue: T,
88 + _currentValue2: T,
89 _changedBits: number,
90 + _changedBits2: number,
91
92 // DEV only
93 _currentRenderer?: Object | null,
94 + _currentRenderer2?: Object | null,
95 };
96
97 export type ReactPortal = {