diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 81474767363fd..fb29fb45d0b4a 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -102,6 +102,7 @@ export function logComponentRender( const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; const debugTask = componentInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const properties: Array<[string, string]> = []; if (componentInfo.key != null) { @@ -110,9 +111,10 @@ export function logComponentRender( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } + debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -125,9 +127,10 @@ export function logComponentRender( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - '\u200b' + entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -152,6 +155,7 @@ export function logComponentAborted( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + const measureName = '\u200b' + entryName; if (__DEV__) { const properties: Array<[string, string]> = [ [ @@ -165,7 +169,8 @@ export function logComponentAborted( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } - performance.measure('\u200b' + entryName, { + + performance.measure(measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -178,9 +183,10 @@ export function logComponentAborted( }, }, }); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -206,6 +212,7 @@ export function logComponentErrored( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + const measureName = '\u200b' + entryName; if (__DEV__) { const message = typeof error === 'object' && @@ -222,7 +229,8 @@ export function logComponentErrored( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } - performance.measure('\u200b' + entryName, { + + performance.measure(measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -235,9 +243,10 @@ export function logComponentErrored( }, }, }); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -397,6 +406,7 @@ export function logComponentAwaitAborted( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -453,6 +463,7 @@ export function logComponentAwaitErrored( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -514,6 +525,7 @@ export function logComponentAwait( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -538,6 +550,7 @@ export function logIOInfoErrored( const description = getIODescription(error); const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); const debugTask = ioInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const message = typeof error === 'object' && @@ -550,9 +563,10 @@ export function logIOInfoErrored( const properties = [['rejected with', message]]; const tooltipText = getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected'; + debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: endTime, detail: { @@ -565,9 +579,10 @@ export function logIOInfoErrored( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, endTime, IO_TRACK, @@ -590,6 +605,7 @@ export function logIOInfo( const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); const color = getIOColor(entryName); const debugTask = ioInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const properties: Array<[string, string]> = []; if (typeof value === 'object' && value !== null) { @@ -605,7 +621,7 @@ export function logIOInfo( ); debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: endTime, detail: { @@ -618,9 +634,10 @@ export function logIOInfo( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, endTime, IO_TRACK, diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 99971f6a1a697..eeb6da60f8aeb 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -51,6 +51,7 @@ import type { ComponentFilter, ElementType, SuspenseNode, + Rect, } from 'react-devtools-shared/src/frontend/types'; import type { FrontendBridge, @@ -99,6 +100,10 @@ export type Capabilities = { supportsAdvancedProfiling: AdvancedProfiling, }; +function isNonZeroRect(rect: Rect) { + return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0; +} + /** * The store is the single source of truth for updates from the backend. * ContextProviders can subscribe to the Store for specific things they want to provide. @@ -918,7 +923,15 @@ export default class Store extends EventEmitter<{ if (current === undefined) { continue; } + // Ignore any suspense boundaries that has no visual representation as this is not + // part of the visible loading sequence. + // TODO: Consider making visible meta data and other side-effects get virtual rects. + const hasRects = + current.rects !== null && + current.rects.length > 0 && + current.rects.some(isNonZeroRect); if ( + hasRects && (!uniqueSuspendersOnly || current.hasUniqueSuspenders) && // Roots are already included as part of the Screen current.id !== rootID diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 88aeab7bfd413..633a4d382eb76 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -194,7 +194,7 @@ export default function InspectedElementWrapper(_: Props): React.Node { } let strictModeBadge = null; - if (element.isStrictModeNonCompliant) { + if (element.isStrictModeNonCompliant && element.parentID !== 0) { strictModeBadge = (
- {name} - {shortDescription === '' ? null : ( + + {skipName ? shortDescription : name} + + {skipName || shortDescription === '' ? null : ( <> {' ('} @@ -300,15 +304,141 @@ type Props = { store: Store, }; -function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number { - const ioA = a.awaited; - const ioB = b.awaited; +function withIndex( + value: SerializedAsyncInfo, + index: number, +): { + index: number, + value: SerializedAsyncInfo, +} { + return { + index, + value, + }; +} + +function compareTime( + a: { + index: number, + value: SerializedAsyncInfo, + }, + b: { + index: number, + value: SerializedAsyncInfo, + }, +): number { + const ioA = a.value.awaited; + const ioB = b.value.awaited; if (ioA.start === ioB.start) { return ioA.end - ioB.end; } return ioA.start - ioB.start; } +type GroupProps = { + bridge: FrontendBridge, + element: Element, + inspectedElement: InspectedElement, + store: Store, + name: string, + suspendedBy: Array<{ + index: number, + value: SerializedAsyncInfo, + }>, + minTime: number, + maxTime: number, +}; + +function SuspendedByGroup({ + bridge, + element, + inspectedElement, + store, + name, + suspendedBy, + minTime, + maxTime, +}: GroupProps) { + const [isOpen, setIsOpen] = useState(false); + let start = Infinity; + let end = -Infinity; + let isRejected = false; + for (let i = 0; i < suspendedBy.length; i++) { + const asyncInfo: SerializedAsyncInfo = suspendedBy[i].value; + const ioInfo = asyncInfo.awaited; + if (ioInfo.start < start) { + start = ioInfo.start; + } + if (ioInfo.end > end) { + end = ioInfo.end; + } + const value: any = ioInfo.value; + if ( + value !== null && + typeof value === 'object' && + value[meta.name] === 'rejected Thenable' + ) { + isRejected = true; + } + } + const timeScale = 100 / (maxTime - minTime); + let left = (start - minTime) * timeScale; + let width = (end - start) * timeScale; + if (width < 5) { + // Use at least a 5% width to avoid showing too small indicators. + width = 5; + if (left > 95) { + left = 95; + } + } + const pluralizedName = pluralize(name); + return ( +
+ + {isOpen && + suspendedBy.map(({value, index}) => ( + + ))} +
+ ); +} + export default function InspectedElementSuspendedBy({ bridge, element, @@ -364,9 +494,31 @@ export default function InspectedElementSuspendedBy({ minTime = maxTime - 25; } - const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0); + const sortedSuspendedBy = + suspendedBy === null ? [] : suspendedBy.map(withIndex); sortedSuspendedBy.sort(compareTime); + // Organize into groups of consecutive entries with the same name. + const groups = []; + let currentGroup = null; + let currentGroupName = null; + for (let i = 0; i < sortedSuspendedBy.length; i++) { + const entry = sortedSuspendedBy[i]; + const name = entry.value.awaited.name; + if ( + currentGroupName !== name || + !name || + name === 'Promise' || + currentGroup === null + ) { + // Create a new group. + currentGroupName = name; + currentGroup = []; + groups.push(currentGroup); + } + currentGroup.push(entry); + } + let unknownSuspenders = null; switch (inspectedElement.unknownSuspenders) { case UNKNOWN_SUSPENDERS_REASON_PRODUCTION: @@ -407,19 +559,48 @@ export default function InspectedElementSuspendedBy({
- {sortedSuspendedBy.map((asyncInfo, index) => ( - - ))} + {groups.length === 1 + ? // If it's only one type of suspender we can flatten it. + groups[0].map(entry => ( + + )) + : groups.map((entries, index) => + entries.length === 1 ? ( + + ) : ( + + ), + )} {unknownSuspenders} ); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 30ca21476f2b6..8ebb06899d62a 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useContext, useEffect, useRef} from 'react'; +import {useContext, useEffect} from 'react'; import {BridgeContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks'; @@ -29,9 +29,8 @@ function SuspenseTimelineInput() { useHighlightHostInstance(); const scrollToHostInstance = useScrollToHostInstance(); - const {timeline, timelineIndex, hoveredTimelineIndex, playing} = useContext( - SuspenseTreeStateContext, - ); + const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} = + useContext(SuspenseTreeStateContext); const min = 0; const max = timeline.length > 0 ? timeline.length - 1 : 0; @@ -102,7 +101,6 @@ function SuspenseTimelineInput() { }); } - const isInitialMount = useRef(true); // TODO: useEffectEvent here once it's supported in all versions DevTools supports. // For now we just exclude it from deps since we don't lint those anyway. function changeTimelineIndex(newIndex: number) { @@ -115,22 +113,21 @@ function SuspenseTimelineInput() { bridge.send('overrideSuspenseMilestone', { suspendedSet, }); - if (isInitialMount.current) { - // Skip scrolling on initial mount. Only when we're changing the timeline. - isInitialMount.current = false; - } else { - // When we're scrubbing through the timeline, scroll the current boundary - // into view as it was just revealed. This is after we override the milestone - // to reveal it. - const selectedSuspenseID = timeline[timelineIndex]; - scrollToHostInstance(selectedSuspenseID); - } } useEffect(() => { changeTimelineIndex(timelineIndex); }, [timelineIndex]); + useEffect(() => { + if (autoScroll.id > 0) { + const scrollToId = autoScroll.id; + // Consume the scroll ref so that we only trigger this scroll once. + autoScroll.id = 0; + scrollToHostInstance(scrollToId); + } + }, [autoScroll]); + useEffect(() => { if (!playing) { return undefined; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js index 60235e09f3935..484a336c34959 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js @@ -31,6 +31,7 @@ export type SuspenseTreeState = { uniqueSuspendersOnly: boolean, playing: boolean, autoSelect: boolean, + autoScroll: {id: number}, // Ref that's set to 0 after scrolling once. }; type ACTION_SUSPENSE_TREE_MUTATION = { @@ -125,6 +126,7 @@ function getInitialState(store: Store): SuspenseTreeState { uniqueSuspendersOnly, playing: false, autoSelect: true, + autoScroll: {id: 0}, // Don't auto-scroll initially }; return initialState; @@ -218,6 +220,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { selectedSuspenseID, playing: false, // pause autoSelect: false, + autoScroll: {id: selectedSuspenseID}, // scroll }; } case 'SET_SUSPENSE_LINEAGE': { @@ -285,6 +288,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'SUSPENSE_SKIP_TIMELINE_INDEX': { @@ -308,6 +312,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'SUSPENSE_PLAY_PAUSE': { @@ -359,6 +364,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { selectedSuspenseID: nextSelectedSuspenseID, timelineIndex: nextTimelineIndex, playing: nextPlaying, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'TOGGLE_TIMELINE_FOR_ID': { @@ -392,6 +398,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, }; } case 'HOVER_TIMELINE_FOR_ID': { diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js index ed14b2c236bd5..3b0de4118a29d 100644 --- a/packages/react-devtools-shared/src/devtools/views/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/utils.js @@ -198,3 +198,39 @@ export function truncateText(text: string, maxLength: number): string { return text; } } + +export function pluralize(word: string): string { + if (!/^[a-z]+$/i.test(word)) { + // If it's not a single a-z word, give up. + return word; + } + + switch (word) { + case 'man': + return 'men'; + case 'woman': + return 'women'; + case 'child': + return 'children'; + case 'foot': + return 'feet'; + case 'tooth': + return 'teeth'; + case 'mouse': + return 'mice'; + case 'person': + return 'people'; + } + + // Words ending in s, x, z, ch, sh → add "es" + if (/(s|x|z|ch|sh)$/i.test(word)) return word + 'es'; + + // Words ending in consonant + y → replace y with "ies" + if (/[bcdfghjklmnpqrstvwxz]y$/i.test(word)) return word.slice(0, -1) + 'ies'; + + // Words ending in f or fe → replace with "ves" + if (/(?:f|fe)$/i.test(word)) return word.replace(/(?:f|fe)$/i, 'ves'); + + // Default: just add "s" + return word + 's'; +} diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 825b814db5103..adff6647f6d41 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -496,7 +496,9 @@ function commitBeforeMutationEffectsOnFiber( } switch (finishedWork.tag) { - case FunctionComponent: { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { if (enableUseEffectEventHook) { if ((flags & Update) !== NoFlags) { const updateQueue: FunctionComponentUpdateQueue | null = @@ -513,10 +515,6 @@ function commitBeforeMutationEffectsOnFiber( } break; } - case ForwardRef: - case SimpleMemoComponent: { - break; - } case ClassComponent: { if ((flags & Snapshot) !== NoFlags) { if (current !== null) { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 65cc7f0406688..5f94bd35e0258 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -133,6 +133,7 @@ function logComponentTrigger( } else { performance.measure(trigger, reusableComponentOptions); } + performance.clearMeasures(trigger); } } @@ -200,7 +201,7 @@ const reusableComponentOptions: PerformanceMeasureOptions = { }, }; -const resuableChangedPropsEntry = ['Changed Props', '']; +const reusableChangedPropsEntry = ['Changed Props', '']; const DEEP_EQUALITY_WARNING = 'This component received deeply equal props. It might benefit from useMemo or the React Compiler in its owner.'; @@ -261,7 +262,7 @@ export function logComponentRender( alternate.memoizedProps !== props ) { // If this is an update, we'll diff the props and emit which ones changed. - const properties: Array<[string, string]> = [resuableChangedPropsEntry]; + const properties: Array<[string, string]> = [reusableChangedPropsEntry]; const isDeeplyEqual = addObjectDiffToProperties( alternate.memoizedProps, props, @@ -293,17 +294,43 @@ export function logComponentRender( reusableComponentOptions.start = startTime; reusableComponentOptions.end = endTime; + const measureName = '\u200b' + name; if (debugTask != null) { debugTask.run( // $FlowFixMe[method-unbinding] performance.measure.bind( performance, - '\u200b' + name, + measureName, reusableComponentOptions, ), ); } else { - performance.measure('\u200b' + name, reusableComponentOptions); + performance.measure(measureName, reusableComponentOptions); + } + performance.clearMeasures(measureName); + } else { + if (debugTask != null) { + debugTask.run( + // $FlowFixMe[method-unbinding] + console.timeStamp.bind( + console, + name, + startTime, + endTime, + COMPONENTS_TRACK, + undefined, + color, + ), + ); + } else { + console.timeStamp( + name, + startTime, + endTime, + COMPONENTS_TRACK, + undefined, + color, + ); } } } else { @@ -397,14 +424,17 @@ export function logComponentErrored( }, }, }; + + const measureName = '\u200b' + name; if (__DEV__ && debugTask) { debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + name, options), + performance.measure.bind(performance, measureName, options), ); } else { - performance.measure('\u200b' + name, options); + performance.measure(measureName, options); } + performance.clearMeasures(measureName); } else { console.timeStamp( name, @@ -464,14 +494,16 @@ function logComponentEffectErrored( }, }; const debugTask = fiber._debugTask; + const measureName = '\u200b' + name; if (debugTask) { debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + name, options), + performance.measure.bind(performance, measureName, options), ); } else { - performance.measure('\u200b' + name, options); + performance.measure(measureName, options); } + performance.clearMeasures(measureName); } else { console.timeStamp( name, @@ -738,6 +770,7 @@ export function logBlockingStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -843,6 +876,7 @@ export function logGestureStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -983,6 +1017,7 @@ export function logTransitionStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -1214,6 +1249,7 @@ export function logRecoveredRenderPhase( } else { performance.measure('Recovered', options); } + performance.clearMeasures('Recovered'); } else { console.timeStamp( 'Recovered', @@ -1425,6 +1461,7 @@ export function logCommitErrored( } else { performance.measure('Errored', options); } + performance.clearMeasures('Errored'); } else { console.timeStamp( 'Errored', diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js index 72310812482e4..0f5152e8d47c3 100644 --- a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js +++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js @@ -15,9 +15,20 @@ let act; let useEffect; describe('ReactPerformanceTracks', () => { + const performanceMeasureCalls = []; + beforeEach(() => { + performanceMeasureCalls.length = 0; Object.defineProperty(performance, 'measure', { - value: jest.fn(), + value: jest.fn((measureName, reusableOptions) => { + performanceMeasureCalls.push([ + measureName, + { + // React will mutate the options it passes to performance.measure. + ...reusableOptions, + }, + ]); + }), configurable: true, }); console.timeStamp = () => {}; @@ -32,6 +43,19 @@ describe('ReactPerformanceTracks', () => { useEffect = React.useEffect; }); + function getConsoleTimestampEntries() { + try { + return console.timeStamp.mock.calls.filter(call => { + const [, startTime, endTime] = call; + + const isRegisterTrackCall = startTime !== 0.003 && endTime !== 0.003; + return isRegisterTrackCall; + }); + } finally { + console.timeStamp.mockClear(); + } + } + // @gate __DEV__ && enableComponentPerformanceTrack it('shows a hint if an update is triggered by a deeply equal object', async () => { const App = function App({items}) { @@ -45,7 +69,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -62,14 +86,14 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); await act(() => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -105,7 +129,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -122,14 +146,14 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); await act(() => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -171,7 +195,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -188,7 +212,7 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); @@ -197,7 +221,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -324,4 +348,96 @@ describe('ReactPerformanceTracks', () => { ], ]); }); + + // @gate __DEV__ && enableComponentPerformanceTrack + it('includes console.timeStamp spans for Components with no prop changes', async () => { + function Left({value}) { + Scheduler.unstable_advanceTime(5000); + } + function Right() { + Scheduler.unstable_advanceTime(10000); + } + + await act(() => { + ReactNoop.render( + <> + + + , + ); + }); + + expect(performanceMeasureCalls).toEqual([ + [ + 'Mount', + { + detail: { + devtools: { + color: 'warning', + properties: null, + tooltipText: 'Mount', + track: 'Components ⚛', + }, + }, + end: 5000, + start: 0, + }, + ], + [ + 'Mount', + { + detail: { + devtools: { + color: 'warning', + properties: null, + tooltipText: 'Mount', + track: 'Components ⚛', + }, + }, + end: 15000, + start: 5000, + }, + ], + ]); + performanceMeasureCalls.length = 0; + getConsoleTimestampEntries(); + + Scheduler.unstable_advanceTime(1000); + + await act(() => { + ReactNoop.render( + <> + + + , + ); + }); + + expect(performanceMeasureCalls).toEqual([ + [ + '​Left', + { + detail: { + devtools: { + color: 'error', + properties: [ + ['Changed Props', ''], + ['– value', '1'], + ['+ value', '2'], + ], + tooltipText: 'Left', + track: 'Components ⚛', + }, + }, + end: 21000, + start: 16000, + }, + ], + ]); + expect(getConsoleTimestampEntries()).toEqual([ + ['Render', 16000, 31000, 'Blocking', 'Scheduler ⚛', 'primary-dark'], + ['Right', 21000, 31000, 'Components ⚛', undefined, 'error'], + ]); + performanceMeasureCalls.length = 0; + }); }); diff --git a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js index 17b8d6d421f77..f263c9af2693f 100644 --- a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js @@ -850,4 +850,82 @@ describe('useEffectEvent', () => { ); assertLog(['Add to cart', 'url: /shop/2, numberOfItems: 1']); }); + + it('reads the latest context value in memo Components', async () => { + const MyContext = createContext('default'); + + let logContextValue; + const ContextReader = React.memo(function ContextReader() { + const value = useContext(MyContext); + Scheduler.log('ContextReader: ' + value); + const fireLogContextValue = useEffectEvent(() => { + Scheduler.log('ContextReader (Effect event): ' + value); + }); + useEffect(() => { + logContextValue = fireLogContextValue; + }, []); + return null; + }); + + function App({value}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog(['ContextReader: first']); + + logContextValue(); + + assertLog(['ContextReader (Effect event): first']); + + await act(() => root.render()); + assertLog(['ContextReader: second']); + + logContextValue(); + assertLog(['ContextReader (Effect event): second']); + }); + + it('reads the latest context value in forwardRef Components', async () => { + const MyContext = createContext('default'); + + let logContextValue; + const ContextReader = React.forwardRef(function ContextReader(props, ref) { + const value = useContext(MyContext); + Scheduler.log('ContextReader: ' + value); + const fireLogContextValue = useEffectEvent(() => { + Scheduler.log('ContextReader (Effect event): ' + value); + }); + useEffect(() => { + logContextValue = fireLogContextValue; + }, []); + return null; + }); + + function App({value}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog(['ContextReader: first']); + + logContextValue(); + + assertLog(['ContextReader (Effect event): first']); + + await act(() => root.render()); + assertLog(['ContextReader: second']); + + logContextValue(); + assertLog(['ContextReader (Effect event): second']); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 152be2eecca57..1c769c93ee7cb 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -252,7 +252,11 @@ function findCalledFunctionNameFromStackTrace( const url = devirtualizeURL(callsite[1]); const lineNumber = callsite[2]; const columnNumber = callsite[3]; - if (filterStackFrame(url, functionName, lineNumber, columnNumber)) { + if ( + filterStackFrame(url, functionName, lineNumber, columnNumber) && + // Don't consider anonymous code first party even if the filter wants to include them in the stack. + url !== '' + ) { if (bestMatch === '') { // If we had no good stack frames for internal calls, just use the last // first party function name. @@ -308,7 +312,10 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean { const isAsync = callsite[6]; if ( !isAsync && - filterStackFrame(url, functionName, lineNumber, columnNumber) + filterStackFrame(url, functionName, lineNumber, columnNumber) && + // Ignore anonymous stack frames like internals. They are also not in first party + // code even though it might be useful to include them in the final stack. + url !== '' ) { return true; } @@ -367,7 +374,10 @@ export function isAwaitInUserspace( const url = devirtualizeURL(callsite[1]); const lineNumber = callsite[2]; const columnNumber = callsite[3]; - return filterStackFrame(url, functionName, lineNumber, columnNumber); + return ( + filterStackFrame(url, functionName, lineNumber, columnNumber) && + url !== '' + ); } return false; } @@ -2347,6 +2357,7 @@ function visitAsyncNode( } const awaited = node.awaited; let match: void | null | PromiseNode | IONode = previousIONode; + const promise = node.promise.deref(); if (awaited !== null) { const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff); if (ioNode === undefined) { @@ -2361,17 +2372,27 @@ function visitAsyncNode( if (ioNode.tag === PROMISE_NODE) { // If the ioNode was a Promise, then that means we found one in user space since otherwise // we would've returned an IO node. We assume this has the best stack. + // Note: This might also be a Promise with a displayName but potentially a worse stack. + // We could potentially favor the outer Promise if it has a stack but not the inner. match = ioNode; } else if ( - node.stack === null || - !hasUnfilteredFrame(request, node.stack) + (node.stack !== null && hasUnfilteredFrame(request, node.stack)) || + (promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string' && + (ioNode.stack === null || + !hasUnfilteredFrame(request, ioNode.stack))) ) { + // If this Promise has a stack trace then we favor that over the I/O node since we're + // mainly dealing with Promises as the abstraction. + // If it has no stack but at least has a displayName and the io doesn't have a better + // stack anyway, then also use this Promise instead since at least it has a name. + match = node; + } else { // If this Promise was created inside only third party code, then try to use // the inner I/O node instead. This could happen if third party calls into first // party to perform some I/O. match = ioNode; - } else { - match = node; } } else if (request.status === ABORTING) { if (node.start < request.abortTime && node.end > request.abortTime) { @@ -2379,8 +2400,11 @@ function visitAsyncNode( // Promise that was aborted. This won't necessarily have I/O associated with it but // it's a point of interest. if ( - node.stack !== null && - hasUnfilteredFrame(request, node.stack) + (node.stack !== null && + hasUnfilteredFrame(request, node.stack)) || + (promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string') ) { match = node; } @@ -2389,7 +2413,6 @@ function visitAsyncNode( } // We need to forward after we visit awaited nodes because what ever I/O we requested that's // the thing that generated this node and its virtual children. - const promise = node.promise.deref(); if (promise !== undefined) { const debugInfo = promise._debugInfo; if (debugInfo != null && !visited.has(debugInfo)) { @@ -4497,17 +4520,33 @@ function serializeIONode( let stack = null; let name = ''; + if (ioNode.promise !== null) { + // Pick an explicit name from the Promise itself if it exists. + // Note that we don't use the promiseRef passed in since that's sometimes the awaiting Promise + // which is the value observed but it's likely not the one with the name on it. + const promise = ioNode.promise.deref(); + if ( + promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string' + ) { + name = promise.displayName; + } + } if (ioNode.stack !== null) { // The stack can contain some leading internal frames for the construction of the promise that we skip. const fullStack = stripLeadingPromiseCreationFrames(ioNode.stack); stack = filterStackTrace(request, fullStack); - name = findCalledFunctionNameFromStackTrace(request, fullStack); - // The name can include the object that this was called on but sometimes that's - // just unnecessary context. - if (name.startsWith('Window.')) { - name = name.slice(7); - } else if (name.startsWith('.')) { - name = name.slice(7); + if (name === '') { + // If we didn't have an explicit name, try finding one from the stack. + name = findCalledFunctionNameFromStackTrace(request, fullStack); + // The name can include the object that this was called on but sometimes that's + // just unnecessary context. + if (name.startsWith('Window.')) { + name = name.slice(7); + } else if (name.startsWith('.')) { + name = name.slice(7); + } } } const owner = ioNode.owner; diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index 9253371f54a69..992b58d3880cf 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -109,6 +109,7 @@ describe('ReactFlightAsyncDebugInfo', () => { async function getData(text) { await delay(1); const promise = delay(2); + promise.displayName = 'hello'; await Promise.all([promise]); return text.toUpperCase(); } @@ -159,7 +160,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -183,7 +184,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -210,9 +211,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 26, - 116, + 117, 5, ], ], @@ -231,7 +232,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -250,9 +251,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 26, - 116, + 117, 5, ], ], @@ -267,7 +268,7 @@ describe('ReactFlightAsyncDebugInfo', () => { "awaited": { "end": 0, "env": "Server", - "name": "delay", + "name": "hello", "owner": { "env": "Server", "key": null, @@ -277,7 +278,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -304,9 +305,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 20, - 116, + 117, 5, ], ], @@ -325,7 +326,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -336,7 +337,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 112, + 113, 21, 109, 5, @@ -344,9 +345,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 20, - 116, + 117, 5, ], ], @@ -366,9 +367,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 119, + 120, 60, - 116, + 117, 5, ], ], @@ -380,7 +381,7 @@ describe('ReactFlightAsyncDebugInfo', () => { "awaited": { "end": 0, "env": "Server", - "name": "delay", + "name": "hello", "owner": { "env": "Server", "key": null, @@ -390,7 +391,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -430,9 +431,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 119, + 120, 60, - 116, + 117, 5, ], ], @@ -441,9 +442,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 125, + 126, 35, - 122, + 123, 5, ], ], @@ -624,9 +625,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -656,9 +657,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -683,17 +684,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 576, + 577, 36, - 575, + 576, 5, ], ], @@ -712,9 +713,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -731,17 +732,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 576, + 577, 36, - 575, + 576, 5, ], ], @@ -761,9 +762,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 578, + 579, 60, - 575, + 576, 5, ], ], @@ -782,9 +783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -809,17 +810,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 577, + 578, 22, - 575, + 576, 5, ], ], @@ -838,9 +839,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 578, + 579, 60, - 575, + 576, 5, ], ], @@ -849,9 +850,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 584, + 585, 40, - 581, + 582, 5, ], ], @@ -926,9 +927,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -947,9 +948,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -966,9 +967,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -1040,9 +1041,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1009, + 1010, 109, - 1000, + 1001, 94, ], ], @@ -1125,9 +1126,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1094, + 1095, 109, - 1070, + 1071, 50, ], ], @@ -1221,9 +1222,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1190, + 1191, 109, - 1173, + 1174, 63, ], ], @@ -1248,9 +1249,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1280,9 +1281,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1299,17 +1300,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1175, + 1176, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 24, - 1180, + 1181, 5, ], ], @@ -1336,9 +1337,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1347,17 +1348,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1175, + 1176, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 24, - 1180, + 1181, 5, ], ], @@ -1390,9 +1391,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1409,17 +1410,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1176, + 1177, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 18, - 1180, + 1181, 5, ], ], @@ -1446,9 +1447,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1457,17 +1458,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1176, + 1177, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 18, - 1180, + 1181, 5, ], ], @@ -1565,9 +1566,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1597,9 +1598,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1624,17 +1625,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1653,9 +1654,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1672,17 +1673,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1702,9 +1703,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1525, + 1526, 60, - 1523, + 1524, 5, ], ], @@ -1726,9 +1727,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1753,17 +1754,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1782,9 +1783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1525, + 1526, 60, - 1523, + 1524, 5, ], ], @@ -1793,9 +1794,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1519, + 1520, 28, - 1518, + 1519, 5, ], ], @@ -1878,9 +1879,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1910,9 +1911,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1937,17 +1938,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -1966,9 +1967,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1985,17 +1986,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -2015,9 +2016,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1838, + 1839, 60, - 1836, + 1837, 5, ], ], @@ -2036,9 +2037,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -2063,17 +2064,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -2087,9 +2088,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1838, + 1839, 60, - 1836, + 1837, 5, ], ], @@ -2174,9 +2175,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2206,9 +2207,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2233,17 +2234,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2128, + 2129, 13, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2262,9 +2263,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2281,17 +2282,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2128, + 2129, 13, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2313,9 +2314,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2340,25 +2341,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2122, + 2123, 13, - 2121, + 2122, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2127, + 2128, 15, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2377,9 +2378,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2396,25 +2397,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2122, + 2123, 13, - 2121, + 2122, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2127, + 2128, 15, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2436,9 +2437,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2463,9 +2464,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2123, + 2124, 13, - 2121, + 2122, 5, ], ], @@ -2484,9 +2485,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2503,9 +2504,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2123, + 2124, 13, - 2121, + 2122, 5, ], ], @@ -2578,9 +2579,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2602,9 +2603,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2621,17 +2622,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2538, + 2539, 14, - 2537, + 2538, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2544, + 2545, 20, - 2543, + 2544, 5, ], ], @@ -2650,9 +2651,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2661,17 +2662,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2538, + 2539, 23, - 2537, + 2538, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2544, + 2545, 20, - 2543, + 2544, 5, ], ], @@ -2750,9 +2751,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2782,9 +2783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2809,9 +2810,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2710, + 2711, 20, - 2709, + 2710, 5, ], ], @@ -2830,9 +2831,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2849,9 +2850,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2710, + 2711, 20, - 2709, + 2710, 5, ], ], @@ -2944,9 +2945,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -2976,9 +2977,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -2995,17 +2996,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2889, + 2890, 15, - 2888, + 2889, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 19, - 2897, + 2898, 5, ], ], @@ -3024,9 +3025,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3043,17 +3044,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2889, + 2890, 15, - 2888, + 2889, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 19, - 2897, + 2898, 5, ], ], @@ -3075,9 +3076,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3094,9 +3095,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 25, - 2897, + 2898, 5, ], ], @@ -3115,9 +3116,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3134,9 +3135,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 25, - 2897, + 2898, 5, ], ], @@ -3212,9 +3213,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3236,9 +3237,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3247,9 +3248,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3171, + 3172, 7, - 3169, + 3170, 5, ], ], @@ -3268,9 +3269,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3279,9 +3280,136 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3173, + 3174, 7, - 3169, + 3170, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + { + "awaited": { + "byteSize": 0, + "end": 0, + "name": "RSC stream", + "owner": null, + "start": 0, + "value": { + "value": "stream", + }, + }, + }, + ] + `); + } + }); + + it('can track a promise created fully outside first party code', async function internal_test() { + async function internal_API(text, timeout) { + let resolve; + const promise = new Promise(r => { + resolve = r; + }); + promise.displayName = 'greeting'; + setTimeout(() => resolve(text), timeout); + return promise; + } + + async function Component({promise}) { + const result = await promise; + return result; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + {}, + { + filterStackFrame, + }, + ); + + const readable = new Stream.PassThrough(streamOptions); + + const result = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: {}, + }); + stream.pipe(readable); + + expect(await result).toBe('hello'); + + await finishLoadingStream(readable); + if ( + __DEV__ && + gate( + flags => + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo, + ) + ) { + expect(getDebugInfo(result)).toMatchInlineSnapshot(` + [ + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + { + "time": 0, + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "greeting", + "start": 0, + "value": { + "status": "halted", + }, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3325, + 20, + 3324, 5, ], ],