Support disabling interaction tracing for suspense promises (#16776)
* Support disabling interaction tracing for suspense promises If a thrown Promise has the __reactDoNotTraceInteractions attribute, React will not wrapped its callbacks to continue tracing any current interaction(s). * Added optional '__reactDoNotTraceInteractions' attribute to Flow Thenable type
Brian Vaughn committed
Sep 13, 2019 at 08:59 UTC
d6f6b951e12e25b67f681094714a337356d2a310
4 files changed
+94
-2
packages/react-reconciler/src/ReactFiberCommitWork.js
+3
-1
@@ -1512,7 +1512,9 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
1512
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
1513
if (!retryCache.has(thenable)) {
1514
if (enableSchedulerTracing) {
1515
- retry = Schedule_tracing_wrap(retry);
1515
+ if (thenable.__reactDoNotTraceInteractions !== true) {
1516
+ retry = Schedule_tracing_wrap(retry);
1517
+ }
1518
}
1519
retryCache.add(thenable);
1520
thenable.then(retry, retry);
packages/react-reconciler/src/ReactFiberThrow.js
+3
-1
@@ -174,7 +174,9 @@ function attachPingListener(
174
renderExpirationTime,
175
);
176
if (enableSchedulerTracing) {
177
- ping = Schedule_tracing_wrap(ping);
177
+ if (thenable.__reactDoNotTraceInteractions !== true) {
178
+ ping = Schedule_tracing_wrap(ping);
179
+ }
180
}
181
thenable.then(ping, ping);
182
}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+3
@@ -204,6 +204,9 @@ const RootCompleted = 4;
204
205
export type Thenable = {
206
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
207
+
208
+ // Special flag to opt out of tracing interactions across a Suspense boundary.
209
+ __reactDoNotTraceInteractions?: boolean,
210
};
211
212
// Describes where we are in the React execution stack
packages/react/src/__tests__/ReactProfiler-test.internal.js
+85
@@ -2729,6 +2729,91 @@ describe('Profiler', () => {
2729
onInteractionScheduledWorkCompleted.mock.calls[1][0],
2730
).toMatchInteraction(highPriUpdateInteraction);
2731
});
2732
+
2733
+ it('does not trace Promises flagged with __reactDoNotTraceInteractions', async () => {
2734
+ loadModulesForTracing({useNoopRenderer: true});
2735
+
2736
+ const interaction = {
2737
+ id: 0,
2738
+ name: 'initial render',
2739
+ timestamp: Scheduler.unstable_now(),
2740
+ };
2741
+
2742
+ AsyncText = ({ms, text}) => {
2743
+ try {
2744
+ TextResource.read([text, ms]);
2745
+ Scheduler.unstable_yieldValue(`AsyncText [${text}]`);
2746
+ return text;
2747
+ } catch (promise) {
2748
+ promise.__reactDoNotTraceInteractions = true;
2749
+
2750
+ if (typeof promise.then === 'function') {
2751
+ Scheduler.unstable_yieldValue(`Suspend [${text}]`);
2752
+ } else {
2753
+ Scheduler.unstable_yieldValue(`Error [${text}]`);
2754
+ }
2755
+ throw promise;
2756
+ }
2757
+ };
2758
+
2759
+ const onRender = jest.fn();
2760
+ SchedulerTracing.unstable_trace(
2761
+ interaction.name,
2762
+ Scheduler.unstable_now(),
2763
+ () => {
2764
+ ReactNoop.render(
2765
+ <React.Profiler id="test-profiler" onRender={onRender}>
2766
+ <React.Suspense fallback={<Text text="Loading..." />}>
2767
+ <AsyncText text="Async" ms={20000} />
2768
+ </React.Suspense>
2769
+ <Text text="Sync" />
2770
+ </React.Profiler>,
2771
+ );
2772
+ },
2773
+ );
2774
+
2775
+ expect(onInteractionTraced).toHaveBeenCalledTimes(1);
2776
+ expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
2777
+ interaction,
2778
+ );
2779
+ expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
2780
+ expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
2781
+ expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);
2782
+
2783
+ expect(Scheduler).toFlushAndYield([
2784
+ 'Suspend [Async]',
2785
+ 'Text [Loading...]',
2786
+ 'Text [Sync]',
2787
+ ]);
2788
+ // Should have committed the placeholder.
2789
+ expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync');
2790
+ expect(onRender).toHaveBeenCalledTimes(1);
2791
+
2792
+ let call = onRender.mock.calls[0];
2793
+ expect(call[0]).toEqual('test-profiler');
2794
+ expect(call[6]).toMatchInteractions(
2795
+ ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
2796
+ );
2797
+
2798
+ // The interaction is now complete.
2799
+ expect(onInteractionTraced).toHaveBeenCalledTimes(1);
2800
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
2801
+ expect(
2802
+ onInteractionScheduledWorkCompleted,
2803
+ ).toHaveBeenLastNotifiedOfInteraction(interaction);
2804
+
2805
+ // Once the promise resolves, we render the suspended view
2806
+ await awaitableAdvanceTimers(20000);
2807
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
2808
+ expect(Scheduler).toFlushAndYield(['AsyncText [Async]']);
2809
+ expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync');
2810
+ expect(onRender).toHaveBeenCalledTimes(2);
2811
+
2812
+ // No interactions should be associated with this update.
2813
+ call = onRender.mock.calls[1];
2814
+ expect(call[0]).toEqual('test-profiler');
2815
+ expect(call[6]).toMatchInteractions([]);
2816
+ });
2817
});
2818
});
2819
});