@samitouri / QOS-React-2 / commits / 0556bab32c

[Transition Tracing] More Accurate End Time (#25105)

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

Luna Ruan committed Sep 13, 2022 at 13:55 UTC 0556bab32c4350971619350d1efc230baf2ea147
12 files changed +367 -31
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -451,3 +451,7 @@ export function preparePortalMount(portalInstance: any): void {
451 export function detachDeletedInstance(node: Instance): void {
452 // noop
453 }
454 +
455 +export function requestPostPaintCallback(callback: (time: number) => void) {
456 + // noop
457 +}
packages/react-dom/src/client/ReactDOMHostConfig.js
+10 -1
@@ -377,7 +377,10 @@ export const cancelTimeout: any =
377 typeof clearTimeout === 'function' ? clearTimeout : (undefined: any);
378 export const noTimeout = -1;
379 const localPromise = typeof Promise === 'function' ? Promise : undefined;
380 -
380 +const localRequestAnimationFrame =
381 + typeof requestAnimationFrame === 'function'
382 + ? requestAnimationFrame
383 + : scheduleTimeout;
384 // -------------------
385 // Microtasks
386 // -------------------
@@ -1379,3 +1382,9 @@ export function setupIntersectionObserver(
1382 },
1383 };
1384 }
1385 +
1386 +export function requestPostPaintCallback(callback: (time: number) => void) {
1387 + localRequestAnimationFrame(() => {
1388 + localRequestAnimationFrame(time => callback(time));
1389 + });
1390 +}
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -611,3 +611,7 @@ export function preparePortalMount(portalInstance: Instance): void {
611 export function detachDeletedInstance(node: Instance): void {
612 // noop
613 }
614 +
615 +export function requestPostPaintCallback(callback: (time: number) => void) {
616 + // noop
617 +}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -510,3 +510,7 @@ export function preparePortalMount(portalInstance: Instance): void {
510 export function detachDeletedInstance(node: Instance): void {
511 // noop
512 }
513 +
514 +export function requestPostPaintCallback(callback: (time: number) => void) {
515 + // noop
516 +}
packages/react-noop-renderer/src/createReactNoop.js
+5
@@ -473,6 +473,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
473 logRecoverableError() {
474 // no-op
475 },
476 +
477 + requestPostPaintCallback(callback) {
478 + const endTime = Scheduler.unstable_now();
479 + callback(endTime);
480 + },
481 };
482
483 const hostConfig = useMutation
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+40 -15
@@ -261,6 +261,7 @@ import {
261 suspendedThenableDidResolve,
262 isTrackingSuspendedThenable,
263 } from './ReactFiberWakeable.new';
264 +import {schedulePostPaintCallback} from './ReactPostPaintCallback';
265
266 const ceil = Math.ceil;
267
@@ -361,6 +362,7 @@ export function getWorkInProgressTransitions() {
362 }
363
364 let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
365 +let currentEndTime: number | null = null;
366
367 export function addTransitionStartCallbackToPendingTransition(
368 transition: Transition,
@@ -2643,6 +2645,36 @@ function commitRootImpl(
2645 markCommitStopped();
2646 }
2647
2648 + if (enableTransitionTracing) {
2649 + // We process transitions during passive effects. However, passive effects can be
2650 + // processed synchronously during the commit phase as well as asynchronously after
2651 + // paint. At the end of the commit phase, we schedule a callback that will be called
2652 + // after the next paint. If the transitions have already been processed (passive
2653 + // effect phase happened synchronously), we will schedule a callback to process
2654 + // the transitions. However, if we don't have any pending transition callbacks, this
2655 + // means that the transitions have yet to be processed (passive effects processed after paint)
2656 + // so we will store the end time of paint so that we can process the transitions
2657 + // and then call the callback via the correct end time.
2658 + const prevRootTransitionCallbacks = root.transitionCallbacks;
2659 + if (prevRootTransitionCallbacks !== null) {
2660 + schedulePostPaintCallback(endTime => {
2661 + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2662 + if (prevPendingTransitionCallbacks !== null) {
2663 + currentPendingTransitionCallbacks = null;
2664 + scheduleCallback(IdleSchedulerPriority, () => {
2665 + processTransitionCallbacks(
2666 + prevPendingTransitionCallbacks,
2667 + endTime,
2668 + prevRootTransitionCallbacks,
2669 + );
2670 + });
2671 + } else {
2672 + currentEndTime = endTime;
2673 + }
2674 + });
2675 + }
2676 + }
2677 +
2678 return null;
2679 }
2680
@@ -2784,28 +2816,21 @@ function flushPassiveEffectsImpl() {
2816 if (enableTransitionTracing) {
2817 const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2818 const prevRootTransitionCallbacks = root.transitionCallbacks;
2819 + const prevEndTime = currentEndTime;
2820 if (
2821 prevPendingTransitionCallbacks !== null &&
2789 - prevRootTransitionCallbacks !== null
2822 + prevRootTransitionCallbacks !== null &&
2823 + prevEndTime !== null
2824 ) {
2791 - // TODO(luna) Refactor this code into the Host Config
2792 - // TODO(luna) The end time here is not necessarily accurate
2793 - // because passive effects could be called before paint
2794 - // (synchronously) or after paint (normally). We need
2795 - // to come up with a way to get the correct end time for both cases.
2796 - // One solution is in the host config, if the passive effects
2797 - // have not yet been run, make a call to flush the passive effects
2798 - // right after paint.
2799 - const endTime = now();
2825 currentPendingTransitionCallbacks = null;
2801 -
2802 - scheduleCallback(IdleSchedulerPriority, () =>
2826 + currentEndTime = null;
2827 + scheduleCallback(IdleSchedulerPriority, () => {
2828 processTransitionCallbacks(
2829 prevPendingTransitionCallbacks,
2805 - endTime,
2830 + prevEndTime,
2831 prevRootTransitionCallbacks,
2807 - ),
2808 - );
2832 + );
2833 + });
2834 }
2835 }
2836
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+40 -15
@@ -261,6 +261,7 @@ import {
261 suspendedThenableDidResolve,
262 isTrackingSuspendedThenable,
263 } from './ReactFiberWakeable.old';
264 +import {schedulePostPaintCallback} from './ReactPostPaintCallback';
265
266 const ceil = Math.ceil;
267
@@ -361,6 +362,7 @@ export function getWorkInProgressTransitions() {
362 }
363
364 let currentPendingTransitionCallbacks: PendingTransitionCallbacks | null = null;
365 +let currentEndTime: number | null = null;
366
367 export function addTransitionStartCallbackToPendingTransition(
368 transition: Transition,
@@ -2643,6 +2645,36 @@ function commitRootImpl(
2645 markCommitStopped();
2646 }
2647
2648 + if (enableTransitionTracing) {
2649 + // We process transitions during passive effects. However, passive effects can be
2650 + // processed synchronously during the commit phase as well as asynchronously after
2651 + // paint. At the end of the commit phase, we schedule a callback that will be called
2652 + // after the next paint. If the transitions have already been processed (passive
2653 + // effect phase happened synchronously), we will schedule a callback to process
2654 + // the transitions. However, if we don't have any pending transition callbacks, this
2655 + // means that the transitions have yet to be processed (passive effects processed after paint)
2656 + // so we will store the end time of paint so that we can process the transitions
2657 + // and then call the callback via the correct end time.
2658 + const prevRootTransitionCallbacks = root.transitionCallbacks;
2659 + if (prevRootTransitionCallbacks !== null) {
2660 + schedulePostPaintCallback(endTime => {
2661 + const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2662 + if (prevPendingTransitionCallbacks !== null) {
2663 + currentPendingTransitionCallbacks = null;
2664 + scheduleCallback(IdleSchedulerPriority, () => {
2665 + processTransitionCallbacks(
2666 + prevPendingTransitionCallbacks,
2667 + endTime,
2668 + prevRootTransitionCallbacks,
2669 + );
2670 + });
2671 + } else {
2672 + currentEndTime = endTime;
2673 + }
2674 + });
2675 + }
2676 + }
2677 +
2678 return null;
2679 }
2680
@@ -2784,28 +2816,21 @@ function flushPassiveEffectsImpl() {
2816 if (enableTransitionTracing) {
2817 const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
2818 const prevRootTransitionCallbacks = root.transitionCallbacks;
2819 + const prevEndTime = currentEndTime;
2820 if (
2821 prevPendingTransitionCallbacks !== null &&
2789 - prevRootTransitionCallbacks !== null
2822 + prevRootTransitionCallbacks !== null &&
2823 + prevEndTime !== null
2824 ) {
2791 - // TODO(luna) Refactor this code into the Host Config
2792 - // TODO(luna) The end time here is not necessarily accurate
2793 - // because passive effects could be called before paint
2794 - // (synchronously) or after paint (normally). We need
2795 - // to come up with a way to get the correct end time for both cases.
2796 - // One solution is in the host config, if the passive effects
2797 - // have not yet been run, make a call to flush the passive effects
2798 - // right after paint.
2799 - const endTime = now();
2825 currentPendingTransitionCallbacks = null;
2801 -
2802 - scheduleCallback(IdleSchedulerPriority, () =>
2826 + currentEndTime = null;
2827 + scheduleCallback(IdleSchedulerPriority, () => {
2828 processTransitionCallbacks(
2829 prevPendingTransitionCallbacks,
2805 - endTime,
2830 + prevEndTime,
2831 prevRootTransitionCallbacks,
2807 - ),
2808 - );
2832 + );
2833 + });
2834 }
2835 }
2836
packages/react-reconciler/src/ReactPostPaintCallback.js new
+26
@@ -0,0 +1,26 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +import {requestPostPaintCallback} from './ReactFiberHostConfig';
10 +
11 +let postPaintCallbackScheduled = false;
12 +let callbacks = [];
13 +
14 +export function schedulePostPaintCallback(callback: (endTime: number) => void) {
15 + callbacks.push(callback);
16 + if (!postPaintCallbackScheduled) {
17 + postPaintCallbackScheduled = true;
18 + requestPostPaintCallback(endTime => {
19 + for (let i = 0; i < callbacks.length; i++) {
20 + callbacks[i](endTime);
21 + }
22 + postPaintCallbackScheduled = false;
23 + callbacks = [];
24 + });
25 + }
26 +}
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+2
@@ -67,6 +67,7 @@ describe('ReactFiberHostContext', () => {
67 return DefaultEventPriority;
68 },
69 supportsMutation: true,
70 + requestPostPaintCallback: function() {},
71 });
72
73 const container = Renderer.createContainer(
@@ -129,6 +130,7 @@ describe('ReactFiberHostContext', () => {
130 getCurrentEventPriority: function() {
131 return DefaultEventPriority;
132 },
133 + requestPostPaintCallback: function() {},
134 supportsMutation: true,
135 });
136
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+227
@@ -2273,4 +2273,231 @@ describe('ReactInteractionTracing', () => {
2273 });
2274 expect(Scheduler).toHaveYielded(['Hidden Text']);
2275 });
2276 +
2277 + // @gate enableTransitionTracing
2278 + it('discrete events', async () => {
2279 + const transitionCallbacks = {
2280 + onTransitionStart: (name, startTime) => {
2281 + Scheduler.unstable_yieldValue(
2282 + `onTransitionStart(${name}, ${startTime})`,
2283 + );
2284 + },
2285 + onTransitionProgress: (name, startTime, endTime, pending) => {
2286 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
2287 + Scheduler.unstable_yieldValue(
2288 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
2289 + );
2290 + },
2291 + onTransitionComplete: (name, startTime, endTime) => {
2292 + Scheduler.unstable_yieldValue(
2293 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
2294 + );
2295 + },
2296 + };
2297 +
2298 + function App() {
2299 + return (
2300 + <Suspense
2301 + fallback={<Text text="Loading..." />}
2302 + unstable_name="suspense page">
2303 + <AsyncText text="Page Two" />
2304 + </Suspense>
2305 + );
2306 + }
2307 +
2308 + const root = ReactNoop.createRoot({
2309 + unstable_transitionCallbacks: transitionCallbacks,
2310 + });
2311 +
2312 + await act(async () => {
2313 + ReactNoop.discreteUpdates(() =>
2314 + startTransition(() => root.render(<App />), {name: 'page transition'}),
2315 + );
2316 + ReactNoop.expire(1000);
2317 + await advanceTimers(1000);
2318 + });
2319 +
2320 + expect(Scheduler).toHaveYielded([
2321 + 'Suspend [Page Two]',
2322 + 'Loading...',
2323 + 'onTransitionStart(page transition, 0)',
2324 + 'onTransitionProgress(page transition, 0, 1000, [suspense page])',
2325 + ]);
2326 + await act(async () => {
2327 + ReactNoop.discreteUpdates(() => resolveText('Page Two'));
2328 + ReactNoop.expire(1000);
2329 + await advanceTimers(1000);
2330 + });
2331 +
2332 + expect(Scheduler).toHaveYielded([
2333 + 'Page Two',
2334 + 'onTransitionProgress(page transition, 0, 2000, [])',
2335 + 'onTransitionComplete(page transition, 0, 2000)',
2336 + ]);
2337 + });
2338 +
2339 + // @gate enableTransitionTracing
2340 + it('multiple commits happen before a paint', async () => {
2341 + const transitionCallbacks = {
2342 + onTransitionStart: (name, startTime) => {
2343 + Scheduler.unstable_yieldValue(
2344 + `onTransitionStart(${name}, ${startTime})`,
2345 + );
2346 + },
2347 + onTransitionProgress: (name, startTime, endTime, pending) => {
2348 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
2349 + Scheduler.unstable_yieldValue(
2350 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
2351 + );
2352 + },
2353 + onTransitionComplete: (name, startTime, endTime) => {
2354 + Scheduler.unstable_yieldValue(
2355 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
2356 + );
2357 + },
2358 + };
2359 +
2360 + function App() {
2361 + const [, setRerender] = useState(false);
2362 + React.useLayoutEffect(() => {
2363 + resolveText('Text');
2364 + setRerender(true);
2365 + });
2366 + return (
2367 + <>
2368 + <Suspense unstable_name="one" fallback={<Text text="Loading..." />}>
2369 + <AsyncText text="Text" />
2370 + </Suspense>
2371 + <Suspense
2372 + unstable_name="two"
2373 + fallback={<Text text="Loading Two..." />}>
2374 + <AsyncText text="Text Two" />
2375 + </Suspense>
2376 + </>
2377 + );
2378 + }
2379 +
2380 + const root = ReactNoop.createRoot({
2381 + unstable_transitionCallbacks: transitionCallbacks,
2382 + });
2383 +
2384 + await act(() => {
2385 + startTransition(() => root.render(<App />), {name: 'transition'});
2386 + ReactNoop.expire(1000);
2387 + advanceTimers(1000);
2388 + });
2389 +
2390 + expect(Scheduler).toHaveYielded([
2391 + 'Suspend [Text]',
2392 + 'Loading...',
2393 + 'Suspend [Text Two]',
2394 + 'Loading Two...',
2395 + 'Text',
2396 + 'Suspend [Text Two]',
2397 + 'Loading Two...',
2398 + 'onTransitionStart(transition, 0)',
2399 + 'onTransitionProgress(transition, 0, 1000, [two])',
2400 + ]);
2401 +
2402 + await act(() => {
2403 + resolveText('Text Two');
2404 + ReactNoop.expire(1000);
2405 + advanceTimers(1000);
2406 + });
2407 + expect(Scheduler).toHaveYielded([
2408 + 'Text Two',
2409 + 'onTransitionProgress(transition, 0, 2000, [])',
2410 + 'onTransitionComplete(transition, 0, 2000)',
2411 + ]);
2412 + });
2413 +
2414 + // @gate enableTransitionTracing
2415 + it('transition callbacks work for multiple roots', async () => {
2416 + const getTransitionCallbacks = transitionName => {
2417 + return {
2418 + onTransitionStart: (name, startTime) => {
2419 + Scheduler.unstable_yieldValue(
2420 + `onTransitionStart(${name}, ${startTime}) /${transitionName}/`,
2421 + );
2422 + },
2423 + onTransitionProgress: (name, startTime, endTime, pending) => {
2424 + const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
2425 + Scheduler.unstable_yieldValue(
2426 + `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}]) /${transitionName}/`,
2427 + );
2428 + },
2429 + onTransitionComplete: (name, startTime, endTime) => {
2430 + Scheduler.unstable_yieldValue(
2431 + `onTransitionComplete(${name}, ${startTime}, ${endTime}) /${transitionName}/`,
2432 + );
2433 + },
2434 + };
2435 + };
2436 +
2437 + function App({name}) {
2438 + return (
2439 + <>
2440 + <Suspense
2441 + unstable_name={name}
2442 + fallback={<Text text={`Loading ${name}...`} />}>
2443 + <AsyncText text={`Text ${name}`} />
2444 + </Suspense>
2445 + </>
2446 + );
2447 + }
2448 +
2449 + const rootOne = ReactNoop.createRoot({
2450 + unstable_transitionCallbacks: getTransitionCallbacks('root one'),
2451 + });
2452 +
2453 + const rootTwo = ReactNoop.createRoot({
2454 + unstable_transitionCallbacks: getTransitionCallbacks('root two'),
2455 + });
2456 +
2457 + await act(() => {
2458 + startTransition(() => rootOne.render(<App name="one" />), {
2459 + name: 'transition one',
2460 + });
2461 + startTransition(() => rootTwo.render(<App name="two" />), {
2462 + name: 'transition two',
2463 + });
2464 + ReactNoop.expire(1000);
2465 + advanceTimers(1000);
2466 + });
2467 +
2468 + expect(Scheduler).toHaveYielded([
2469 + 'Suspend [Text one]',
2470 + 'Loading one...',
2471 + 'Suspend [Text two]',
2472 + 'Loading two...',
2473 + 'onTransitionStart(transition one, 0) /root one/',
2474 + 'onTransitionProgress(transition one, 0, 1000, [one]) /root one/',
2475 + 'onTransitionStart(transition two, 0) /root two/',
2476 + 'onTransitionProgress(transition two, 0, 1000, [two]) /root two/',
2477 + ]);
2478 +
2479 + await act(() => {
2480 + caches[0].resolve('Text one');
2481 + ReactNoop.expire(1000);
2482 + advanceTimers(1000);
2483 + });
2484 +
2485 + expect(Scheduler).toHaveYielded([
2486 + 'Text one',
2487 + 'onTransitionProgress(transition one, 0, 2000, []) /root one/',
2488 + 'onTransitionComplete(transition one, 0, 2000) /root one/',
2489 + ]);
2490 +
2491 + await act(() => {
2492 + resolveText('Text two');
2493 + ReactNoop.expire(1000);
2494 + advanceTimers(1000);
2495 + });
2496 +
2497 + expect(Scheduler).toHaveYielded([
2498 + 'Text two',
2499 + 'onTransitionProgress(transition two, 0, 3000, []) /root two/',
2500 + 'onTransitionComplete(transition two, 0, 3000) /root two/',
2501 + ]);
2502 + });
2503 });
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -67,6 +67,7 @@ export const prepareScopeUpdate = $$$hostConfig.prepareScopeUpdate;
67 export const getInstanceFromScope = $$$hostConfig.getInstanceFromScope;
68 export const getCurrentEventPriority = $$$hostConfig.getCurrentEventPriority;
69 export const detachDeletedInstance = $$$hostConfig.detachDeletedInstance;
70 +export const requestPostPaintCallback = $$$hostConfig.requestPostPaintCallback;
71
72 // -------------------
73 // Microtasks
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -317,3 +317,7 @@ export function detachDeletedInstance(node: Instance): void {
317 export function logRecoverableError(error: mixed): void {
318 // noop
319 }
320 +
321 +export function requestPostPaintCallback(callback: (time: number) => void) {
322 + // noop
323 +}