@samitouri / QOS-React / commits / 952dfff3f1

Split suspended work loop logic into separate functions

Refactors the logic for handling when the work loop is suspended into separate functions for replaying versus unwinding. This allows us to hoist certain checks into the caller. For example, when rendering due to flushSync, we will always unwind the stack without yielding the microtasks. No intentional behavior change in this commit.

Andrew Clark committed Oct 24, 2022 at 13:23 UTC 952dfff3f1b1ef4644ba99a778dda787fa6d9b18
2 files changed +242 -168
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+121 -84
@@ -1936,6 +1936,42 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1936
1937 do {
1938 try {
1939 + if (
1940 + workInProgressSuspendedReason !== NotSuspended &&
1941 + workInProgress !== null
1942 + ) {
1943 + // The work loop is suspended. We need to either unwind the stack or
1944 + // replay the suspended component.
1945 + const unitOfWork = workInProgress;
1946 + const thrownValue = workInProgressThrownValue;
1947 + workInProgressSuspendedReason = NotSuspended;
1948 + workInProgressThrownValue = null;
1949 +
1950 + // TODO: This check is only here to account for thenables that
1951 + // synchronously resolve. Otherwise we would always unwind when
1952 + // rendering with renderRootSync. (In the future, discrete updates will
1953 + // use renderRootConcurrent instead.) We should account for
1954 + // synchronously resolved thenables before hitting this path.
1955 + switch (workInProgressSuspendedReason) {
1956 + case SuspendedOnError: {
1957 + // Unwind then continue with the normal work loop.
1958 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1959 + break;
1960 + }
1961 + default: {
1962 + const wasPinged =
1963 + workInProgressSuspendedThenableState !== null &&
1964 + isThenableStateResolved(workInProgressSuspendedThenableState);
1965 + if (wasPinged) {
1966 + replaySuspendedUnitOfWork(unitOfWork, thrownValue);
1967 + } else {
1968 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1969 + }
1970 + // Continue with the normal work loop.
1971 + break;
1972 + }
1973 + }
1974 + }
1975 workLoopSync();
1976 break;
1977 } catch (thrownValue) {
@@ -1980,18 +2016,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2016 /** @noinline */
2017 function workLoopSync() {
2018 // Perform work without checking if we need to yield between fiber.
1983 -
1984 - if (workInProgressSuspendedReason !== NotSuspended) {
1985 - // The current work-in-progress was already attempted. We need to unwind
1986 - // it before we continue the normal work loop.
1987 - const thrownValue = workInProgressThrownValue;
1988 - workInProgressSuspendedReason = NotSuspended;
1989 - workInProgressThrownValue = null;
1990 - if (workInProgress !== null) {
1991 - resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1992 - }
1993 - }
1994 -
2019 while (workInProgress !== null) {
2020 performUnitOfWork(workInProgress);
2021 }
@@ -2039,6 +2063,36 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2063
2064 do {
2065 try {
2066 + if (
2067 + workInProgressSuspendedReason !== NotSuspended &&
2068 + workInProgress !== null
2069 + ) {
2070 + // The work loop is suspended. We need to either unwind the stack or
2071 + // replay the suspended component.
2072 + const unitOfWork = workInProgress;
2073 + const thrownValue = workInProgressThrownValue;
2074 + workInProgressSuspendedReason = NotSuspended;
2075 + workInProgressThrownValue = null;
2076 + switch (workInProgressSuspendedReason) {
2077 + case SuspendedOnError: {
2078 + // Unwind then continue with the normal work loop.
2079 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2080 + break;
2081 + }
2082 + default: {
2083 + const wasPinged =
2084 + workInProgressSuspendedThenableState !== null &&
2085 + isThenableStateResolved(workInProgressSuspendedThenableState);
2086 + if (wasPinged) {
2087 + replaySuspendedUnitOfWork(unitOfWork, thrownValue);
2088 + } else {
2089 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2090 + }
2091 + // Continue with the normal work loop.
2092 + break;
2093 + }
2094 + }
2095 + }
2096 workLoopConcurrent();
2097 break;
2098 } catch (thrownValue) {
@@ -2091,18 +2145,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2145 /** @noinline */
2146 function workLoopConcurrent() {
2147 // Perform work until Scheduler asks us to yield
2094 -
2095 - if (workInProgressSuspendedReason !== NotSuspended) {
2096 - // The current work-in-progress was already attempted. We need to unwind
2097 - // it before we continue the normal work loop.
2098 - const thrownValue = workInProgressThrownValue;
2099 - workInProgressSuspendedReason = NotSuspended;
2100 - workInProgressThrownValue = null;
2101 - if (workInProgress !== null) {
2102 - resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2103 - }
2104 - }
2105 -
2148 while (workInProgress !== null && !shouldYield()) {
2149 // $FlowFixMe[incompatible-call] found when upgrading Flow
2150 performUnitOfWork(workInProgress);
@@ -2137,69 +2179,15 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2179 ReactCurrentOwner.current = null;
2180 }
2181
2140 -function resumeSuspendedUnitOfWork(
2182 +function replaySuspendedUnitOfWork(
2183 unitOfWork: Fiber,
2184 thrownValue: mixed,
2185 ): void {
2144 - // This is a fork of performUnitOfWork specifcally for resuming a fiber that
2145 - // just suspended. In some cases, we may choose to retry the fiber immediately
2146 - // instead of unwinding the stack. It's a separate function to keep the
2147 - // additional logic out of the work loop's hot path.
2148 -
2149 - const wasPinged =
2150 - workInProgressSuspendedThenableState !== null &&
2151 - isThenableStateResolved(workInProgressSuspendedThenableState);
2152 -
2153 - if (!wasPinged) {
2154 - // The thenable wasn't pinged. Return to the normal work loop. This will
2155 - // unwind the stack, and potentially result in showing a fallback.
2156 - workInProgressSuspendedThenableState = null;
2157 -
2158 - const returnFiber = unitOfWork.return;
2159 - if (returnFiber === null || workInProgressRoot === null) {
2160 - // Expected to be working on a non-root fiber. This is a fatal error
2161 - // because there's no ancestor that can handle it; the root is
2162 - // supposed to capture all errors that weren't caught by an error
2163 - // boundary.
2164 - workInProgressRootExitStatus = RootFatalErrored;
2165 - workInProgressRootFatalError = thrownValue;
2166 - // Set `workInProgress` to null. This represents advancing to the next
2167 - // sibling, or the parent if there are no siblings. But since the root
2168 - // has no siblings nor a parent, we set it to null. Usually this is
2169 - // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2170 - // intentionally not calling those, we need set it here.
2171 - // TODO: Consider calling `unwindWork` to pop the contexts.
2172 - workInProgress = null;
2173 - return;
2174 - }
2175 -
2176 - try {
2177 - // Find and mark the nearest Suspense or error boundary that can handle
2178 - // this "exception".
2179 - throwException(
2180 - workInProgressRoot,
2181 - returnFiber,
2182 - unitOfWork,
2183 - thrownValue,
2184 - workInProgressRootRenderLanes,
2185 - );
2186 - } catch (error) {
2187 - // We had trouble processing the error. An example of this happening is
2188 - // when accessing the `componentDidCatch` property of an error boundary
2189 - // throws an error. A weird edge case. There's a regression test for this.
2190 - // To prevent an infinite loop, bubble the error up to the next parent.
2191 - workInProgress = returnFiber;
2192 - throw error;
2193 - }
2194 -
2195 - // Return to the normal work loop.
2196 - completeUnitOfWork(unitOfWork);
2197 - return;
2198 - }
2199 -
2200 - // The work-in-progress was immediately pinged. Instead of unwinding the
2201 - // stack and potentially showing a fallback, unwind only the last stack frame,
2202 - // reset the fiber, and try rendering it again.
2186 + // This is a fork of performUnitOfWork specifcally for replaying a fiber that
2187 + // just suspended.
2188 + //
2189 + // Instead of unwinding the stack and potentially showing a fallback, unwind
2190 + // only the last stack frame, reset the fiber, and try rendering it again.
2191 const current = unitOfWork.alternate;
2192 unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2193 unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
@@ -2232,6 +2220,55 @@ function resumeSuspendedUnitOfWork(
2220 ReactCurrentOwner.current = null;
2221 }
2222
2223 +function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2224 + // This is a fork of performUnitOfWork specifcally for unwinding a fiber
2225 + // that threw an exception.
2226 + //
2227 + // Return to the normal work loop. This will unwind the stack, and potentially
2228 + // result in showing a fallback.
2229 + workInProgressSuspendedThenableState = null;
2230 +
2231 + const returnFiber = unitOfWork.return;
2232 + if (returnFiber === null || workInProgressRoot === null) {
2233 + // Expected to be working on a non-root fiber. This is a fatal error
2234 + // because there's no ancestor that can handle it; the root is
2235 + // supposed to capture all errors that weren't caught by an error
2236 + // boundary.
2237 + workInProgressRootExitStatus = RootFatalErrored;
2238 + workInProgressRootFatalError = thrownValue;
2239 + // Set `workInProgress` to null. This represents advancing to the next
2240 + // sibling, or the parent if there are no siblings. But since the root
2241 + // has no siblings nor a parent, we set it to null. Usually this is
2242 + // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2243 + // intentionally not calling those, we need set it here.
2244 + // TODO: Consider calling `unwindWork` to pop the contexts.
2245 + workInProgress = null;
2246 + return;
2247 + }
2248 +
2249 + try {
2250 + // Find and mark the nearest Suspense or error boundary that can handle
2251 + // this "exception".
2252 + throwException(
2253 + workInProgressRoot,
2254 + returnFiber,
2255 + unitOfWork,
2256 + thrownValue,
2257 + workInProgressRootRenderLanes,
2258 + );
2259 + } catch (error) {
2260 + // We had trouble processing the error. An example of this happening is
2261 + // when accessing the `componentDidCatch` property of an error boundary
2262 + // throws an error. A weird edge case. There's a regression test for this.
2263 + // To prevent an infinite loop, bubble the error up to the next parent.
2264 + workInProgress = returnFiber;
2265 + throw error;
2266 + }
2267 +
2268 + // Return to the normal work loop.
2269 + completeUnitOfWork(unitOfWork);
2270 +}
2271 +
2272 export function getSuspendedThenableState(): ThenableState | null {
2273 return workInProgressSuspendedThenableState;
2274 }
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+121 -84
@@ -1936,6 +1936,42 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
1936
1937 do {
1938 try {
1939 + if (
1940 + workInProgressSuspendedReason !== NotSuspended &&
1941 + workInProgress !== null
1942 + ) {
1943 + // The work loop is suspended. We need to either unwind the stack or
1944 + // replay the suspended component.
1945 + const unitOfWork = workInProgress;
1946 + const thrownValue = workInProgressThrownValue;
1947 + workInProgressSuspendedReason = NotSuspended;
1948 + workInProgressThrownValue = null;
1949 +
1950 + // TODO: This check is only here to account for thenables that
1951 + // synchronously resolve. Otherwise we would always unwind when
1952 + // rendering with renderRootSync. (In the future, discrete updates will
1953 + // use renderRootConcurrent instead.) We should account for
1954 + // synchronously resolved thenables before hitting this path.
1955 + switch (workInProgressSuspendedReason) {
1956 + case SuspendedOnError: {
1957 + // Unwind then continue with the normal work loop.
1958 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1959 + break;
1960 + }
1961 + default: {
1962 + const wasPinged =
1963 + workInProgressSuspendedThenableState !== null &&
1964 + isThenableStateResolved(workInProgressSuspendedThenableState);
1965 + if (wasPinged) {
1966 + replaySuspendedUnitOfWork(unitOfWork, thrownValue);
1967 + } else {
1968 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1969 + }
1970 + // Continue with the normal work loop.
1971 + break;
1972 + }
1973 + }
1974 + }
1975 workLoopSync();
1976 break;
1977 } catch (thrownValue) {
@@ -1980,18 +2016,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2016 /** @noinline */
2017 function workLoopSync() {
2018 // Perform work without checking if we need to yield between fiber.
1983 -
1984 - if (workInProgressSuspendedReason !== NotSuspended) {
1985 - // The current work-in-progress was already attempted. We need to unwind
1986 - // it before we continue the normal work loop.
1987 - const thrownValue = workInProgressThrownValue;
1988 - workInProgressSuspendedReason = NotSuspended;
1989 - workInProgressThrownValue = null;
1990 - if (workInProgress !== null) {
1991 - resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1992 - }
1993 - }
1994 -
2019 while (workInProgress !== null) {
2020 performUnitOfWork(workInProgress);
2021 }
@@ -2039,6 +2063,36 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2063
2064 do {
2065 try {
2066 + if (
2067 + workInProgressSuspendedReason !== NotSuspended &&
2068 + workInProgress !== null
2069 + ) {
2070 + // The work loop is suspended. We need to either unwind the stack or
2071 + // replay the suspended component.
2072 + const unitOfWork = workInProgress;
2073 + const thrownValue = workInProgressThrownValue;
2074 + workInProgressSuspendedReason = NotSuspended;
2075 + workInProgressThrownValue = null;
2076 + switch (workInProgressSuspendedReason) {
2077 + case SuspendedOnError: {
2078 + // Unwind then continue with the normal work loop.
2079 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2080 + break;
2081 + }
2082 + default: {
2083 + const wasPinged =
2084 + workInProgressSuspendedThenableState !== null &&
2085 + isThenableStateResolved(workInProgressSuspendedThenableState);
2086 + if (wasPinged) {
2087 + replaySuspendedUnitOfWork(unitOfWork, thrownValue);
2088 + } else {
2089 + unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2090 + }
2091 + // Continue with the normal work loop.
2092 + break;
2093 + }
2094 + }
2095 + }
2096 workLoopConcurrent();
2097 break;
2098 } catch (thrownValue) {
@@ -2091,18 +2145,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2145 /** @noinline */
2146 function workLoopConcurrent() {
2147 // Perform work until Scheduler asks us to yield
2094 -
2095 - if (workInProgressSuspendedReason !== NotSuspended) {
2096 - // The current work-in-progress was already attempted. We need to unwind
2097 - // it before we continue the normal work loop.
2098 - const thrownValue = workInProgressThrownValue;
2099 - workInProgressSuspendedReason = NotSuspended;
2100 - workInProgressThrownValue = null;
2101 - if (workInProgress !== null) {
2102 - resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2103 - }
2104 - }
2105 -
2148 while (workInProgress !== null && !shouldYield()) {
2149 // $FlowFixMe[incompatible-call] found when upgrading Flow
2150 performUnitOfWork(workInProgress);
@@ -2137,69 +2179,15 @@ function performUnitOfWork(unitOfWork: Fiber): void {
2179 ReactCurrentOwner.current = null;
2180 }
2181
2140 -function resumeSuspendedUnitOfWork(
2182 +function replaySuspendedUnitOfWork(
2183 unitOfWork: Fiber,
2184 thrownValue: mixed,
2185 ): void {
2144 - // This is a fork of performUnitOfWork specifcally for resuming a fiber that
2145 - // just suspended. In some cases, we may choose to retry the fiber immediately
2146 - // instead of unwinding the stack. It's a separate function to keep the
2147 - // additional logic out of the work loop's hot path.
2148 -
2149 - const wasPinged =
2150 - workInProgressSuspendedThenableState !== null &&
2151 - isThenableStateResolved(workInProgressSuspendedThenableState);
2152 -
2153 - if (!wasPinged) {
2154 - // The thenable wasn't pinged. Return to the normal work loop. This will
2155 - // unwind the stack, and potentially result in showing a fallback.
2156 - workInProgressSuspendedThenableState = null;
2157 -
2158 - const returnFiber = unitOfWork.return;
2159 - if (returnFiber === null || workInProgressRoot === null) {
2160 - // Expected to be working on a non-root fiber. This is a fatal error
2161 - // because there's no ancestor that can handle it; the root is
2162 - // supposed to capture all errors that weren't caught by an error
2163 - // boundary.
2164 - workInProgressRootExitStatus = RootFatalErrored;
2165 - workInProgressRootFatalError = thrownValue;
2166 - // Set `workInProgress` to null. This represents advancing to the next
2167 - // sibling, or the parent if there are no siblings. But since the root
2168 - // has no siblings nor a parent, we set it to null. Usually this is
2169 - // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2170 - // intentionally not calling those, we need set it here.
2171 - // TODO: Consider calling `unwindWork` to pop the contexts.
2172 - workInProgress = null;
2173 - return;
2174 - }
2175 -
2176 - try {
2177 - // Find and mark the nearest Suspense or error boundary that can handle
2178 - // this "exception".
2179 - throwException(
2180 - workInProgressRoot,
2181 - returnFiber,
2182 - unitOfWork,
2183 - thrownValue,
2184 - workInProgressRootRenderLanes,
2185 - );
2186 - } catch (error) {
2187 - // We had trouble processing the error. An example of this happening is
2188 - // when accessing the `componentDidCatch` property of an error boundary
2189 - // throws an error. A weird edge case. There's a regression test for this.
2190 - // To prevent an infinite loop, bubble the error up to the next parent.
2191 - workInProgress = returnFiber;
2192 - throw error;
2193 - }
2194 -
2195 - // Return to the normal work loop.
2196 - completeUnitOfWork(unitOfWork);
2197 - return;
2198 - }
2199 -
2200 - // The work-in-progress was immediately pinged. Instead of unwinding the
2201 - // stack and potentially showing a fallback, unwind only the last stack frame,
2202 - // reset the fiber, and try rendering it again.
2186 + // This is a fork of performUnitOfWork specifcally for replaying a fiber that
2187 + // just suspended.
2188 + //
2189 + // Instead of unwinding the stack and potentially showing a fallback, unwind
2190 + // only the last stack frame, reset the fiber, and try rendering it again.
2191 const current = unitOfWork.alternate;
2192 unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
2193 unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
@@ -2232,6 +2220,55 @@ function resumeSuspendedUnitOfWork(
2220 ReactCurrentOwner.current = null;
2221 }
2222
2223 +function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2224 + // This is a fork of performUnitOfWork specifcally for unwinding a fiber
2225 + // that threw an exception.
2226 + //
2227 + // Return to the normal work loop. This will unwind the stack, and potentially
2228 + // result in showing a fallback.
2229 + workInProgressSuspendedThenableState = null;
2230 +
2231 + const returnFiber = unitOfWork.return;
2232 + if (returnFiber === null || workInProgressRoot === null) {
2233 + // Expected to be working on a non-root fiber. This is a fatal error
2234 + // because there's no ancestor that can handle it; the root is
2235 + // supposed to capture all errors that weren't caught by an error
2236 + // boundary.
2237 + workInProgressRootExitStatus = RootFatalErrored;
2238 + workInProgressRootFatalError = thrownValue;
2239 + // Set `workInProgress` to null. This represents advancing to the next
2240 + // sibling, or the parent if there are no siblings. But since the root
2241 + // has no siblings nor a parent, we set it to null. Usually this is
2242 + // handled by `completeUnitOfWork` or `unwindWork`, but since we're
2243 + // intentionally not calling those, we need set it here.
2244 + // TODO: Consider calling `unwindWork` to pop the contexts.
2245 + workInProgress = null;
2246 + return;
2247 + }
2248 +
2249 + try {
2250 + // Find and mark the nearest Suspense or error boundary that can handle
2251 + // this "exception".
2252 + throwException(
2253 + workInProgressRoot,
2254 + returnFiber,
2255 + unitOfWork,
2256 + thrownValue,
2257 + workInProgressRootRenderLanes,
2258 + );
2259 + } catch (error) {
2260 + // We had trouble processing the error. An example of this happening is
2261 + // when accessing the `componentDidCatch` property of an error boundary
2262 + // throws an error. A weird edge case. There's a regression test for this.
2263 + // To prevent an infinite loop, bubble the error up to the next parent.
2264 + workInProgress = returnFiber;
2265 + throw error;
2266 + }
2267 +
2268 + // Return to the normal work loop.
2269 + completeUnitOfWork(unitOfWork);
2270 +}
2271 +
2272 export function getSuspendedThenableState(): ThenableState | null {
2273 return workInProgressSuspendedThenableState;
2274 }