Skip to content

Commit

Permalink
Support disabling interaction tracing for suspense promises (#16776)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Brian Vaughn authored Sep 13, 2019
1 parent b4b8a34 commit d6f6b95
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,9 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) {
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
if (!retryCache.has(thenable)) {
if (enableSchedulerTracing) {
retry = Schedule_tracing_wrap(retry);
if (thenable.__reactDoNotTraceInteractions !== true) {
retry = Schedule_tracing_wrap(retry);
}
}
retryCache.add(thenable);
thenable.then(retry, retry);
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberThrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ function attachPingListener(
renderExpirationTime,
);
if (enableSchedulerTracing) {
ping = Schedule_tracing_wrap(ping);
if (thenable.__reactDoNotTraceInteractions !== true) {
ping = Schedule_tracing_wrap(ping);
}
}
thenable.then(ping, ping);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const RootCompleted = 4;

export type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,

// Special flag to opt out of tracing interactions across a Suspense boundary.
__reactDoNotTraceInteractions?: boolean,
};

// Describes where we are in the React execution stack
Expand Down
85 changes: 85 additions & 0 deletions packages/react/src/__tests__/ReactProfiler-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,91 @@ describe('Profiler', () => {
onInteractionScheduledWorkCompleted.mock.calls[1][0],
).toMatchInteraction(highPriUpdateInteraction);
});

it('does not trace Promises flagged with __reactDoNotTraceInteractions', async () => {
loadModulesForTracing({useNoopRenderer: true});

const interaction = {
id: 0,
name: 'initial render',
timestamp: Scheduler.unstable_now(),
};

AsyncText = ({ms, text}) => {
try {
TextResource.read([text, ms]);
Scheduler.unstable_yieldValue(`AsyncText [${text}]`);
return text;
} catch (promise) {
promise.__reactDoNotTraceInteractions = true;

if (typeof promise.then === 'function') {
Scheduler.unstable_yieldValue(`Suspend [${text}]`);
} else {
Scheduler.unstable_yieldValue(`Error [${text}]`);
}
throw promise;
}
};

const onRender = jest.fn();
SchedulerTracing.unstable_trace(
interaction.name,
Scheduler.unstable_now(),
() => {
ReactNoop.render(
<React.Profiler id="test-profiler" onRender={onRender}>
<React.Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="Async" ms={20000} />
</React.Suspense>
<Text text="Sync" />
</React.Profiler>,
);
},
);

expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
interaction,
);
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0);
expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0);

expect(Scheduler).toFlushAndYield([
'Suspend [Async]',
'Text [Loading...]',
'Text [Sync]',
]);
// Should have committed the placeholder.
expect(ReactNoop.getChildrenAsJSX()).toEqual('Loading...Sync');
expect(onRender).toHaveBeenCalledTimes(1);

let call = onRender.mock.calls[0];
expect(call[0]).toEqual('test-profiler');
expect(call[6]).toMatchInteractions(
ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [],
);

// The interaction is now complete.
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
expect(
onInteractionScheduledWorkCompleted,
).toHaveBeenLastNotifiedOfInteraction(interaction);

// Once the promise resolves, we render the suspended view
await awaitableAdvanceTimers(20000);
expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
expect(Scheduler).toFlushAndYield(['AsyncText [Async]']);
expect(ReactNoop.getChildrenAsJSX()).toEqual('AsyncSync');
expect(onRender).toHaveBeenCalledTimes(2);

// No interactions should be associated with this update.
call = onRender.mock.calls[1];
expect(call[0]).toEqual('test-profiler');
expect(call[6]).toMatchInteractions([]);
});
});
});
});

0 comments on commit d6f6b95

Please sign in to comment.