Skip to content

Commit 8844479

Browse files
author
Brian Vaughn
committed
Scheduling Profiler: Show Suspense resource .displayName
Frameworks like Relay use a convention of attributing thrown Suspense values with a displayName that provides more information about what a component is suspending on specifically. The Scheduling Profiler will now display this information instead of the component name when available. (If no displayName is available, the component name will still be shown as a fallback). Hovering over the Suspended item will show both component name and suspended resource name. (See comment below for a fallback example.)
1 parent eba248c commit 8844479

File tree

11 files changed

+83
-6
lines changed

11 files changed

+83
-6
lines changed

packages/react-devtools-scheduling-profiler/src/EventTooltip.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
white-space: nowrap;
4747
}
4848

49-
.DetailsGridURL {
49+
.DetailsGridLongValue {
5050
word-break: break-all;
5151
max-height: 50vh;
5252
overflow: hidden;

packages/react-devtools-scheduling-profiler/src/EventTooltip.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ const TooltipSuspenseEvent = ({
335335
componentName,
336336
duration,
337337
phase,
338+
promiseName,
338339
resolution,
339340
timestamp,
340341
warning,
@@ -356,6 +357,12 @@ const TooltipSuspenseEvent = ({
356357
{label}
357358
<div className={styles.Divider} />
358359
<div className={styles.DetailsGrid}>
360+
{promiseName !== null && (
361+
<>
362+
<div className={styles.DetailsGridLabel}>Resource:</div>
363+
<div className={styles.DetailsGridLongValue}>{promiseName}</div>
364+
</>
365+
)}
359366
<div className={styles.DetailsGridLabel}>Status:</div>
360367
<div>{resolution}</div>
361368
<div className={styles.DetailsGridLabel}>Timestamp:</div>

packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class SuspenseEventsView extends View {
8080
this._intrinsicSize = {
8181
width: duration,
8282
height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
83+
hideScrollBarIfLessThanHeight: ROW_WITH_BORDER_HEIGHT,
8384
maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY,
8485
};
8586
}
@@ -113,6 +114,7 @@ export class SuspenseEventsView extends View {
113114
depth,
114115
duration,
115116
phase,
117+
promiseName,
116118
resolution,
117119
timestamp,
118120
warning,
@@ -208,7 +210,9 @@ export class SuspenseEventsView extends View {
208210
);
209211

210212
let label = 'suspended';
211-
if (componentName != null) {
213+
if (promiseName != null) {
214+
label = promiseName;
215+
} else if (componentName != null) {
212216
label = `${componentName} ${label}`;
213217
}
214218
if (phase !== null) {

packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,46 @@ describe('preprocessData', () => {
11951195
`);
11961196
});
11971197

1198+
it('should include a suspended resource "displayName" if one is set', async () => {
1199+
let promise = null;
1200+
let resolvedValue = null;
1201+
function readValue(value) {
1202+
if (resolvedValue !== null) {
1203+
return resolvedValue;
1204+
} else if (promise === null) {
1205+
promise = Promise.resolve(true).then(() => {
1206+
resolvedValue = value;
1207+
});
1208+
promise.displayName = 'Testing displayName';
1209+
}
1210+
throw promise;
1211+
}
1212+
1213+
function Component() {
1214+
const value = readValue(123);
1215+
return value;
1216+
}
1217+
1218+
if (gate(flags => flags.enableSchedulingProfiler)) {
1219+
const testMarks = [creactCpuProfilerSample()];
1220+
1221+
const root = ReactDOM.createRoot(document.createElement('div'));
1222+
act(() =>
1223+
root.render(
1224+
<React.Suspense fallback="Loading...">
1225+
<Component />
1226+
</React.Suspense>,
1227+
),
1228+
);
1229+
1230+
testMarks.push(...createUserTimingData(clearedMarks));
1231+
1232+
const data = await preprocessData(testMarks);
1233+
expect(data.suspenseEvents).toHaveLength(1);
1234+
expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName');
1235+
}
1236+
});
1237+
11981238
describe('warnings', () => {
11991239
describe('long event handlers', () => {
12001240
it('should not warn when React scedules a (sync) update inside of a short event handler', async () => {

packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -564,9 +564,13 @@ function processTimelineEvent(
564564

565565
// React Events - suspense
566566
else if (name.startsWith('--suspense-suspend-')) {
567-
const [id, componentName, phase, laneBitmaskString] = name
568-
.substr(19)
569-
.split('-');
567+
const [
568+
id,
569+
componentName,
570+
phase,
571+
laneBitmaskString,
572+
promiseName,
573+
] = name.substr(19).split('-');
570574
const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
571575

572576
const availableDepths = new Array(
@@ -595,6 +599,7 @@ function processTimelineEvent(
595599
duration: null,
596600
id,
597601
phase: ((phase: any): Phase),
602+
promiseName: promiseName || null,
598603
resolution: 'unresolved',
599604
resuspendTimestamps: null,
600605
timestamp: startTime,

packages/react-devtools-scheduling-profiler/src/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type SuspenseEvent = {|
6060
duration: number | null,
6161
+id: string,
6262
+phase: Phase | null,
63+
promiseName: string | null,
6364
resolution: 'rejected' | 'resolved' | 'unresolved',
6465
resuspendTimestamps: Array<number> | null,
6566
+type: 'suspense',

packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
7070
then(callback) {
7171
callbacks.add(callback);
7272
},
73+
74+
// Optional property used by Scheduling Profiler:
75+
displayName: `Searching GitHub issues for error "${errorMessage}"`,
7376
};
7477
const wake = () => {
7578
// This assumes they won't throw.

packages/react-devtools-shared/src/dynamicImportCache.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
7373
then(callback) {
7474
callbacks.add(callback);
7575
},
76+
77+
// Optional property used by Scheduling Profiler:
78+
displayName: `Loading module "${moduleLoaderFunction.name}"`,
7679
};
7780

7881
const wake = () => {

packages/react-devtools-shared/src/hookNamesCache.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export function loadHookNames(
9292
then(callback) {
9393
callbacks.add(callback);
9494
},
95+
96+
// Optional property used by Scheduling Profiler:
97+
displayName: `Loading hook names for ${element.displayName || 'Unknown'}`,
9598
};
9699

97100
let timeoutID;

packages/react-devtools-shared/src/inspectedElementCache.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ export function inspectElement(
9494
then(callback) {
9595
callbacks.add(callback);
9696
},
97+
98+
// Optional property used by Scheduling Profiler:
99+
displayName: `Inspecting ${element.displayName || 'Unknown'}`,
97100
};
101+
98102
const wake = () => {
99103
// This assumes they won't throw.
100104
callbacks.forEach(callback => callback());

0 commit comments

Comments
 (0)