diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css
index 5ed92c5462746..bf3862892bd7d 100644
--- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.css
+++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.css
@@ -46,7 +46,7 @@
white-space: nowrap;
}
-.DetailsGridURL {
+.DetailsGridLongValue {
word-break: break-all;
max-height: 50vh;
overflow: hidden;
diff --git a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js
index 353cf7fc00bb8..9152c7de8f56e 100644
--- a/packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+++ b/packages/react-devtools-scheduling-profiler/src/EventTooltip.js
@@ -335,6 +335,7 @@ const TooltipSuspenseEvent = ({
componentName,
duration,
phase,
+ promiseName,
resolution,
timestamp,
warning,
@@ -356,6 +357,12 @@ const TooltipSuspenseEvent = ({
{label}
+ {promiseName !== null && (
+ <>
+
Resource:
+
{promiseName}
+ >
+ )}
Status:
{resolution}
Timestamp:
diff --git a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
index b6fa643f60054..58601d3590f53 100644
--- a/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
+++ b/packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
@@ -80,6 +80,7 @@ export class SuspenseEventsView extends View {
this._intrinsicSize = {
width: duration,
height: (this._maxDepth + 1) * ROW_WITH_BORDER_HEIGHT,
+ hideScrollBarIfLessThanHeight: ROW_WITH_BORDER_HEIGHT,
maxInitialHeight: ROW_WITH_BORDER_HEIGHT * MAX_ROWS_TO_SHOW_INITIALLY,
};
}
@@ -113,6 +114,7 @@ export class SuspenseEventsView extends View {
depth,
duration,
phase,
+ promiseName,
resolution,
timestamp,
warning,
@@ -208,7 +210,9 @@ export class SuspenseEventsView extends View {
);
let label = 'suspended';
- if (componentName != null) {
+ if (promiseName != null) {
+ label = promiseName;
+ } else if (componentName != null) {
label = `${componentName} ${label}`;
}
if (phase !== null) {
diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
index 11af34c9680a8..e24611e9daea5 100644
--- a/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+++ b/packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
@@ -1195,6 +1195,46 @@ describe('preprocessData', () => {
`);
});
+ it('should include a suspended resource "displayName" if one is set', async () => {
+ let promise = null;
+ let resolvedValue = null;
+ function readValue(value) {
+ if (resolvedValue !== null) {
+ return resolvedValue;
+ } else if (promise === null) {
+ promise = Promise.resolve(true).then(() => {
+ resolvedValue = value;
+ });
+ promise.displayName = 'Testing displayName';
+ }
+ throw promise;
+ }
+
+ function Component() {
+ const value = readValue(123);
+ return value;
+ }
+
+ if (gate(flags => flags.enableSchedulingProfiler)) {
+ const testMarks = [creactCpuProfilerSample()];
+
+ const root = ReactDOM.createRoot(document.createElement('div'));
+ act(() =>
+ root.render(
+
+
+ ,
+ ),
+ );
+
+ testMarks.push(...createUserTimingData(clearedMarks));
+
+ const data = await preprocessData(testMarks);
+ expect(data.suspenseEvents).toHaveLength(1);
+ expect(data.suspenseEvents[0].promiseName).toBe('Testing displayName');
+ }
+ });
+
describe('warnings', () => {
describe('long event handlers', () => {
it('should not warn when React scedules a (sync) update inside of a short event handler', async () => {
diff --git a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
index 02ba5c31c8c29..aff78142b81a6 100644
--- a/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+++ b/packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
@@ -564,9 +564,13 @@ function processTimelineEvent(
// React Events - suspense
else if (name.startsWith('--suspense-suspend-')) {
- const [id, componentName, phase, laneBitmaskString] = name
- .substr(19)
- .split('-');
+ const [
+ id,
+ componentName,
+ phase,
+ laneBitmaskString,
+ promiseName,
+ ] = name.substr(19).split('-');
const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString);
const availableDepths = new Array(
@@ -595,6 +599,7 @@ function processTimelineEvent(
duration: null,
id,
phase: ((phase: any): Phase),
+ promiseName: promiseName || null,
resolution: 'unresolved',
resuspendTimestamps: null,
timestamp: startTime,
diff --git a/packages/react-devtools-scheduling-profiler/src/types.js b/packages/react-devtools-scheduling-profiler/src/types.js
index ff68f3435e5ff..4bfafe9a2eccc 100644
--- a/packages/react-devtools-scheduling-profiler/src/types.js
+++ b/packages/react-devtools-scheduling-profiler/src/types.js
@@ -60,6 +60,7 @@ export type SuspenseEvent = {|
duration: number | null,
+id: string,
+phase: Phase | null,
+ promiseName: string | null,
resolution: 'rejected' | 'resolved' | 'unresolved',
resuspendTimestamps: Array
| null,
+type: 'suspense',
diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js
index 71ee78e979bd9..9e5061cb804ee 100644
--- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js
+++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js
@@ -70,6 +70,9 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
then(callback) {
callbacks.add(callback);
},
+
+ // Optional property used by Scheduling Profiler:
+ displayName: `Searching GitHub issues for error "${errorMessage}"`,
};
const wake = () => {
// This assumes they won't throw.
diff --git a/packages/react-devtools-shared/src/dynamicImportCache.js b/packages/react-devtools-shared/src/dynamicImportCache.js
index 78045856abb88..72c0aa708db50 100644
--- a/packages/react-devtools-shared/src/dynamicImportCache.js
+++ b/packages/react-devtools-shared/src/dynamicImportCache.js
@@ -73,6 +73,9 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
then(callback) {
callbacks.add(callback);
},
+
+ // Optional property used by Scheduling Profiler:
+ displayName: `Loading module "${moduleLoaderFunction.name}"`,
};
const wake = () => {
diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js
index 32179f9de1cbf..106c10fc8ac12 100644
--- a/packages/react-devtools-shared/src/hookNamesCache.js
+++ b/packages/react-devtools-shared/src/hookNamesCache.js
@@ -92,6 +92,9 @@ export function loadHookNames(
then(callback) {
callbacks.add(callback);
},
+
+ // Optional property used by Scheduling Profiler:
+ displayName: `Loading hook names for ${element.displayName || 'Unknown'}`,
};
let timeoutID;
diff --git a/packages/react-devtools-shared/src/inspectedElementCache.js b/packages/react-devtools-shared/src/inspectedElementCache.js
index 040a6cb19b6a6..e0e43e4022b20 100644
--- a/packages/react-devtools-shared/src/inspectedElementCache.js
+++ b/packages/react-devtools-shared/src/inspectedElementCache.js
@@ -94,7 +94,11 @@ export function inspectElement(
then(callback) {
callbacks.add(callback);
},
+
+ // Optional property used by Scheduling Profiler:
+ displayName: `Inspecting ${element.displayName || 'Unknown'}`,
};
+
const wake = () => {
// This assumes they won't throw.
callbacks.forEach(callback => callback());
diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js
index 9b0985cf46139..03148d499282d 100644
--- a/packages/react-reconciler/src/SchedulingProfiler.js
+++ b/packages/react-reconciler/src/SchedulingProfiler.js
@@ -194,9 +194,16 @@ export function markComponentSuspended(
const id = getWakeableID(wakeable);
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
const phase = fiber.alternate === null ? 'mount' : 'update';
+
+ // Following the non-standard fn.displayName convention,
+ // frameworks like Relay may also annotate Promises with a displayName,
+ // describing what operation/data the thrown Promise is related to.
+ // When this is available we should pass it along to the Scheduling Profiler.
+ const displayName = (wakeable: any).displayName || '';
+
// TODO (scheduling profiler) Add component stack id
markAndClear(
- `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}`,
+ `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`,
);
wakeable.then(
() => markAndClear(`--suspense-resolved-${id}-${componentName}`),