diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 43b726e981007..a244c65e3e886 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -98,6 +98,7 @@ import { Cloned, PerformedWork, ForceClientRender, + DidCapture, } from './ReactFiberFlags'; import { commitStartTime, @@ -107,14 +108,17 @@ import { resetComponentEffectTimers, pushComponentEffectStart, popComponentEffectStart, + pushComponentEffectErrors, + popComponentEffectErrors, componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, } from './ReactProfilerTimer'; import { logComponentRender, + logComponentErrored, logComponentEffect, - logSuspenseBoundaryClientRendered, } from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue'; @@ -395,7 +399,7 @@ function commitLayoutEffectOnFiber( committedLanes: Lanes, ): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // When updating this function, also update reappearLayoutEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible. const flags = finishedWork.flags; @@ -631,10 +635,12 @@ function commitLayoutEffectOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function abortRootTransitions( @@ -1627,7 +1633,7 @@ function commitMutationEffectsOnFiber( lanes: Lanes, ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); const current = finishedWork.alternate; const flags = finishedWork.flags; @@ -2136,10 +2142,12 @@ function commitMutationEffectsOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function commitReconciliationEffects(finishedWork: Fiber) { @@ -2212,7 +2220,7 @@ function recursivelyTraverseLayoutEffects( export function disappearLayoutEffects(finishedWork: Fiber) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -2285,10 +2293,12 @@ export function disappearLayoutEffects(finishedWork: Fiber) { componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) { @@ -2310,7 +2320,7 @@ export function reappearLayoutEffects( includeWorkInProgressEffects: boolean, ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // Turn on layout effects in a tree that previously disappeared. const flags = finishedWork.flags; switch (finishedWork.tag) { @@ -2461,10 +2471,12 @@ export function reappearLayoutEffects( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseReappearLayoutEffects( @@ -2701,26 +2713,7 @@ function commitPassiveMountOnFiber( endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ): void { const prevEffectStart = pushComponentEffectStart(); - - // If this component rendered in Profiling mode (DEV or in Profiler component) then log its - // render time. We do this after the fact in the passive effect to avoid the overhead of this - // getting in the way of the render characteristics and avoid the overhead of unwinding - // uncommitted renders. - if ( - enableProfilerTimer && - enableComponentPerformanceTrack && - (finishedWork.mode & ProfileMode) !== NoMode && - ((finishedWork.actualStartTime: any): number) > 0 && - (finishedWork.flags & PerformedWork) !== NoFlags - ) { - logComponentRender( - finishedWork, - ((finishedWork.actualStartTime: any): number), - endTime, - inHydratedSubtree, - ); - } - + const prevEffectErrors = pushComponentEffectErrors(); // When updating this function, also update reconnectPassiveEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible, // or when toggling effects inside a hidden tree. @@ -2729,6 +2722,25 @@ function commitPassiveMountOnFiber( case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its + // render time. We do this after the fact in the passive effect to avoid the overhead of this + // getting in the way of the render characteristics and avoid the overhead of unwinding + // uncommitted renders. + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + ((finishedWork.actualStartTime: any): number) > 0 && + (finishedWork.flags & PerformedWork) !== NoFlags + ) { + logComponentRender( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + inHydratedSubtree, + ); + } + recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -2744,6 +2756,45 @@ function commitPassiveMountOnFiber( } break; } + case ClassComponent: { + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its + // render time. We do this after the fact in the passive effect to avoid the overhead of this + // getting in the way of the render characteristics and avoid the overhead of unwinding + // uncommitted renders. + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + ((finishedWork.actualStartTime: any): number) > 0 + ) { + if ((finishedWork.flags & DidCapture) !== NoFlags) { + logComponentErrored( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + // TODO: The captured values are all hidden inside the updater/callback closures so + // we can't get to the errors but they're there so we should be able to log them. + [], + ); + } else if ((finishedWork.flags & PerformedWork) !== NoFlags) { + logComponentRender( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + inHydratedSubtree, + ); + } + } + + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + endTime, + ); + break; + } case HostRoot: { const prevEffectDuration = pushNestedEffectDurations(); @@ -2891,7 +2942,7 @@ function commitPassiveMountOnFiber( // rendered boundary. Such as postpone. if (hydrationErrors !== null) { const startTime: number = (finishedWork.actualStartTime: any); - logSuspenseBoundaryClientRendered( + logComponentErrored( finishedWork, startTime, endTime, @@ -3074,10 +3125,12 @@ function commitPassiveMountOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseReconnectPassiveEffects( @@ -3137,7 +3190,7 @@ export function reconnectPassiveEffects( endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // If this component rendered in Profiling mode (DEV or in Profiler component) then log its // render time. We do this after the fact in the passive effect to avoid the overhead of this // getting in the way of the render characteristics and avoid the overhead of unwinding @@ -3331,10 +3384,12 @@ export function reconnectPassiveEffects( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseAtomicPassiveEffects( @@ -3611,7 +3666,7 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -3696,10 +3751,12 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { @@ -3819,7 +3876,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( nearestMountedAncestor: Fiber | null, ): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (current.tag) { case FunctionComponent: case ForwardRef: @@ -3946,10 +4003,12 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } export function invokeLayoutEffectMountInDEV(fiber: Fiber): void { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 61bfd5cf7f844..78d53695a51e1 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -13,6 +13,8 @@ import type {Lanes} from './ReactFiberLane'; import type {CapturedValue} from './ReactCapturedValue'; +import {SuspenseComponent} from './ReactWorkTags'; + import getComponentNameFromFiber from './getComponentNameFromFiber'; import { @@ -159,13 +161,64 @@ export function logComponentRender( } } -export function logSuspenseBoundaryClientRendered( +export function logComponentErrored( + fiber: Fiber, + startTime: number, + endTime: number, + errors: Array>, +): void { + if (supportsUserTiming) { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } + const properties = []; + if (__DEV__) { + for (let i = 0; i < errors.length; i++) { + const capturedValue = errors[i]; + const error = capturedValue.value; + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + } + performance.measure(name, { + start: startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: COMPONENTS_TRACK, + tooltipText: + fiber.tag === SuspenseComponent + ? 'Hydration failed' + : 'Error boundary caught an error', + properties, + }, + }, + }); + } +} + +function logComponentEffectErrored( fiber: Fiber, startTime: number, endTime: number, errors: Array>, ): void { if (supportsUserTiming) { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } const properties = []; if (__DEV__) { for (let i = 0; i < errors.length; i++) { @@ -182,14 +235,14 @@ export function logSuspenseBoundaryClientRendered( properties.push(['Error', message]); } } - performance.measure('Suspense', { + performance.measure(name, { start: startTime, end: endTime, detail: { devtools: { color: 'error', track: COMPONENTS_TRACK, - tooltipText: 'Hydration failed', + tooltipText: 'A lifecycle or effect errored', properties, }, }, @@ -202,7 +255,12 @@ export function logComponentEffect( startTime: number, endTime: number, selfTime: number, + errors: null | Array>, ): void { + if (errors !== null) { + logComponentEffectErrored(fiber, startTime, endTime, errors); + return; + } const name = getComponentNameFromFiber(fiber); if (name === null) { // Skip @@ -527,7 +585,54 @@ export function logSuspendedCommitPhase( } } -export function logCommitPhase(startTime: number, endTime: number): void { +export function logCommitErrored( + startTime: number, + endTime: number, + errors: Array>, + passive: boolean, +): void { + if (supportsUserTiming) { + const properties = []; + if (__DEV__) { + for (let i = 0; i < errors.length; i++) { + const capturedValue = errors[i]; + const error = capturedValue.value; + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + } + performance.measure('Errored', { + start: startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: reusableLaneDevToolDetails.track, + trackGroup: LANES_TRACK_GROUP, + tooltipText: passive ? 'Remaining Effects Errored' : 'Commit Errored', + properties, + }, + }, + }); + } +} + +export function logCommitPhase( + startTime: number, + endTime: number, + errors: null | Array>, +): void { + if (errors !== null) { + logCommitErrored(startTime, endTime, errors, false); + return; + } if (supportsUserTiming) { reusableLaneDevToolDetails.color = 'secondary-dark'; reusableLaneOptions.start = startTime; @@ -555,7 +660,12 @@ export function logPaintYieldPhase( export function logPassiveCommitPhase( startTime: number, endTime: number, + errors: null | Array>, ): void { + if (errors !== null) { + logCommitErrored(startTime, endTime, errors, true); + return; + } if (supportsUserTiming) { reusableLaneDevToolDetails.color = 'secondary-dark'; reusableLaneOptions.start = startTime; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 2e8b443d22caa..4801aae52468d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -251,6 +251,7 @@ import { renderStartTime, commitStartTime, commitEndTime, + commitErrors, recordRenderTime, recordCommitTime, recordCommitEndTime, @@ -262,6 +263,8 @@ import { yieldStartTime, yieldReason, startPingTimerByLanes, + recordEffectError, + resetCommitErrors, } from './ReactProfilerTimer'; // DEV stuff @@ -3321,6 +3324,7 @@ function commitRootImpl( if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this // batch. This enables them to be grouped later. + resetCommitErrors(); recordCommitTime(); if (enableComponentPerformanceTrack) { if (suspendedCommitReason === SUSPENDED_COMMIT) { @@ -3414,6 +3418,7 @@ function commitRootImpl( ? completedRenderEndTime : commitStartTime, commitEndTime, + commitErrors, ); } @@ -3703,6 +3708,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { let passiveEffectStartTime = 0; if (enableProfilerTimer && enableComponentPerformanceTrack) { + resetCommitErrors(); passiveEffectStartTime = now(); logPaintYieldPhase( commitEndTime, @@ -3739,7 +3745,11 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { if (enableProfilerTimer && enableComponentPerformanceTrack) { const passiveEffectsEndTime = now(); - logPassiveCommitPhase(passiveEffectStartTime, passiveEffectsEndTime); + logPassiveCommitPhase( + passiveEffectStartTime, + passiveEffectsEndTime, + commitErrors, + ); finalizeRender(lanes, passiveEffectsEndTime); } @@ -3823,6 +3833,9 @@ function captureCommitPhaseErrorOnRoot( error: mixed, ) { const errorInfo = createCapturedValueAtFiber(error, sourceFiber); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordEffectError(errorInfo); + } const update = createRootErrorUpdate( rootFiber.stateNode, errorInfo, @@ -3864,6 +3877,9 @@ export function captureCommitPhaseError( !isAlreadyFailedLegacyErrorBoundary(instance)) ) { const errorInfo = createCapturedValueAtFiber(error, sourceFiber); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordEffectError(errorInfo); + } const update = createClassErrorUpdate((SyncLane: Lane)); const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); if (root !== null) { diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d408ba0ff7bd0..d8e6109a66f3c 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -12,6 +12,9 @@ import type {Fiber} from './ReactInternalTypes'; import type {SuspendedReason} from './ReactFiberWorkLoop'; import type {Lane, Lanes} from './ReactFiberLane'; + +import type {CapturedValue} from './ReactCapturedValue'; + import { isTransitionLane, isBlockingLane, @@ -39,11 +42,13 @@ const {unstable_now: now} = Scheduler; export let renderStartTime: number = -0; export let commitStartTime: number = -0; export let commitEndTime: number = -0; +export let commitErrors: null | Array> = null; export let profilerStartTime: number = -1.1; export let profilerEffectDuration: number = -0; export let componentEffectDuration: number = -0; export let componentEffectStartTime: number = -1.1; export let componentEffectEndTime: number = -1.1; +export let componentEffectErrors: null | Array> = null; export let blockingClampTime: number = -0; export let blockingUpdateTime: number = -1.1; // First sync setState scheduled. @@ -270,6 +275,26 @@ export function popComponentEffectStart(prevEffectStart: number): void { } } +export function pushComponentEffectErrors(): null | Array< + CapturedValue, +> { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return null; + } + const prevErrors = componentEffectErrors; + componentEffectErrors = null; + return prevErrors; +} + +export function popComponentEffectErrors( + prevErrors: null | Array>, +): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + componentEffectErrors = prevErrors; +} + /** * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect). * @@ -404,6 +429,24 @@ export function recordEffectDuration(fiber: Fiber): void { } } +export function recordEffectError(errorInfo: CapturedValue): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + if (componentEffectErrors === null) { + componentEffectErrors = []; + } + componentEffectErrors.push(errorInfo); + if (commitErrors === null) { + commitErrors = []; + } + commitErrors.push(errorInfo); +} + +export function resetCommitErrors(): void { + commitErrors = null; +} + export function startEffectTimer(): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return;