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}`),