Skip to content

Commit d6f6b95

Browse files
author
Brian Vaughn
authored
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
1 parent b4b8a34 commit d6f6b95

File tree

4 files changed

+94
-2
lines changed

4 files changed

+94
-2
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,9 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
15121512
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
15131513
if (!retryCache.has(thenable)) {
15141514
if (enableSchedulerTracing) {
1515-
retry = Schedule_tracing_wrap(retry);
1515+
if (thenable.__reactDoNotTraceInteractions !== true) {
1516+
retry = Schedule_tracing_wrap(retry);
1517+
}
15161518
}
15171519
retryCache.add(thenable);
15181520
thenable.then(retry, retry);

packages/react-reconciler/src/ReactFiberThrow.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ function attachPingListener(
174174
renderExpirationTime,
175175
);
176176
if (enableSchedulerTracing) {
177-
ping = Schedule_tracing_wrap(ping);
177+
if (thenable.__reactDoNotTraceInteractions !== true) {
178+
ping = Schedule_tracing_wrap(ping);
179+
}
178180
}
179181
thenable.then(ping, ping);
180182
}

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ const RootCompleted = 4;
204204

205205
export type Thenable = {
206206
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
207+
208+
// Special flag to opt out of tracing interactions across a Suspense boundary.
209+
__reactDoNotTraceInteractions?: boolean,
207210
};
208211

209212
// Describes where we are in the React execution stack

packages/react/src/__tests__/ReactProfiler-test.internal.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2729,6 +2729,91 @@ describe('Profiler', () => {
27292729
onInteractionScheduledWorkCompleted.mock.calls[1][0],
27302730
).toMatchInteraction(highPriUpdateInteraction);
27312731
});
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+
});
27322817
});
27332818
});
27342819
});

0 commit comments

Comments
 (0)