[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
+}