Skip to content

Commit

Permalink
[Transition Tracing] More Accurate End Time (#25105)
Browse files Browse the repository at this point in the history
add more accurate end time for transitions and update host configs with `requestPostPaintCallback` function and move post paint logic to another module and use it in the work loop
  • Loading branch information
lunaruan committed Sep 13, 2022
1 parent 5fdcd23 commit 0556bab
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 31 deletions.
4 changes: 4 additions & 0 deletions packages/react-art/src/ReactARTHostConfig.js
Expand Up @@ -451,3 +451,7 @@ export function preparePortalMount(portalInstance: any): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
11 changes: 10 additions & 1 deletion packages/react-dom/src/client/ReactDOMHostConfig.js
Expand Up @@ -377,7 +377,10 @@ export const cancelTimeout: any =
typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
export const noTimeout = -1;
const localPromise = typeof Promise === 'function' ? Promise : undefined;

const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: scheduleTimeout;
// -------------------
// Microtasks
// -------------------
Expand Down Expand Up @@ -1379,3 +1382,9 @@ export function setupIntersectionObserver(
},
};
}

export function requestPostPaintCallback(callback: (time: number) => void) {
localRequestAnimationFrame(() => {
localRequestAnimationFrame(time => callback(time));
});
}
4 changes: 4 additions & 0 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Expand Up @@ -611,3 +611,7 @@ export function preparePortalMount(portalInstance: Instance): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
4 changes: 4 additions & 0 deletions packages/react-native-renderer/src/ReactNativeHostConfig.js
Expand Up @@ -510,3 +510,7 @@ export function preparePortalMount(portalInstance: Instance): void {
export function detachDeletedInstance(node: Instance): void {
// noop
}

export function requestPostPaintCallback(callback: (time: number) => void) {
// noop
}
5 changes: 5 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Expand Up @@ -473,6 +473,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
logRecoverableError() {
// no-op
},

requestPostPaintCallback(callback) {
const endTime = Scheduler.unstable_now();
callback(endTime);
},
};

