Read current time without marking event start time (#17160)

* Failing test: DevTools hook freezes timeline

The DevTools hook calls `requestCurrentTime` after the commit phase has
ended, which has the accidnental consequence of freezing the start
time for subsequent updates. If enough time goes by, the next update
will instantly expire.

I'll push a fix in the next commit.

* Read current time without marking event start time

`requestCurrentTime` is only meant to be used for updates, because
subsequent calls within the same event will receive the same time.
Messing this up has bad consequences.

I renamed it to `requestCurrentTimeForUpdate` and created a new
function that returns the current time without the batching heuristic,
called `getCurrentTime`.

Swapping `requestCurrentTime` for `getCurrentTime` in the DevTools
hook fixes the regression test added in the previous commit.
This commit is contained in:
Andrew Clark
2019-10-21 13:15:37 -07:00
committed by GitHub
parent 349cf5acc3
commit 1022ee0ec1
7 changed files with 54 additions and 20 deletions
+2 -2
View File
@@ -173,7 +173,7 @@ import {
} from './ReactFiber';
import {
markSpawnedWork,
requestCurrentTime,
requestCurrentTimeForUpdate,
retryDehydratedSuspenseBoundary,
scheduleWork,
renderDidSuspendDelayIfPossible,
@@ -1990,7 +1990,7 @@ function mountDehydratedSuspenseComponent(
// a protocol to transfer that time, we'll just estimate it by using the current
// time. This will mean that Suspense timeouts are slightly shifted to later than
// they should be.
let serverDisplayTime = requestCurrentTime();
let serverDisplayTime = requestCurrentTimeForUpdate();
// Schedule a normal pri update to render this content.
let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
if (enableSchedulerTracing) {
+4 -4
View File
@@ -50,7 +50,7 @@ import {
} from './ReactFiberContext';
import {readContext} from './ReactFiberNewContext';
import {
requestCurrentTime,
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleWork,
} from './ReactFiberWorkLoop';
@@ -183,7 +183,7 @@ const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
@@ -205,7 +205,7 @@ const classComponentUpdater = {
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
@@ -229,7 +229,7 @@ const classComponentUpdater = {
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
+2 -2
View File
@@ -8,7 +8,7 @@
*/
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
import {requestCurrentTime} from './ReactFiberWorkLoop';
import {getCurrentTime} from './ReactFiberWorkLoop';
import {inferPriorityFromExpirationTime} from './ReactFiberExpirationTime';
import type {Fiber} from './ReactFiber';
@@ -58,7 +58,7 @@ export function injectInternals(internals: Object): boolean {
try {
const didError = (root.current.effectTag & DidCapture) === DidCapture;
if (enableProfilerTimer) {
const currentTime = requestCurrentTime();
const currentTime = getCurrentTime();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
+2 -2
View File
@@ -39,7 +39,7 @@ import {
import {
scheduleWork,
computeExpirationForFiber,
requestCurrentTime,
requestCurrentTimeForUpdate,
warnIfNotCurrentlyActingEffectsInDEV,
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotScopedWithMatchingAct,
@@ -1273,7 +1273,7 @@ function dispatchAction<S, A>(
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
+10 -6
View File
@@ -50,7 +50,7 @@ import {
import {createFiberRoot} from './ReactFiberRoot';
import {injectInternals} from './ReactFiberDevToolsHook';
import {
requestCurrentTime,
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleWork,
flushRoot,
@@ -231,7 +231,7 @@ export function updateContainer(
callback: ?Function,
): ExpirationTime {
const current = container.current;
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
@@ -348,7 +348,9 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
// If we're still blocked after this, we need to increase
// the priority of any promises resolving within this
// boundary so that they next attempt also has higher pri.
let retryExpTime = computeInteractiveExpiration(requestCurrentTime());
let retryExpTime = computeInteractiveExpiration(
requestCurrentTimeForUpdate(),
);
markRetryTimeIfNotHydrated(fiber, retryExpTime);
break;
}
@@ -380,7 +382,7 @@ export function attemptUserBlockingHydration(fiber: Fiber): void {
// Suspense.
return;
}
let expTime = computeInteractiveExpiration(requestCurrentTime());
let expTime = computeInteractiveExpiration(requestCurrentTimeForUpdate());
scheduleWork(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
}
@@ -393,7 +395,9 @@ export function attemptContinuousHydration(fiber: Fiber): void {
// Suspense.
return;
}
let expTime = computeContinuousHydrationExpiration(requestCurrentTime());
let expTime = computeContinuousHydrationExpiration(
requestCurrentTimeForUpdate(),
);
scheduleWork(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
}
@@ -404,7 +408,7 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
// their priority other than synchronously flush it.
return;
}
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const expTime = computeExpirationForFiber(currentTime, fiber, null);
scheduleWork(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
+8 -4
View File
@@ -288,7 +288,7 @@ let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
// receive the same expiration time. Otherwise we get tearing.
let currentEventTime: ExpirationTime = NoWork;
export function requestCurrentTime() {
export function requestCurrentTimeForUpdate() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
@@ -303,6 +303,10 @@ export function requestCurrentTime() {
return currentEventTime;
}
export function getCurrentTime() {
return msToExpirationTime(now());
}
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
@@ -571,7 +575,7 @@ function ensureRootIsScheduled(root: FiberRoot) {
// TODO: If this is an update, we already read the current time. Pass the
// time as an argument.
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
@@ -632,7 +636,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
if (didTimeout) {
// The render task took too long to complete. Mark the current time as
// expired to synchronously render all expired work in a single batch.
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
markRootExpiredAtTime(root, currentTime);
// This will schedule a synchronous callback.
ensureRootIsScheduled(root);
@@ -2380,7 +2384,7 @@ function retryTimedOutBoundary(
// likely unblocked. Try rendering again, at a new expiration time.
if (retryTime === NoWork) {
const suspenseConfig = null; // Retries don't carry over the already committed update.
const currentTime = requestCurrentTime();
const currentTime = requestCurrentTimeForUpdate();
retryTime = computeExpirationForFiber(
currentTime,
boundaryFiber,
@@ -177,4 +177,30 @@ describe('ReactProfiler DevTools integration', () => {
{name: 'some event', timestamp: eventTime},
]);
});
it('regression test: #17159', () => {
function Text({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
// Commit something
root.update(<Text text="A" />);
expect(Scheduler).toFlushAndYield(['A']);
expect(root).toMatchRenderedOutput('A');
// Advance time by many seconds, larger than the default expiration time
// for updates.
Scheduler.unstable_advanceTime(10000);
// Schedule an update.
root.update(<Text text="B" />);
// Update B should not instantly expire.
expect(Scheduler).toFlushExpired([]);
expect(Scheduler).toFlushAndYield(['B']);
expect(root).toMatchRenderedOutput('B');
});
});