Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC Implementation: Speculative work #18262

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c9c7f80
implement initial reification functionality
gnoff Feb 18, 2020
6e23780
implement a working reification for function and context provider com…
gnoff Feb 19, 2020
eae61cb
hacky hook bailout to demo deferred bailouts
gnoff Feb 19, 2020
fbeccb0
tune up speculative work bailout. implement context selectors
gnoff Feb 20, 2020
38f3f9e
handle effects properly
gnoff Feb 20, 2020
59ee9cb
remove reify fiber property in lieu of speculativeWorkMode
gnoff Feb 21, 2020
c17f532
add feature flag and restore original work mode
gnoff Feb 21, 2020
adf1f5b
fixup context error warning and reduce test load
gnoff Feb 21, 2020
ce71231
fix bug in completeUnitOfWork and get ReactNewContext tests working
gnoff Feb 22, 2020
344972a
remove tracing
gnoff Feb 22, 2020
fae3f57
crude perf measure
gnoff Feb 23, 2020
6abfd74
fix dependency bug when applying stashed selection in updateContext hook
gnoff Feb 24, 2020
ff55c0a
add experimental nextChildren residue bailout
gnoff Feb 24, 2020
e3bd338
support additional fiber types for specualtive work
gnoff Mar 3, 2020
26fdebd
implement context reader propagation
gnoff Mar 14, 2020
585c539
in progress implementation of reifyNextWork as alternative to specula…
gnoff Mar 15, 2020
b59c99b
support contexts in reifyingNextWork
gnoff Mar 15, 2020
03490c6
fix bitwise not bug
gnoff Mar 15, 2020
5c62e8c
add support for forwardRef and memo apis
gnoff Mar 15, 2020
b7992f2
remove loop breaker
gnoff Mar 15, 2020
daaa863
be defensive about checking bailouts in reify step
gnoff Mar 16, 2020
7d9949f
first pass at a real bailoutReducer
gnoff Mar 17, 2020
8300bde
add test for exercising reducer bailout
gnoff Apr 14, 2020
1272f5a
remove logging
gnoff Apr 14, 2020
5eb9508
remove speculative work implementation
gnoff Apr 14, 2020
c8a5937
additional cleanup
gnoff Apr 14, 2020
c1c0529
explore force scheduling work to help with react fresh cases
gnoff May 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
enableUserTimingAPI,
enableScopeAPI,
enableChunksAPI,
enableContextReaderPropagation,
} from 'shared/ReactFeatureFlags';
import {NoEffect, Placement} from 'shared/ReactSideEffectTags';
import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags';
Expand Down Expand Up @@ -116,6 +117,9 @@ if (__DEV__) {
export type Dependencies = {
expirationTime: ExpirationTime,
firstContext: ContextDependency<mixed> | null,
previousFirstContext?: ContextDependency<mixed> | null,
contextSet?: Set<ReactContext<mixed>> | null,
cleanupSet?: Set<() => mixed> | null,
responders: Map<
ReactEventResponder<any, any>,
ReactEventResponderInstance<any, any>,
Expand Down Expand Up @@ -468,14 +472,28 @@ export function createWorkInProgress(
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
if (enableContextReaderPropagation) {
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
previousFirstContext: currentDependencies.firstContext,
contextSet: currentDependencies.contextSet,
cleanupSet: currentDependencies.cleanupSet,
responders: currentDependencies.responders,
};
} else {
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
}

// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
Expand Down Expand Up @@ -563,14 +581,28 @@ export function resetWorkInProgress(
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
if (enableContextReaderPropagation) {
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
previousFirstContext: currentDependencies.firstContext,
contextSet: currentDependencies.contextSet,
cleanupSet: currentDependencies.cleanupSet,
responders: currentDependencies.responders,
};
} else {
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
}

if (enableProfilerTimer) {
// Note: We don't reset the actualTime counts. It's useful to accumulate
Expand Down
161 changes: 158 additions & 3 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableChunksAPI,
enableReifyNextWork,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
Expand Down Expand Up @@ -107,6 +108,7 @@ import {
ProfileMode,
StrictMode,
BlockingMode,
ReifiedWorkMode,
} from './ReactTypeOfMode';
import {
shouldSetTextContent,
Expand All @@ -131,13 +133,18 @@ import {
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
import {
pushProvider,
popProvider,
propagateContextChange,
readContext,
prepareToReadContext,
calculateChangedBits,
scheduleWorkOnParentPath,
} from './ReactFiberNewContext';
import {renderWithHooks, bailoutHooks} from './ReactFiberHooks';
import {
renderWithHooks,
bailoutHooks,
canBailoutSpeculativeWorkWithHooks,
} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
Expand Down Expand Up @@ -671,7 +678,7 @@ function updateFunctionComponent(
);
}

if (current !== null && !didReceiveUpdate) {
if (!enableReifyNextWork && current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderExpirationTime);
return bailoutOnAlreadyFinishedWork(
current,
Expand Down Expand Up @@ -1457,7 +1464,6 @@ function mountIndeterminateComponent(
getComponentName(Component) || 'Unknown',
);
}

if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
Expand Down Expand Up @@ -2785,6 +2791,143 @@ export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}

function reifyNextWork(workInProgress: Fiber, renderExpirationTime) {
// console.log('reifying next work from ', workInProgress.tag);
let fiber = workInProgress.child;
if (fiber !== null) {
// Set the return pointer of the child to the work-in-progress fiber.
fiber.return = workInProgress;
}

while (fiber !== null) {
let nextFiber;

if (fiber.mode & ReifiedWorkMode) {
// this fiber and it's sub tree have already been reified. whatever work exists
// there we need to progress forward with it. no need to delve deeper
nextFiber = null;
} else {
fiber.mode |= ReifiedWorkMode;

// console.log(
// '-- checking',
// fiber.tag,
// fiber._debugNeedsRemount,
// fiber.type && fiber.type.name,
// );
// fiber.alternate &&
// console.log(
// '-- checking alternate',
// fiber.alternate.tag,
// fiber.alternate._debugNeedsRemount,
// fiber.type === fiber.alternate.type,
// );

if (__DEV__ && fiber._debugNeedsRemount) {
// this fiber needs to be remounted. we can bail out of the reify algo for this
// subtree and let normal work takes it's course
console.log('fiber', fiber.tag, 'has debug marker');
nextFiber = null;
} else if (fiber.expirationTime >= renderExpirationTime) {
let didBailout;
switch (fiber.tag) {
case ForwardRef:
case SimpleMemoComponent:
case FunctionComponent: {
try {
didBailout = canBailoutSpeculativeWorkWithHooks(
fiber,
renderExpirationTime,
);
} catch (e) {
// suppress error and do not bailout. it should error again
// when the component renders when the context selector is run
// or when the reducer state is updated
didBailout = false;
}

break;
}
case ClassComponent: {
// class component is not yet supported for bailing out of context and state updates
// but it support should be possible
// @TODO implement a ClassComponent bailout here
didBailout = false;
break;
}
default: {
// in unsupported cases we cannot bail out of work and we
// consider the speculative work reified
didBailout = false;
}
}
if (didBailout) {
fiber.expirationTime = NoWork;
nextFiber = fiber.child;
} else {
nextFiber = null;
}
} else if (fiber.childExpirationTime >= renderExpirationTime) {
nextFiber = fiber.child;
} else {
nextFiber = null;
}
}

if (fiber.tag === ContextProvider) {
const newValue = fiber.memoizedProps.value;
pushProvider(fiber, newValue);
}

if (nextFiber !== null) {
nextFiber.return = fiber;
} else {
// No child. Traverse to next sibling.
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
// We're back to the root of this subtree. Exit.
nextFiber = null;
break;
}
if (nextFiber.tag === ContextProvider) {
popProvider(nextFiber);
}
let sibling = nextFiber.sibling;
if (sibling !== null) {
// Set the return pointer of the sibling to the work-in-progress fiber.
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
// No more siblings. Traverse up.
nextFiber = nextFiber.return;
resetChildExpirationTime(nextFiber);
}
}
fiber = nextFiber;
}
}

function resetChildExpirationTime(fiber: Fiber) {
let newChildExpirationTime = NoWork;

let child = fiber.child;
while (child !== null) {
const childUpdateExpirationTime = child.expirationTime;
const childChildExpirationTime = child.childExpirationTime;
if (childUpdateExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childUpdateExpirationTime;
}
if (childChildExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childChildExpirationTime;
}
child = child.sibling;
}

fiber.childExpirationTime = newChildExpirationTime;
}

function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
Expand All @@ -2807,6 +2950,16 @@ function bailoutOnAlreadyFinishedWork(
markUnprocessedUpdateTime(updateExpirationTime);
}

if (enableReifyNextWork) {
if (workInProgress.childExpirationTime >= renderExpirationTime) {
if (workInProgress.mode & ReifiedWorkMode) {
// noop, we don't need to do any checking if we've already done it
} else {
reifyNextWork(workInProgress, renderExpirationTime);
}
}
}

// Check if the children have any pending work.
const childExpirationTime = workInProgress.childExpirationTime;
if (childExpirationTime < renderExpirationTime) {
Expand Down Expand Up @@ -2892,6 +3045,8 @@ function beginWork(
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;

// console.log('beginWork', workInProgress.tag);

if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
Expand Down
8 changes: 8 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
enableFundamentalAPI,
enableSuspenseCallback,
enableScopeAPI,
enableContextReaderPropagation,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -77,6 +78,7 @@ import {logCapturedError} from './ReactFiberErrorLogger';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {getCommitTime} from './ReactProfilerTimer';
import {commitUpdateQueue} from './ReactUpdateQueue';
import {cleanupReadersOnUnmount} from './ReactFiberNewContext';
import {
getPublicInstance,
supportsMutation,
Expand Down Expand Up @@ -789,6 +791,9 @@ function commitUnmount(
case MemoComponent:
case SimpleMemoComponent:
case Chunk: {
if (enableContextReaderPropagation) {
cleanupReadersOnUnmount(current);
}
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
Expand Down Expand Up @@ -841,6 +846,9 @@ function commitUnmount(
return;
}
case ClassComponent: {
if (enableContextReaderPropagation) {
cleanupReadersOnUnmount(current);
}
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
Expand Down
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import {
enableFundamentalAPI,
enableScopeAPI,
enableChunksAPI,
enableContextReaderPropagation,
} from 'shared/ReactFeatureFlags';
import {
markSpawnedWork,
Expand Down Expand Up @@ -973,6 +974,11 @@ function completeWork(
updateHostContainer(workInProgress);
return null;
case ContextProvider:
if (enableContextReaderPropagation) {
// capture readers and store on memoizedState
workInProgress.memoizedState =
workInProgress.type._context._currentReaders;
}
// Pop provider fiber
popProvider(workInProgress);
return null;
Expand Down
Loading