const hostConfig = useMutation
Expand Down
55 changes: 40 additions & 15 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Expand Up @@ -261,6 +261,7 @@ import {
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.new';
import {schedulePostPaintCallback} from './ReactPostPaintCallback';

const ceil = Math.ceil;

Expand Down Expand Up @@ -361,6 +362,7 @@ export function getWorkInProgressTransitions() {
}

let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
let currentEndTime: number | null = null;

export function addTransitionStartCallbackToPendingTransition(
transition: Transition,
Expand Down Expand Up @@ -2643,6 +2645,36 @@ function commitRootImpl(
markCommitStopped();
}

if (enableTransitionTracing) {
// We process transitions during passive effects. However, passive effects can be
// processed synchronously during the commit phase as well as asynchronously after
// paint. At the end of the commit phase, we schedule a callback that will be called
// after the next paint. If the transitions have already been processed (passive
// effect phase happened synchronously), we will schedule a callback to process
// the transitions. However, if we don't have any pending transition callbacks, this
// means that the transitions have yet to be processed (passive effects processed after paint)
// so we will store the end time of paint so that we can process the transitions
// and then call the callback via the correct end time.
const prevRootTransitionCallbacks = root.transitionCallbacks;
if (prevRootTransitionCallbacks !== null) {
schedulePostPaintCallback(endTime => {
const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
if (prevPendingTransitionCallbacks !== null) {
currentPendingTransitionCallbacks = null;
scheduleCallback(IdleSchedulerPriority, () => {
processTransitionCallbacks(
prevPendingTransitionCallbacks,
endTime,
prevRootTransitionCallbacks,
);
});
} else {
currentEndTime = endTime;
}
});
}
}

return null;
}

Expand Down Expand Up @@ -2784,28 +2816,21 @@ function flushPassiveEffectsImpl() {
if (enableTransitionTracing) {
const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
const prevRootTransitionCallbacks = root.transitionCallbacks;
const prevEndTime = currentEndTime;
if (
prevPendingTransitionCallbacks !== null &&
prevRootTransitionCallbacks !== null
prevRootTransitionCallbacks !== null &&
prevEndTime !== null
) {
// TODO(luna) Refactor this code into the Host Config
// TODO(luna) The end time here is not necessarily accurate
// because passive effects could be called before paint
// (synchronously) or after paint (normally). We need
// to come up with a way to get the correct end time for both cases.
// One solution is in the host config, if the passive effects
// have not yet been run, make a call to flush the passive effects
// right after paint.
const endTime = now();
currentPendingTransitionCallbacks = null;

scheduleCallback(IdleSchedulerPriority, () =>
currentEndTime = null;
scheduleCallback(IdleSchedulerPriority, () => {
processTransitionCallbacks(
prevPendingTransitionCallbacks,
endTime,
prevEndTime,
prevRootTransitionCallbacks,
),
);
);
});
}
}

Expand Down
55 changes: 40 additions & 15 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Expand Up @@ -261,6 +261,7 @@ import {
suspendedThenableDidResolve,
isTrackingSuspendedThenable,
} from './ReactFiberWakeable.old';
import {schedulePostPaintCallback} from './ReactPostPaintCallback';

const ceil = Math.ceil;

Expand Down Expand Up @@ -361,6 +362,7 @@ export function getWorkInProgressTransitions() {
}

let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
let currentEndTime: number | null = null;

export function addTransitionStartCallbackToPendingTransition(
transition: Transition,
Expand Down Expand Up @@ -2643,6 +2645,36 @@ function commitRootImpl(
markCommitStopped();
}

if (enableTransitionTracing) {
// We process transitions during passive effects. However, passive effects can be
// processed synchronously during the commit phase as well as asynchronously after
// paint. At the end of the commit phase, we schedule a callback that will be called
// after the next paint. If the transitions have already been processed (passive
// effect phase happened synchronously), we will schedule a callback to process
// the transitions. However, if we don't have any pending transition callbacks, this
// means that the transitions have yet to be processed (passive effects processed after paint)
// so we will store the end time of paint so that we can process the transitions
// and then call the callback via the correct end time.
const prevRootTransitionCallbacks = root.transitionCallbacks;
if (prevRootTransitionCallbacks !== null) {
schedulePostPaintCallback(endTime => {
const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
if (prevPendingTransitionCallbacks !== null) {
currentPendingTransitionCallbacks = null;
scheduleCallback(IdleSchedulerPriority, () => {
processTransitionCallbacks(
prevPendingTransitionCallbacks,
endTime,
prevRootTransitionCallbacks,
);
});
} else {
currentEndTime = endTime;
}
});
}
}

return null;
}

Expand Down Expand Up @@ -2784,28 +2816,21 @@ function flushPassiveEffectsImpl() {
if (enableTransitionTracing) {
const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
const prevRootTransitionCallbacks = root.transitionCallbacks;
const prevEndTime = currentEndTime;
if (
prevPendingTransitionCallbacks !== null &&
prevRootTransitionCallbacks !== null
prevRootTransitionCallbacks !== null &&
prevEndTime !== null
) {
// TODO(luna) Refactor this code into the Host Config
// TODO(luna) The end time here is not necessarily accurate
// because passive effects could be called before paint
// (synchronously) or after paint (normally). We need
// to come up with a way to get the correct end time for both cases.
// One solution is in the host config, if the passive effects
// have not yet been run, make a call to flush the passive effects
// right after paint.
const endTime = now();
currentPendingTransitionCallbacks = null;

scheduleCallback(IdleSchedulerPriority, () =>
currentEndTime = null;
scheduleCallback(IdleSchedulerPriority, () => {
processTransitionCallbacks(
prevPendingTransitionCallbacks,
endTime,
prevEndTime,
prevRootTransitionCallbacks,
),
);
);
});
}
}

Expand Down
26 changes: 26 additions & 0 deletions packages/react-reconciler/src/ReactPostPaintCallback.js
@@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {requestPostPaintCallback} from './ReactFiberHostConfig';

let postPaintCallbackScheduled = false;
let callbacks = [];

export function schedulePostPaintCallback(callback: (endTime: number) => void) {
callbacks.push(callback);
if (!postPaintCallbackScheduled) {
postPaintCallbackScheduled = true;
requestPostPaintCallback(endTime => {
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](endTime);
}
postPaintCallbackScheduled = false;
callbacks = [];
});
}
}
Expand Up @@ -67,6 +67,7 @@ describe('ReactFiberHostContext', () => {
return DefaultEventPriority;
},
supportsMutation: true,
requestPostPaintCallback: function() {},
});

const container = Renderer.createContainer(
Expand Down Expand Up @@ -129,6 +130,7 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function() {
return DefaultEventPriority;
},
requestPostPaintCallback: function() {},
supportsMutation: true,
});

Expand Down

0 comments on commit 0556bab

Please sign in to comment.