@samitouri / QOS-React-2 / commits / 9403c3b536

Add Profiler callback when nested updates are scheduled (#20211)

This callback accepts the no parameters (except for the current interactions). Users of this hook can inspect the call stack to access and log the source location of the component.

Brian Vaughn committed Nov 12, 2020 at 09:31 UTC 9403c3b536f510fc54a16607bb211f630c71b55f
13 files changed +617
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+38
@@ -22,6 +22,7 @@ import {
22 replayFailedUnitOfWorkWithInvokeGuardedCallback,
23 enableProfilerTimer,
24 enableProfilerNestedUpdatePhase,
25 + enableProfilerNestedUpdateScheduledHook,
26 enableSchedulerTracing,
27 warnAboutUnmockedScheduler,
28 deferRenderPhaseUpdateToNextBatch,
@@ -110,6 +111,7 @@ import {
111 ForwardRef,
112 MemoComponent,
113 SimpleMemoComponent,
114 + Profiler,
115 } from './ReactWorkTags';
116 import {LegacyRoot} from './ReactRootTags';
117 import {
@@ -258,6 +260,10 @@ let workInProgress: Fiber | null = null;
260 // The lanes we're rendering
261 let workInProgressRootRenderLanes: Lanes = NoLanes;
262
263 +// Only used when enableProfilerNestedUpdateScheduledHook is true;
264 +// to track which root is currently committing layout effects.
265 +let rootCommittingMutationOrLayoutEffects: FiberRoot | null = null;
266 +
267 // Stack that allows components to change the render lanes for its subtree
268 // This is a superset of the lanes we started working on at the root. The only
269 // case where it's different from `workInProgressRootRenderLanes` is when we
@@ -509,6 +515,30 @@ export function scheduleUpdateOnFiber(
515 // Mark that the root has a pending update.
516 markRootUpdated(root, lane, eventTime);
517
518 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
519 + if (
520 + executionContext === CommitContext &&
521 + root === rootCommittingMutationOrLayoutEffects
522 + ) {
523 + if (fiber.mode & ProfileMode) {
524 + let current = fiber;
525 + while (current !== null) {
526 + if (current.tag === Profiler) {
527 + const {onNestedUpdateScheduled} = current.memoizedProps;
528 + if (typeof onNestedUpdateScheduled === 'function') {
529 + if (enableSchedulerTracing) {
530 + onNestedUpdateScheduled(root.memoizedInteractions);
531 + } else {
532 + onNestedUpdateScheduled();
533 + }
534 + }
535 + }
536 + current = current.return;
537 + }
538 + }
539 + }
540 + }
541 +
542 if (root === workInProgressRoot) {
543 // Received an update to a tree that's in the middle of rendering. Mark
544 // that there was an interleaved update work on this root. Unless the
@@ -1898,6 +1928,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1928 recordCommitTime();
1929 }
1930
1931 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
1932 + rootCommittingMutationOrLayoutEffects = root;
1933 + }
1934 +
1935 // The next phase is the mutation phase, where we mutate the host tree.
1936 commitMutationEffects(finishedWork, root, renderPriorityLevel);
1937
@@ -1936,6 +1970,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1970 markLayoutEffectsStopped();
1971 }
1972
1973 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
1974 + rootCommittingMutationOrLayoutEffects = null;
1975 + }
1976 +
1977 // Tell Scheduler to yield at the end of the frame, so the browser has an
1978 // opportunity to paint.
1979 requestPaint();
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+40
@@ -23,6 +23,7 @@ import {
23 enableProfilerTimer,
24 enableProfilerCommitHooks,
25 enableProfilerNestedUpdatePhase,
26 + enableProfilerNestedUpdateScheduledHook,
27 enableSchedulerTracing,
28 warnAboutUnmockedScheduler,
29 deferRenderPhaseUpdateToNextBatch,
@@ -112,6 +113,7 @@ import {
113 OffscreenComponent,
114 LegacyHiddenComponent,
115 ScopeComponent,
116 + Profiler,
117 } from './ReactWorkTags';
118 import {LegacyRoot} from './ReactRootTags';
119 import {
@@ -329,6 +331,10 @@ let hasUncaughtError = false;
331 let firstUncaughtError = null;
332 let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
333
334 +// Only used when enableProfilerNestedUpdateScheduledHook is true;
335 +// to track which root is currently committing layout effects.
336 +let rootCommittingMutationOrLayoutEffects: FiberRoot | null = null;
337 +
338 let rootDoesHavePassiveEffects: boolean = false;
339 let rootWithPendingPassiveEffects: FiberRoot | null = null;
340 let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
@@ -533,6 +539,30 @@ export function scheduleUpdateOnFiber(
539 // Mark that the root has a pending update.
540 markRootUpdated(root, lane, eventTime);
541
542 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
543 + if (
544 + executionContext === CommitContext &&
545 + root === rootCommittingMutationOrLayoutEffects
546 + ) {
547 + if (fiber.mode & ProfileMode) {
548 + let current = fiber;
549 + while (current !== null) {
550 + if (current.tag === Profiler) {
551 + const {onNestedUpdateScheduled} = current.memoizedProps;
552 + if (typeof onNestedUpdateScheduled === 'function') {
553 + if (enableSchedulerTracing) {
554 + onNestedUpdateScheduled(root.memoizedInteractions);
555 + } else {
556 + onNestedUpdateScheduled();
557 + }
558 + }
559 + }
560 + current = current.return;
561 + }
562 + }
563 + }
564 + }
565 +
566 if (root === workInProgressRoot) {
567 // Received an update to a tree that's in the middle of rendering. Mark
568 // that there was an interleaved update work on this root. Unless the
@@ -2047,6 +2077,12 @@ function commitRootImpl(root, renderPriorityLevel) {
2077 recordCommitTime();
2078 }
2079
2080 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
2081 + // Track the root here, rather than in commitLayoutEffects(), because of ref setters.
2082 + // Updates scheduled during ref detachment should also be flagged.
2083 + rootCommittingMutationOrLayoutEffects = root;
2084 + }
2085 +
2086 // The next phase is the mutation phase, where we mutate the host tree.
2087 nextEffect = firstEffect;
2088 do {
@@ -2112,6 +2148,10 @@ function commitRootImpl(root, renderPriorityLevel) {
2148
2149 nextEffect = null;
2150
2151 + if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
2152 + rootCommittingMutationOrLayoutEffects = null;
2153 + }
2154 +
2155 // Tell Scheduler to yield at the end of the frame, so the browser has an
2156 // opportunity to paint.
2157 requestPaint();
packages/react/src/__tests__/ReactProfiler-test.internal.js
+524
@@ -29,6 +29,7 @@ function loadModules({
29 enableProfilerTimer = true,
30 enableProfilerCommitHooks = true,
31 enableProfilerNestedUpdatePhase = true,
32 + enableProfilerNestedUpdateScheduledHook = false,
33 enableSchedulerTracing = true,
34 replayFailedUnitOfWorkWithInvokeGuardedCallback = false,
35 useNoopRenderer = false,
@@ -38,6 +39,7 @@ function loadModules({
39 ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
40 ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks;
41 ReactFeatureFlags.enableProfilerNestedUpdatePhase = enableProfilerNestedUpdatePhase;
42 + ReactFeatureFlags.enableProfilerNestedUpdateScheduledHook = enableProfilerNestedUpdateScheduledHook;
43 ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
44 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
45
@@ -2438,6 +2440,528 @@ describe('Profiler', () => {
2440 });
2441 }
2442 });
2443 +
2444 + describe(`onNestedUpdateScheduled enableSchedulerTracing:${
2445 + enableSchedulerTracing ? 'enabled' : 'disabled'
2446 + }`, () => {
2447 + beforeEach(() => {
2448 + jest.resetModules();
2449 +
2450 + loadModules({
2451 + enableProfilerNestedUpdateScheduledHook: true,
2452 + enableSchedulerTracing,
2453 + useNoopRenderer: true,
2454 + });
2455 + });
2456 +
2457 + it('is not called when the legacy render API is used to schedule an update', () => {
2458 + const onNestedUpdateScheduled = jest.fn();
2459 +
2460 + ReactNoop.renderLegacySyncRoot(
2461 + <React.Profiler
2462 + id="test"
2463 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2464 + <div>initial</div>
2465 + </React.Profiler>,
2466 + );
2467 +
2468 + ReactNoop.renderLegacySyncRoot(
2469 + <React.Profiler
2470 + id="test"
2471 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2472 + <div>update</div>
2473 + </React.Profiler>,
2474 + );
2475 +
2476 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2477 + });
2478 +
2479 + it('is not called when the root API is used to schedule an update', () => {
2480 + const onNestedUpdateScheduled = jest.fn();
2481 +
2482 + ReactNoop.render(
2483 + <React.Profiler
2484 + id="test"
2485 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2486 + <div>initial</div>
2487 + </React.Profiler>,
2488 + );
2489 +
2490 + ReactNoop.render(
2491 + <React.Profiler
2492 + id="test"
2493 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2494 + <div>update</div>
2495 + </React.Profiler>,
2496 + );
2497 +
2498 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2499 + });
2500 +
2501 + it('is called when a function component schedules an update during a layout effect', () => {
2502 + function Component() {
2503 + const [didMount, setDidMount] = React.useState(false);
2504 + React.useLayoutEffect(() => {
2505 + setDidMount(true);
2506 + }, []);
2507 + Scheduler.unstable_yieldValue(`Component:${didMount}`);
2508 + return didMount;
2509 + }
2510 +
2511 + const interactionCreation = {
2512 + id: 0,
2513 + name: 'creation event',
2514 + timestamp: Scheduler.unstable_now(),
2515 + };
2516 +
2517 + const onNestedUpdateScheduled = jest.fn();
2518 +
2519 + SchedulerTracing.unstable_trace(
2520 + interactionCreation.name,
2521 + Scheduler.unstable_now(),
2522 + () => {
2523 + ReactNoop.act(() => {
2524 + ReactNoop.render(
2525 + <React.Profiler
2526 + id="test"
2527 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2528 + <Component />
2529 + </React.Profiler>,
2530 + );
2531 + });
2532 + },
2533 + );
2534 +
2535 + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']);
2536 + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
2537 + if (ReactFeatureFlags.enableSchedulerTracing) {
2538 + expect(onNestedUpdateScheduled.mock.calls[0][0]).toMatchInteractions([
2539 + interactionCreation,
2540 + ]);
2541 + }
2542 + });
2543 +
2544 + it('bubbles up and calls all ancestor Profilers', () => {
2545 + function Component() {
2546 + const [didMount, setDidMount] = React.useState(false);
2547 + React.useLayoutEffect(() => {
2548 + setDidMount(true);
2549 + }, []);
2550 + Scheduler.unstable_yieldValue(`Component:${didMount}`);
2551 + return didMount;
2552 + }
2553 + const onNestedUpdateScheduledOne = jest.fn();
2554 + const onNestedUpdateScheduledTwo = jest.fn();
2555 + const onNestedUpdateScheduledThree = jest.fn();
2556 +
2557 + ReactNoop.act(() => {
2558 + ReactNoop.render(
2559 + <React.Profiler
2560 + id="one"
2561 + onNestedUpdateScheduled={onNestedUpdateScheduledOne}>
2562 + <React.Profiler
2563 + id="two"
2564 + onNestedUpdateScheduled={onNestedUpdateScheduledTwo}>
2565 + <>
2566 + <Component />
2567 + <React.Profiler
2568 + id="three"
2569 + onNestedUpdateScheduled={onNestedUpdateScheduledThree}
2570 + />
2571 + </>
2572 + </React.Profiler>
2573 + </React.Profiler>,
2574 + );
2575 + });
2576 +
2577 + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']);
2578 + expect(onNestedUpdateScheduledOne).toHaveBeenCalledTimes(1);
2579 + expect(onNestedUpdateScheduledTwo).toHaveBeenCalledTimes(1);
2580 + expect(onNestedUpdateScheduledThree).not.toHaveBeenCalled();
2581 + });
2582 +
2583 + it('is not called when an update is scheduled for another doort during a layout effect', () => {
2584 + const setStateRef = React.createRef(null);
2585 +
2586 + function ComponentRootOne() {
2587 + const [state, setState] = React.useState(false);
2588 + setStateRef.current = setState;
2589 + Scheduler.unstable_yieldValue(`ComponentRootOne:${state}`);
2590 + return state;
2591 + }
2592 +
2593 + function ComponentRootTwo() {
2594 + React.useLayoutEffect(() => {
2595 + setStateRef.current(true);
2596 + }, []);
2597 + Scheduler.unstable_yieldValue('ComponentRootTwo');
2598 + return null;
2599 + }
2600 +
2601 + const interactionCreation = {
2602 + id: 0,
2603 + name: 'creation event',
2604 + timestamp: Scheduler.unstable_now(),
2605 + };
2606 +
2607 + const onNestedUpdateScheduled = jest.fn();
2608 +
2609 + SchedulerTracing.unstable_trace(
2610 + interactionCreation.name,
2611 + Scheduler.unstable_now(),
2612 + () => {
2613 + ReactNoop.act(() => {
2614 + ReactNoop.renderToRootWithID(
2615 + <React.Profiler
2616 + id="test"
2617 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2618 + <ComponentRootOne />
2619 + </React.Profiler>,
2620 + 1,
2621 + );
2622 +
2623 + ReactNoop.renderToRootWithID(
2624 + <React.Profiler
2625 + id="test"
2626 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2627 + <ComponentRootTwo />
2628 + </React.Profiler>,
2629 + 2,
2630 + );
2631 + });
2632 + },
2633 + );
2634 +
2635 + expect(Scheduler).toHaveYielded([
2636 + 'ComponentRootOne:false',
2637 + 'ComponentRootTwo',
2638 + 'ComponentRootOne:true',
2639 + ]);
2640 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2641 + });
2642 +
2643 + it('is not called when a function component schedules an update during a passive effect', () => {
2644 + function Component() {
2645 + const [didMount, setDidMount] = React.useState(false);
2646 + React.useEffect(() => {
2647 + setDidMount(true);
2648 + }, []);
2649 + Scheduler.unstable_yieldValue(`Component:${didMount}`);
2650 + return didMount;
2651 + }
2652 +
2653 + const onNestedUpdateScheduled = jest.fn();
2654 +
2655 + ReactNoop.act(() => {
2656 + ReactNoop.render(
2657 + <React.Profiler
2658 + id="test"
2659 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2660 + <Component />
2661 + </React.Profiler>,
2662 + );
2663 + });
2664 +
2665 + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']);
2666 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2667 + });
2668 +
2669 + it('is not called when a function component schedules an update outside of render', () => {
2670 + const updateFnRef = React.createRef(null);
2671 +
2672 + function Component() {
2673 + const [state, setState] = React.useState(false);
2674 + updateFnRef.current = () => setState(true);
2675 + Scheduler.unstable_yieldValue(`Component:${state}`);
2676 + return state;
2677 + }
2678 +
2679 + const onNestedUpdateScheduled = jest.fn();
2680 +
2681 + ReactNoop.act(() => {
2682 + ReactNoop.render(
2683 + <React.Profiler
2684 + id="test"
2685 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2686 + <Component />
2687 + </React.Profiler>,
2688 + );
2689 + });
2690 + expect(Scheduler).toHaveYielded(['Component:false']);
2691 +
2692 + ReactNoop.act(() => {
2693 + updateFnRef.current();
2694 + });
2695 + expect(Scheduler).toHaveYielded(['Component:true']);
2696 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2697 + });
2698 +
2699 + it('it is not called when a component schedules an update during render', () => {
2700 + function Component() {
2701 + const [state, setState] = React.useState(false);
2702 + if (state === false) {
2703 + setState(true);
2704 + }
2705 + Scheduler.unstable_yieldValue(`Component:${state}`);
2706 + return state;
2707 + }
2708 +
2709 + const onNestedUpdateScheduled = jest.fn();
2710 +
2711 + ReactNoop.act(() => {
2712 + ReactNoop.render(
2713 + <React.Profiler
2714 + id="test"
2715 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2716 + <Component />
2717 + </React.Profiler>,
2718 + );
2719 + });
2720 +
2721 + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']);
2722 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2723 + });
2724 +
2725 + it('it is called when a component schedules an update from a ref callback', () => {
2726 + function Component({mountChild}) {
2727 + const [refAttached, setRefAttached] = React.useState(false);
2728 + const [refDetached, setRefDetached] = React.useState(false);
2729 + const refSetter = React.useCallback(ref => {
2730 + if (ref !== null) {
2731 + setRefAttached(true);
2732 + } else {
2733 + setRefDetached(true);
2734 + }
2735 + }, []);
2736 + Scheduler.unstable_yieldValue(
2737 + `Component:${refAttached}:${refDetached}`,
2738 + );
2739 + return mountChild ? <div ref={refSetter} /> : null;
2740 + }
2741 +
2742 + const onNestedUpdateScheduled = jest.fn();
2743 +
2744 + const interactionCreation = {
2745 + id: 0,
2746 + name: 'creation event',
2747 + timestamp: Scheduler.unstable_now(),
2748 + };
2749 +
2750 + SchedulerTracing.unstable_trace(
2751 + interactionCreation.name,
2752 + Scheduler.unstable_now(),
2753 + () => {
2754 + ReactNoop.act(() => {
2755 + ReactNoop.render(
2756 + <React.Profiler
2757 + id="test"
2758 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2759 + <Component mountChild={true} />
2760 + </React.Profiler>,
2761 + );
2762 + });
2763 + },
2764 + );
2765 +
2766 + expect(Scheduler).toHaveYielded([
2767 + 'Component:false:false',
2768 + 'Component:true:false',
2769 + ]);
2770 + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
2771 + if (ReactFeatureFlags.enableSchedulerTracing) {
2772 + expect(onNestedUpdateScheduled.mock.calls[0][0]).toMatchInteractions([
2773 + interactionCreation,
2774 + ]);
2775 + }
2776 +
2777 + const interactionUpdate = {
2778 + id: 1,
2779 + name: 'update event',
2780 + timestamp: Scheduler.unstable_now(),
2781 + };
2782 +
2783 + SchedulerTracing.unstable_trace(
2784 + interactionUpdate.name,
2785 + Scheduler.unstable_now(),
2786 + () => {
2787 + ReactNoop.act(() => {
2788 + ReactNoop.render(
2789 + <React.Profiler
2790 + id="test"
2791 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2792 + <Component mountChild={false} />
2793 + </React.Profiler>,
2794 + );
2795 + });
2796 + },
2797 + );
2798 +
2799 + expect(Scheduler).toHaveYielded([
2800 + 'Component:true:false',
2801 + 'Component:true:true',
2802 + ]);
2803 + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(2);
2804 + if (ReactFeatureFlags.enableSchedulerTracing) {
2805 + expect(onNestedUpdateScheduled.mock.calls[1][0]).toMatchInteractions([
2806 + interactionUpdate,
2807 + ]);
2808 + }
2809 + });
2810 +
2811 + it('is called when a class component schedules an update from the componentDidMount lifecycles', () => {
2812 + class Component extends React.Component {
2813 + state = {
2814 + value: false,
2815 + };
2816 + componentDidMount() {
2817 + this.setState({value: true});
2818 + }
2819 + render() {
2820 + const {value} = this.state;
2821 + Scheduler.unstable_yieldValue(`Component:${value}`);
2822 + return value;
2823 + }
2824 + }
2825 +
2826 + const interactionCreation = {
2827 + id: 0,
2828 + name: 'creation event',
2829 + timestamp: Scheduler.unstable_now(),
2830 + };
2831 +
2832 + const onNestedUpdateScheduled = jest.fn();
2833 +
2834 + SchedulerTracing.unstable_trace(
2835 + interactionCreation.name,
2836 + Scheduler.unstable_now(),
2837 + () => {
2838 + ReactNoop.act(() => {
2839 + ReactNoop.render(
2840 + <React.Profiler
2841 + id="test"
2842 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2843 + <Component />
2844 + </React.Profiler>,
2845 + );
2846 + });
2847 + },
2848 + );
2849 +
2850 + expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']);
2851 + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
2852 + if (ReactFeatureFlags.enableSchedulerTracing) {
2853 + expect(onNestedUpdateScheduled.mock.calls[0][0]).toMatchInteractions([
2854 + interactionCreation,
2855 + ]);
2856 + }
2857 + });
2858 +
2859 + it('is called when a class component schedules an update from the componentDidUpdate lifecycles', () => {
2860 + class Component extends React.Component {
2861 + state = {
2862 + nestedUpdateSheduled: false,
2863 + };
2864 + componentDidUpdate(prevProps, prevState) {
2865 + if (
2866 + this.props.scheduleNestedUpdate &&
2867 + !this.state.nestedUpdateSheduled
2868 + ) {
2869 + this.setState({nestedUpdateSheduled: true});
2870 + }
2871 + }
2872 + render() {
2873 + const {scheduleNestedUpdate} = this.props;
2874 + const {nestedUpdateSheduled} = this.state;
2875 + Scheduler.unstable_yieldValue(
2876 + `Component:${scheduleNestedUpdate}:${nestedUpdateSheduled}`,
2877 + );
2878 + return nestedUpdateSheduled;
2879 + }
2880 + }
2881 +
2882 + const onNestedUpdateScheduled = jest.fn();
2883 +
2884 + ReactNoop.act(() => {
2885 + ReactNoop.render(
2886 + <React.Profiler
2887 + id="test"
2888 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2889 + <Component scheduleNestedUpdate={false} />
2890 + </React.Profiler>,
2891 + );
2892 + });
2893 + expect(Scheduler).toHaveYielded(['Component:false:false']);
2894 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2895 +
2896 + const interactionCreation = {
2897 + id: 0,
2898 + name: 'creation event',
2899 + timestamp: Scheduler.unstable_now(),
2900 + };
2901 +
2902 + SchedulerTracing.unstable_trace(
2903 + interactionCreation.name,
2904 + Scheduler.unstable_now(),
2905 + () => {
2906 + ReactNoop.act(() => {
2907 + ReactNoop.render(
2908 + <React.Profiler
2909 + id="test"
2910 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2911 + <Component scheduleNestedUpdate={true} />
2912 + </React.Profiler>,
2913 + );
2914 + });
2915 + },
2916 + );
2917 +
2918 + expect(Scheduler).toHaveYielded([
2919 + 'Component:true:false',
2920 + 'Component:true:true',
2921 + ]);
2922 + expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1);
2923 + if (ReactFeatureFlags.enableSchedulerTracing) {
2924 + expect(onNestedUpdateScheduled.mock.calls[0][0]).toMatchInteractions([
2925 + interactionCreation,
2926 + ]);
2927 + }
2928 + });
2929 +
2930 + it('is not called when a class component schedules an update outside of render', () => {
2931 + const updateFnRef = React.createRef(null);
2932 +
2933 + class Component extends React.Component {
2934 + state = {
2935 + value: false,
2936 + };
2937 + render() {
2938 + const {value} = this.state;
2939 + updateFnRef.current = () => this.setState({value: true});
2940 + Scheduler.unstable_yieldValue(`Component:${value}`);
2941 + return value;
2942 + }
2943 + }
2944 +
2945 + const onNestedUpdateScheduled = jest.fn();
2946 +
2947 + ReactNoop.act(() => {
2948 + ReactNoop.render(
2949 + <React.Profiler
2950 + id="test"
2951 + onNestedUpdateScheduled={onNestedUpdateScheduled}>
2952 + <Component />
2953 + </React.Profiler>,
2954 + );
2955 + });
2956 + expect(Scheduler).toHaveYielded(['Component:false']);
2957 +
2958 + ReactNoop.act(() => {
2959 + updateFnRef.current();
2960 + });
2961 + expect(Scheduler).toHaveYielded(['Component:true']);
2962 + expect(onNestedUpdateScheduled).not.toHaveBeenCalled();
2963 + });
2964 + });
2965 });
2966
2967 describe('interaction tracing', () => {
packages/shared/ReactFeatureFlags.js
+4
@@ -39,6 +39,10 @@ export const enableProfilerCommitHooks = false;
39 // Phase param passed to onRender callback differentiates between an "update" and a "cascading-update".
40 export const enableProfilerNestedUpdatePhase = false;
41
42 +// Profiler API accepts a function to be called when a nested update is scheduled.
43 +// This callback accepts the component type (class instance or function) the update is scheduled for.
44 +export const enableProfilerNestedUpdateScheduledHook = false;
45 +
46 // Trace which interactions trigger each commit.
47 export const enableSchedulerTracing = __PROFILE__;
48
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -16,6 +16,7 @@ export const enableSchedulingProfiler = false;
16 export const enableProfilerTimer = __PROFILE__;
17 export const enableProfilerCommitHooks = false;
18 export const enableProfilerNestedUpdatePhase = false;
19 +export const enableProfilerNestedUpdateScheduledHook = false;
20 export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -18,6 +18,7 @@ export const warnAboutDeprecatedLifecycles = true;
18 export const enableProfilerTimer = __PROFILE__;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -18,6 +18,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 export const enableProfilerTimer = __PROFILE__;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -18,6 +18,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 export const enableProfilerTimer = __PROFILE__;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -18,6 +18,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 export const enableProfilerTimer = __PROFILE__;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -18,6 +18,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 export const enableProfilerTimer = __PROFILE__;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = __PROFILE__;
23 export const enableSuspenseServerRenderer = false;
24 export const enableSelectiveHydration = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -18,6 +18,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 export const enableProfilerTimer = false;
19 export const enableProfilerCommitHooks = false;
20 export const enableProfilerNestedUpdatePhase = false;
21 +export const enableProfilerNestedUpdateScheduledHook = false;
22 export const enableSchedulerTracing = false;
23 export const enableSuspenseServerRenderer = true;
24 export const enableSelectiveHydration = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+2
@@ -48,3 +48,5 @@ export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
48
49 export const enableDoubleInvokingEffects = false;
50 export const enableUseRefAccessWarning = __VARIANT__;
51 +
52 +export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -36,6 +36,8 @@ export const {
36 export const enableProfilerTimer = __PROFILE__;
37 export const enableProfilerCommitHooks = __PROFILE__;
38 export const enableProfilerNestedUpdatePhase = __PROFILE__;
39 +export const enableProfilerNestedUpdateScheduledHook =
40 + __PROFILE__ && dynamicFeatureFlags.enableProfilerNestedUpdateScheduledHook;
41
42 // Logs additional User Timing API marks for use with an experimental profiling tool.
43 export const enableSchedulingProfiler = __PROFILE__;