Skip to content

Commit 0e8a5af

Browse files
author
Brian Vaughn
authoredOct 21, 2021
Scheduling Profiler: Add marks for component effects (mount and unmount) (#22578)
DevTools (and its profilers) should not require users to be familiar with React internals. Although the scheduling profiler includes a CPU sample flame graph, it's there for advanced use cases and shouldn't be required to identify common performance issues. This PR proposes adding new marks around component effects. This will enable users to identify components with slow effect create/destroy functions without requiring them to dig through the call stack. (Once #22529 lands, these new marks will also include component stacks, making them more useful still.) For example, here's a profile with a long-running effect. Without this change, it's not clear why the effects phase takes so long. After this change, it's more clear why that the phase is longer because of a specific component. We may consider adding similar marks around render phase hooks like useState, useReducer, useMemo. I avoided doing that in this PR because it would be a pretty pervasive change to the ReactFiberHooks file. Note that this change should have no effect on production bundles since everything is guarded behind a profiling feature flag. Going to tag more people than I normally would for this pR, since it touches both reconciler and DevTools packages. Feel free to ignore though if you don't have strong feelings. Note that although this PR adds new marks to the scheduling profiler, it's done in a way that's backwards compatible for older profiles.
1 parent 4ba2057 commit 0e8a5af

File tree

10 files changed

+649
-127
lines changed

10 files changed

+649
-127
lines changed
 

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,26 @@ const TooltipReactComponentMeasure = ({
140140
}: {|
141141
componentMeasure: ReactComponentMeasure,
142142
|}) => {
143-
const {componentName, duration, timestamp, warning} = componentMeasure;
143+
const {componentName, duration, timestamp, type, warning} = componentMeasure;
144144

145-
const label = `${componentName} rendered`;
145+
let label = componentName;
146+
switch (type) {
147+
case 'render':
148+
label += ' rendered';
149+
break;
150+
case 'layout-effect-mount':
151+
label += ' mounted layout effect';
152+
break;
153+
case 'layout-effect-unmount':
154+
label += ' unmounted layout effect';
155+
break;
156+
case 'passive-effect-mount':
157+
label += ' mounted passive effect';
158+
break;
159+
case 'passive-effect-unmount':
160+
label += ' unmounted passive effect';
161+
break;
162+
}
146163

147164
return (
148165
<>

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

+51-6
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@ export class ComponentMeasuresView extends View {
7676
showHoverHighlight: boolean,
7777
): boolean {
7878
const {frame} = this;
79-
const {componentName, duration, timestamp, warning} = componentMeasure;
79+
const {
80+
componentName,
81+
duration,
82+
timestamp,
83+
type,
84+
warning,
85+
} = componentMeasure;
8086

8187
const xStart = timestampToPosition(timestamp, scaleFactor, frame);
8288
const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
@@ -96,16 +102,53 @@ export class ComponentMeasuresView extends View {
96102
return false; // Too small to render at this zoom level
97103
}
98104

105+
let textFillStyle = ((null: any): string);
106+
let typeLabel = ((null: any): string);
107+
99108
const drawableRect = intersectionOfRects(componentMeasureRect, rect);
100109
context.beginPath();
101110
if (warning !== null) {
102111
context.fillStyle = showHoverHighlight
103112
? COLORS.WARNING_BACKGROUND_HOVER
104113
: COLORS.WARNING_BACKGROUND;
105114
} else {
106-
context.fillStyle = showHoverHighlight
107-
? COLORS.REACT_COMPONENT_MEASURE_HOVER
108-
: COLORS.REACT_COMPONENT_MEASURE;
115+
switch (type) {
116+
case 'render':
117+
context.fillStyle = showHoverHighlight
118+
? COLORS.REACT_RENDER_HOVER
119+
: COLORS.REACT_RENDER;
120+
textFillStyle = COLORS.REACT_RENDER_TEXT;
121+
typeLabel = 'rendered';
122+
break;
123+
case 'layout-effect-mount':
124+
context.fillStyle = showHoverHighlight
125+
? COLORS.REACT_LAYOUT_EFFECTS_HOVER
126+
: COLORS.REACT_LAYOUT_EFFECTS;
127+
textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
128+
typeLabel = 'mounted layout effect';
129+
break;
130+
case 'layout-effect-unmount':
131+
context.fillStyle = showHoverHighlight
132+
? COLORS.REACT_LAYOUT_EFFECTS_HOVER
133+
: COLORS.REACT_LAYOUT_EFFECTS;
134+
textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
135+
typeLabel = 'unmounted layout effect';
136+
break;
137+
case 'passive-effect-mount':
138+
context.fillStyle = showHoverHighlight
139+
? COLORS.REACT_PASSIVE_EFFECTS_HOVER
140+
: COLORS.REACT_PASSIVE_EFFECTS;
141+
textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
142+
typeLabel = 'mounted passive effect';
143+
break;
144+
case 'passive-effect-unmount':
145+
context.fillStyle = showHoverHighlight
146+
? COLORS.REACT_PASSIVE_EFFECTS_HOVER
147+
: COLORS.REACT_PASSIVE_EFFECTS;
148+
textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
149+
typeLabel = 'unmounted passive effect';
150+
break;
151+
}
109152
}
110153
context.fillRect(
111154
drawableRect.origin.x,
@@ -114,9 +157,11 @@ export class ComponentMeasuresView extends View {
114157
drawableRect.size.height,
115158
);
116159

117-
const label = `${componentName} rendered - ${formatDuration(duration)}`;
160+
const label = `${componentName} ${typeLabel} - ${formatDuration(duration)}`;
118161

119-
drawText(label, context, componentMeasureRect, drawableRect);
162+
drawText(label, context, componentMeasureRect, drawableRect, {
163+
fillStyle: textFillStyle,
164+
});
120165

121166
return true;
122167
}

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

-8
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ export let COLORS = {
5959
PRIORITY_LABEL: '',
6060
USER_TIMING: '',
6161
USER_TIMING_HOVER: '',
62-
REACT_COMPONENT_MEASURE: '',
63-
REACT_COMPONENT_MEASURE_HOVER: '',
6462
REACT_IDLE: '',
6563
REACT_IDLE_HOVER: '',
6664
REACT_RENDER: '',
@@ -150,12 +148,6 @@ export function updateColorsToMatchTheme(element: Element): boolean {
150148
USER_TIMING_HOVER: computedStyle.getPropertyValue(
151149
'--color-scheduling-profiler-user-timing-hover',
152150
),
153-
REACT_COMPONENT_MEASURE: computedStyle.getPropertyValue(
154-
'--color-scheduling-profiler-react-render',
155-
),
156-
REACT_COMPONENT_MEASURE_HOVER: computedStyle.getPropertyValue(
157-
'--color-scheduling-profiler-react-render-hover',
158-
),
159151
REACT_IDLE: computedStyle.getPropertyValue(
160152
'--color-scheduling-profiler-react-idle',
161153
),

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

+39-23
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ describe('preprocessData', () => {
840840
Object {
841841
"batchUID": 0,
842842
"depth": 0,
843-
"duration": 0.0019999999999999983,
843+
"duration": 0.004,
844844
"lanes": Array [
845845
4,
846846
],
@@ -852,11 +852,11 @@ describe('preprocessData', () => {
852852
Object {
853853
"batchUID": 1,
854854
"depth": 0,
855-
"duration": 0.010000000000000002,
855+
"duration": 0.009999999999999998,
856856
"lanes": Array [
857857
4,
858858
],
859-
"timestamp": 0.019,
859+
"timestamp": 0.021,
860860
"type": "render-idle",
861861
},
862862
Object {
@@ -866,37 +866,37 @@ describe('preprocessData', () => {
866866
"lanes": Array [
867867
4,
868868
],
869-
"timestamp": 0.019,
869+
"timestamp": 0.021,
870870
"type": "render",
871871
},
872872
Object {
873873
"batchUID": 1,
874874
"depth": 0,
875-
"duration": 0.006000000000000002,
875+
"duration": 0.005999999999999998,
876876
"lanes": Array [
877877
4,
878878
],
879-
"timestamp": 0.023,
879+
"timestamp": 0.025,
880880
"type": "commit",
881881
},
882882
Object {
883883
"batchUID": 1,
884884
"depth": 1,
885-
"duration": 0.0010000000000000009,
885+
"duration": 0.0009999999999999974,
886886
"lanes": Array [
887887
4,
888888
],
889-
"timestamp": 0.027,
889+
"timestamp": 0.029,
890890
"type": "layout-effects",
891891
},
892892
Object {
893893
"batchUID": 1,
894894
"depth": 0,
895-
"duration": 0.0010000000000000009,
895+
"duration": 0.0030000000000000027,
896896
"lanes": Array [
897897
4,
898898
],
899-
"timestamp": 0.03,
899+
"timestamp": 0.032,
900900
"type": "passive-effects",
901901
},
902902
],
@@ -906,16 +906,32 @@ describe('preprocessData', () => {
906906
"componentName": "App",
907907
"duration": 0.001,
908908
"timestamp": 0.006,
909+
"type": "render",
910+
"warning": null,
911+
},
912+
Object {
913+
"componentName": "App",
914+
"duration": 0.0019999999999999983,
915+
"timestamp": 0.017,
916+
"type": "passive-effect-mount",
909917
"warning": null,
910918
},
911919
Object {
912920
"componentName": "App",
913921
"duration": 0.0010000000000000009,
914-
"timestamp": 0.02,
922+
"timestamp": 0.022,
923+
"type": "render",
924+
"warning": null,
925+
},
926+
Object {
927+
"componentName": "App",
928+
"duration": 0.0010000000000000009,
929+
"timestamp": 0.033,
930+
"type": "passive-effect-mount",
915931
"warning": null,
916932
},
917933
],
918-
"duration": 0.031,
934+
"duration": 0.035,
919935
"flamechart": Array [],
920936
"internalModuleSourceToRanges": Map {},
921937
"laneToLabelMap": Map {
@@ -1000,7 +1016,7 @@ describe('preprocessData', () => {
10001016
Object {
10011017
"batchUID": 0,
10021018
"depth": 0,
1003-
"duration": 0.0019999999999999983,
1019+
"duration": 0.004,
10041020
"lanes": Array [
10051021
4,
10061022
],
@@ -1010,11 +1026,11 @@ describe('preprocessData', () => {
10101026
Object {
10111027
"batchUID": 1,
10121028
"depth": 0,
1013-
"duration": 0.010000000000000002,
1029+
"duration": 0.009999999999999998,
10141030
"lanes": Array [
10151031
4,
10161032
],
1017-
"timestamp": 0.019,
1033+
"timestamp": 0.021,
10181034
"type": "render-idle",
10191035
},
10201036
Object {
@@ -1024,37 +1040,37 @@ describe('preprocessData', () => {
10241040
"lanes": Array [
10251041
4,
10261042
],
1027-
"timestamp": 0.019,
1043+
"timestamp": 0.021,
10281044
"type": "render",
10291045
},
10301046
Object {
10311047
"batchUID": 1,
10321048
"depth": 0,
1033-
"duration": 0.006000000000000002,
1049+
"duration": 0.005999999999999998,
10341050
"lanes": Array [
10351051
4,
10361052
],
1037-
"timestamp": 0.023,
1053+
"timestamp": 0.025,
10381054
"type": "commit",
10391055
},
10401056
Object {
10411057
"batchUID": 1,
10421058
"depth": 1,
1043-
"duration": 0.0010000000000000009,
1059+
"duration": 0.0009999999999999974,
10441060
"lanes": Array [
10451061
4,
10461062
],
1047-
"timestamp": 0.027,
1063+
"timestamp": 0.029,
10481064
"type": "layout-effects",
10491065
},
10501066
Object {
10511067
"batchUID": 1,
10521068
"depth": 0,
1053-
"duration": 0.0010000000000000009,
1069+
"duration": 0.0030000000000000027,
10541070
"lanes": Array [
10551071
4,
10561072
],
1057-
"timestamp": 0.03,
1073+
"timestamp": 0.032,
10581074
"type": "passive-effects",
10591075
},
10601076
],
@@ -1108,7 +1124,7 @@ describe('preprocessData', () => {
11081124
"lanes": Array [
11091125
4,
11101126
],
1111-
"timestamp": 0.017,
1127+
"timestamp": 0.018,
11121128
"type": "schedule-state-update",
11131129
"warning": null,
11141130
},

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

+156-25
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
Phase,
2323
ReactLane,
2424
ReactComponentMeasure,
25+
ReactComponentMeasureType,
2526
ReactMeasure,
2627
ReactMeasureType,
2728
ReactProfilerData,
@@ -484,31 +485,13 @@ function processTimelineEvent(
484485
} else if (name.startsWith('--react-lane-labels-')) {
485486
const [laneLabelTuplesString] = name.substr(20).split('-');
486487
updateLaneToLabelMap(currentProfilerData, laneLabelTuplesString);
487-
} else if (name.startsWith('--component-render-start-')) {
488-
const [componentName] = name.substr(25).split('-');
489-
490-
if (state.currentReactComponentMeasure !== null) {
491-
console.error(
492-
'Render started while another render in progress:',
493-
state.currentReactComponentMeasure,
494-
);
495-
}
496-
497-
state.currentReactComponentMeasure = {
498-
componentName,
499-
timestamp: startTime,
500-
duration: 0,
501-
warning: null,
502-
};
503-
} else if (name === '--component-render-stop') {
504-
if (state.currentReactComponentMeasure !== null) {
505-
const componentMeasure = state.currentReactComponentMeasure;
506-
componentMeasure.duration = startTime - componentMeasure.timestamp;
507-
508-
state.currentReactComponentMeasure = null;
509-
510-
currentProfilerData.componentMeasures.push(componentMeasure);
511-
}
488+
} else if (name.startsWith('--component-')) {
489+
processReactComponentMeasure(
490+
name,
491+
startTime,
492+
currentProfilerData,
493+
state,
494+
);
512495
} else if (name.startsWith('--schedule-render-')) {
513496
const [laneBitmaskString] = name.substr(18).split('-');
514497

@@ -868,6 +851,154 @@ function processTimelineEvent(
868851
}
869852
}
870853

854+
function assertNoOverlappingComponentMeasure(state: ProcessorState) {
855+
if (state.currentReactComponentMeasure !== null) {
856+
console.error(
857+
'Component measure started while another measure in progress:',
858+
state.currentReactComponentMeasure,
859+
);
860+
}
861+
}
862+
863+
function assertCurrentComponentMeasureType(
864+
state: ProcessorState,
865+
type: ReactComponentMeasureType,
866+
): void {
867+
if (state.currentReactComponentMeasure === null) {
868+
console.error(
869+
`Component measure type "${type}" stopped while no measure was in progress`,
870+
);
871+
} else if (state.currentReactComponentMeasure.type !== type) {
872+
console.error(
873+
`Component measure type "${type}" stopped while type ${state.currentReactComponentMeasure.type} in progress`,
874+
);
875+
}
876+
}
877+
878+
function processReactComponentMeasure(
879+
name: string,
880+
startTime: Milliseconds,
881+
currentProfilerData: ReactProfilerData,
882+
state: ProcessorState,
883+
): void {
884+
if (name.startsWith('--component-render-start-')) {
885+
const [componentName] = name.substr(25).split('-');
886+
887+
assertNoOverlappingComponentMeasure(state);
888+
889+
state.currentReactComponentMeasure = {
890+
componentName,
891+
timestamp: startTime,
892+
duration: 0,
893+
type: 'render',
894+
warning: null,
895+
};
896+
} else if (name === '--component-render-stop') {
897+
assertCurrentComponentMeasureType(state, 'render');
898+
899+
if (state.currentReactComponentMeasure !== null) {
900+
const componentMeasure = state.currentReactComponentMeasure;
901+
componentMeasure.duration = startTime - componentMeasure.timestamp;
902+
903+
state.currentReactComponentMeasure = null;
904+
905+
currentProfilerData.componentMeasures.push(componentMeasure);
906+
}
907+
} else if (name.startsWith('--component-layout-effect-mount-start-')) {
908+
const [componentName] = name.substr(38).split('-');
909+
910+
assertNoOverlappingComponentMeasure(state);
911+
912+
state.currentReactComponentMeasure = {
913+
componentName,
914+
timestamp: startTime,
915+
duration: 0,
916+
type: 'layout-effect-mount',
917+
warning: null,
918+
};
919+
} else if (name === '--component-layout-effect-mount-stop') {
920+
assertCurrentComponentMeasureType(state, 'layout-effect-mount');
921+
922+
if (state.currentReactComponentMeasure !== null) {
923+
const componentMeasure = state.currentReactComponentMeasure;
924+
componentMeasure.duration = startTime - componentMeasure.timestamp;
925+
926+
state.currentReactComponentMeasure = null;
927+
928+
currentProfilerData.componentMeasures.push(componentMeasure);
929+
}
930+
} else if (name.startsWith('--component-layout-effect-unmount-start-')) {
931+
const [componentName] = name.substr(40).split('-');
932+
933+
assertNoOverlappingComponentMeasure(state);
934+
935+
state.currentReactComponentMeasure = {
936+
componentName,
937+
timestamp: startTime,
938+
duration: 0,
939+
type: 'layout-effect-unmount',
940+
warning: null,
941+
};
942+
} else if (name === '--component-layout-effect-unmount-stop') {
943+
assertCurrentComponentMeasureType(state, 'layout-effect-unmount');
944+
945+
if (state.currentReactComponentMeasure !== null) {
946+
const componentMeasure = state.currentReactComponentMeasure;
947+
componentMeasure.duration = startTime - componentMeasure.timestamp;
948+
949+
state.currentReactComponentMeasure = null;
950+
951+
currentProfilerData.componentMeasures.push(componentMeasure);
952+
}
953+
} else if (name.startsWith('--component-passive-effect-mount-start-')) {
954+
const [componentName] = name.substr(39).split('-');
955+
956+
assertNoOverlappingComponentMeasure(state);
957+
958+
state.currentReactComponentMeasure = {
959+
componentName,
960+
timestamp: startTime,
961+
duration: 0,
962+
type: 'passive-effect-mount',
963+
warning: null,
964+
};
965+
} else if (name === '--component-passive-effect-mount-stop') {
966+
assertCurrentComponentMeasureType(state, 'passive-effect-mount');
967+
968+
if (state.currentReactComponentMeasure !== null) {
969+
const componentMeasure = state.currentReactComponentMeasure;
970+
componentMeasure.duration = startTime - componentMeasure.timestamp;
971+
972+
state.currentReactComponentMeasure = null;
973+
974+
currentProfilerData.componentMeasures.push(componentMeasure);
975+
}
976+
} else if (name.startsWith('--component-passive-effect-unmount-start-')) {
977+
const [componentName] = name.substr(41).split('-');
978+
979+
assertNoOverlappingComponentMeasure(state);
980+
981+
state.currentReactComponentMeasure = {
982+
componentName,
983+
timestamp: startTime,
984+
duration: 0,
985+
type: 'passive-effect-unmount',
986+
warning: null,
987+
};
988+
} else if (name === '--component-passive-effect-unmount-stop') {
989+
assertCurrentComponentMeasureType(state, 'passive-effect-unmount');
990+
991+
if (state.currentReactComponentMeasure !== null) {
992+
const componentMeasure = state.currentReactComponentMeasure;
993+
componentMeasure.duration = startTime - componentMeasure.timestamp;
994+
995+
state.currentReactComponentMeasure = null;
996+
997+
currentProfilerData.componentMeasures.push(componentMeasure);
998+
}
999+
}
1000+
}
1001+
8711002
function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
8721003
let parsedData;
8731004
try {

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

+8
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,18 @@ export type NetworkMeasure = {|
119119
url: string,
120120
|};
121121

122+
export type ReactComponentMeasureType =
123+
| 'render'
124+
| 'layout-effect-mount'
125+
| 'layout-effect-unmount'
126+
| 'passive-effect-mount'
127+
| 'passive-effect-unmount';
128+
122129
export type ReactComponentMeasure = {|
123130
+componentName: string,
124131
duration: Milliseconds,
125132
+timestamp: Milliseconds,
133+
+type: ReactComponentMeasureType,
126134
warning: string | null,
127135
|};
128136

‎packages/react-reconciler/src/ReactFiberCommitWork.new.js

+56-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
enableProfilerTimer,
3131
enableProfilerCommitHooks,
3232
enableProfilerNestedUpdatePhase,
33+
enableSchedulingProfiler,
3334
enableSuspenseServerRenderer,
3435
enableSuspenseCallback,
3536
enableScopeAPI,
@@ -142,6 +143,16 @@ import {
142143
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
143144
import {doesFiberContain} from './ReactFiberTreeReflection';
144145
import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils';
146+
import {
147+
markComponentPassiveEffectMountStarted,
148+
markComponentPassiveEffectMountStopped,
149+
markComponentPassiveEffectUnmountStarted,
150+
markComponentPassiveEffectUnmountStopped,
151+
markComponentLayoutEffectMountStarted,
152+
markComponentLayoutEffectMountStopped,
153+
markComponentLayoutEffectUnmountStarted,
154+
markComponentLayoutEffectUnmountStopped,
155+
} from './SchedulingProfiler';
145156

146157
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
147158
if (__DEV__) {
@@ -512,26 +523,58 @@ function commitHookEffectListUnmount(
512523
const destroy = effect.destroy;
513524
effect.destroy = undefined;
514525
if (destroy !== undefined) {
526+
if (enableSchedulingProfiler) {
527+
if ((flags & HookPassive) !== NoHookEffect) {
528+
markComponentPassiveEffectUnmountStarted(finishedWork);
529+
} else if ((flags & HookLayout) !== NoHookEffect) {
530+
markComponentLayoutEffectUnmountStarted(finishedWork);
531+
}
532+
}
533+
515534
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
535+
536+
if (enableSchedulingProfiler) {
537+
if ((flags & HookPassive) !== NoHookEffect) {
538+
markComponentPassiveEffectUnmountStopped();
539+
} else if ((flags & HookLayout) !== NoHookEffect) {
540+
markComponentLayoutEffectUnmountStopped();
541+
}
542+
}
516543
}
517544
}
518545
effect = effect.next;
519546
} while (effect !== firstEffect);
520547
}
521548
}
522549

523-
function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
550+
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
524551
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
525552
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
526553
if (lastEffect !== null) {
527554
const firstEffect = lastEffect.next;
528555
let effect = firstEffect;
529556
do {
530-
if ((effect.tag & tag) === tag) {
557+
if ((effect.tag & flags) === flags) {
558+
if (enableSchedulingProfiler) {
559+
if ((flags & HookPassive) !== NoHookEffect) {
560+
markComponentPassiveEffectMountStarted(finishedWork);
561+
} else if ((flags & HookLayout) !== NoHookEffect) {
562+
markComponentLayoutEffectMountStarted(finishedWork);
563+
}
564+
}
565+
531566
// Mount
532567
const create = effect.create;
533568
effect.destroy = create();
534569

570+
if (enableSchedulingProfiler) {
571+
if ((flags & HookPassive) !== NoHookEffect) {
572+
markComponentPassiveEffectMountStopped();
573+
} else if ((flags & HookLayout) !== NoHookEffect) {
574+
markComponentLayoutEffectMountStopped();
575+
}
576+
}
577+
535578
if (__DEV__) {
536579
const destroy = effect.destroy;
537580
if (destroy !== undefined && typeof destroy !== 'function') {
@@ -1180,10 +1223,13 @@ function commitUnmount(
11801223
do {
11811224
const {destroy, tag} = effect;
11821225
if (destroy !== undefined) {
1183-
if (
1184-
(tag & HookInsertion) !== NoHookEffect ||
1185-
(tag & HookLayout) !== NoHookEffect
1186-
) {
1226+
if ((tag & HookInsertion) !== NoHookEffect) {
1227+
safelyCallDestroy(current, nearestMountedAncestor, destroy);
1228+
} else if ((tag & HookLayout) !== NoHookEffect) {
1229+
if (enableSchedulingProfiler) {
1230+
markComponentLayoutEffectUnmountStarted(current);
1231+
}
1232+
11871233
if (
11881234
enableProfilerTimer &&
11891235
enableProfilerCommitHooks &&
@@ -1195,6 +1241,10 @@ function commitUnmount(
11951241
} else {
11961242
safelyCallDestroy(current, nearestMountedAncestor, destroy);
11971243
}
1244+
1245+
if (enableSchedulingProfiler) {
1246+
markComponentLayoutEffectUnmountStopped();
1247+
}
11981248
}
11991249
}
12001250
effect = effect.next;

‎packages/react-reconciler/src/ReactFiberCommitWork.old.js

+56-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
enableProfilerTimer,
3131
enableProfilerCommitHooks,
3232
enableProfilerNestedUpdatePhase,
33+
enableSchedulingProfiler,
3334
enableSuspenseServerRenderer,
3435
enableSuspenseCallback,
3536
enableScopeAPI,
@@ -142,6 +143,16 @@ import {
142143
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
143144
import {doesFiberContain} from './ReactFiberTreeReflection';
144145
import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils';
146+
import {
147+
markComponentPassiveEffectMountStarted,
148+
markComponentPassiveEffectMountStopped,
149+
markComponentPassiveEffectUnmountStarted,
150+
markComponentPassiveEffectUnmountStopped,
151+
markComponentLayoutEffectMountStarted,
152+
markComponentLayoutEffectMountStopped,
153+
markComponentLayoutEffectUnmountStarted,
154+
markComponentLayoutEffectUnmountStopped,
155+
} from './SchedulingProfiler';
145156

146157
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
147158
if (__DEV__) {
@@ -512,26 +523,58 @@ function commitHookEffectListUnmount(
512523
const destroy = effect.destroy;
513524
effect.destroy = undefined;
514525
if (destroy !== undefined) {
526+
if (enableSchedulingProfiler) {
527+
if ((flags & HookPassive) !== NoHookEffect) {
528+
markComponentPassiveEffectUnmountStarted(finishedWork);
529+
} else if ((flags & HookLayout) !== NoHookEffect) {
530+
markComponentLayoutEffectUnmountStarted(finishedWork);
531+
}
532+
}
533+
515534
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
535+
536+
if (enableSchedulingProfiler) {
537+
if ((flags & HookPassive) !== NoHookEffect) {
538+
markComponentPassiveEffectUnmountStopped();
539+
} else if ((flags & HookLayout) !== NoHookEffect) {
540+
markComponentLayoutEffectUnmountStopped();
541+
}
542+
}
516543
}
517544
}
518545
effect = effect.next;
519546
} while (effect !== firstEffect);
520547
}
521548
}
522549

523-
function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
550+
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
524551
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
525552
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
526553
if (lastEffect !== null) {
527554
const firstEffect = lastEffect.next;
528555
let effect = firstEffect;
529556
do {
530-
if ((effect.tag & tag) === tag) {
557+
if ((effect.tag & flags) === flags) {
558+
if (enableSchedulingProfiler) {
559+
if ((flags & HookPassive) !== NoHookEffect) {
560+
markComponentPassiveEffectMountStarted(finishedWork);
561+
} else if ((flags & HookLayout) !== NoHookEffect) {
562+
markComponentLayoutEffectMountStarted(finishedWork);
563+
}
564+
}
565+
531566
// Mount
532567
const create = effect.create;
533568
effect.destroy = create();
534569

570+
if (enableSchedulingProfiler) {
571+
if ((flags & HookPassive) !== NoHookEffect) {
572+
markComponentPassiveEffectMountStopped();
573+
} else if ((flags & HookLayout) !== NoHookEffect) {
574+
markComponentLayoutEffectMountStopped();
575+
}
576+
}
577+
535578
if (__DEV__) {
536579
const destroy = effect.destroy;
537580
if (destroy !== undefined && typeof destroy !== 'function') {
@@ -1180,10 +1223,13 @@ function commitUnmount(
11801223
do {
11811224
const {destroy, tag} = effect;
11821225
if (destroy !== undefined) {
1183-
if (
1184-
(tag & HookInsertion) !== NoHookEffect ||
1185-
(tag & HookLayout) !== NoHookEffect
1186-
) {
1226+
if ((tag & HookInsertion) !== NoHookEffect) {
1227+
safelyCallDestroy(current, nearestMountedAncestor, destroy);
1228+
} else if ((tag & HookLayout) !== NoHookEffect) {
1229+
if (enableSchedulingProfiler) {
1230+
markComponentLayoutEffectUnmountStarted(current);
1231+
}
1232+
11871233
if (
11881234
enableProfilerTimer &&
11891235
enableProfilerCommitHooks &&
@@ -1195,6 +1241,10 @@ function commitUnmount(
11951241
} else {
11961242
safelyCallDestroy(current, nearestMountedAncestor, destroy);
11971243
}
1244+
1245+
if (enableSchedulingProfiler) {
1246+
markComponentLayoutEffectUnmountStopped();
1247+
}
11981248
}
11991249
}
12001250
effect = effect.next;

‎packages/react-reconciler/src/SchedulingProfiler.js

+72
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,78 @@ export function markComponentRenderStopped(): void {
161161
}
162162
}
163163

164+
export function markComponentPassiveEffectMountStarted(fiber: Fiber): void {
165+
if (enableSchedulingProfiler) {
166+
if (supportsUserTimingV3) {
167+
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
168+
// TODO (scheduling profiler) Add component stack id
169+
markAndClear(`--component-passive-effect-mount-start-${componentName}`);
170+
}
171+
}
172+
}
173+
174+
export function markComponentPassiveEffectMountStopped(): void {
175+
if (enableSchedulingProfiler) {
176+
if (supportsUserTimingV3) {
177+
markAndClear('--component-passive-effect-mount-stop');
178+
}
179+
}
180+
}
181+
182+
export function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void {
183+
if (enableSchedulingProfiler) {
184+
if (supportsUserTimingV3) {
185+
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
186+
// TODO (scheduling profiler) Add component stack id
187+
markAndClear(`--component-passive-effect-unmount-start-${componentName}`);
188+
}
189+
}
190+
}
191+
192+
export function markComponentPassiveEffectUnmountStopped(): void {
193+
if (enableSchedulingProfiler) {
194+
if (supportsUserTimingV3) {
195+
markAndClear('--component-passive-effect-unmount-stop');
196+
}
197+
}
198+
}
199+
200+
export function markComponentLayoutEffectMountStarted(fiber: Fiber): void {
201+
if (enableSchedulingProfiler) {
202+
if (supportsUserTimingV3) {
203+
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
204+
// TODO (scheduling profiler) Add component stack id
205+
markAndClear(`--component-layout-effect-mount-start-${componentName}`);
206+
}
207+
}
208+
}
209+
210+
export function markComponentLayoutEffectMountStopped(): void {
211+
if (enableSchedulingProfiler) {
212+
if (supportsUserTimingV3) {
213+
markAndClear('--component-layout-effect-mount-stop');
214+
}
215+
}
216+
}
217+
218+
export function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void {
219+
if (enableSchedulingProfiler) {
220+
if (supportsUserTimingV3) {
221+
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
222+
// TODO (scheduling profiler) Add component stack id
223+
markAndClear(`--component-layout-effect-unmount-start-${componentName}`);
224+
}
225+
}
226+
}
227+
228+
export function markComponentLayoutEffectUnmountStopped(): void {
229+
if (enableSchedulingProfiler) {
230+
if (supportsUserTimingV3) {
231+
markAndClear('--component-layout-effect-unmount-stop');
232+
}
233+
}
234+
}
235+
164236
export function markComponentErrored(
165237
fiber: Fiber,
166238
thrownValue: mixed,

‎packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js

+192-51
Original file line numberDiff line numberDiff line change
@@ -608,30 +608,32 @@ describe('SchedulingProfiler', () => {
608608

609609
if (gate(flags => flags.enableSchedulingProfiler)) {
610610
expect(getMarks()).toMatchInlineSnapshot(`
611-
Array [
612-
"--render-start-16",
613-
"--component-render-start-Example",
614-
"--component-render-stop",
615-
"--render-stop",
616-
"--commit-start-16",
617-
"--react-version-17.0.3",
618-
"--profiler-version-1",
619-
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
620-
"--layout-effects-start-16",
621-
"--schedule-state-update-1-Example",
622-
"--layout-effects-stop",
623-
"--render-start-1",
624-
"--component-render-start-Example",
625-
"--component-render-stop",
626-
"--render-stop",
627-
"--commit-start-1",
628-
"--react-version-17.0.3",
629-
"--profiler-version-1",
630-
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
631-
"--commit-stop",
632-
"--commit-stop",
633-
]
634-
`);
611+
Array [
612+
"--render-start-16",
613+
"--component-render-start-Example",
614+
"--component-render-stop",
615+
"--render-stop",
616+
"--commit-start-16",
617+
"--react-version-17.0.3",
618+
"--profiler-version-1",
619+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
620+
"--layout-effects-start-16",
621+
"--component-layout-effect-mount-start-Example",
622+
"--schedule-state-update-1-Example",
623+
"--component-layout-effect-mount-stop",
624+
"--layout-effects-stop",
625+
"--render-start-1",
626+
"--component-render-start-Example",
627+
"--component-render-stop",
628+
"--render-stop",
629+
"--commit-start-1",
630+
"--react-version-17.0.3",
631+
"--profiler-version-1",
632+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
633+
"--commit-stop",
634+
"--commit-stop",
635+
]
636+
`);
635637
}
636638
});
637639

@@ -652,33 +654,35 @@ describe('SchedulingProfiler', () => {
652654

653655
if (gate(flags => flags.enableSchedulingProfiler)) {
654656
expect(getMarks()).toMatchInlineSnapshot(`
655-
Array [
656-
"--schedule-render-16",
657-
"--render-start-16",
658-
"--component-render-start-Example",
659-
"--component-render-stop",
660-
"--render-stop",
661-
"--commit-start-16",
662-
"--react-version-17.0.3",
663-
"--profiler-version-1",
664-
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
665-
"--layout-effects-start-16",
666-
"--layout-effects-stop",
667-
"--commit-stop",
668-
"--passive-effects-start-16",
669-
"--schedule-state-update-16-Example",
670-
"--passive-effects-stop",
671-
"--render-start-16",
672-
"--component-render-start-Example",
673-
"--component-render-stop",
674-
"--render-stop",
675-
"--commit-start-16",
676-
"--react-version-17.0.3",
677-
"--profiler-version-1",
678-
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
679-
"--commit-stop",
680-
]
681-
`);
657+
Array [
658+
"--schedule-render-16",
659+
"--render-start-16",
660+
"--component-render-start-Example",
661+
"--component-render-stop",
662+
"--render-stop",
663+
"--commit-start-16",
664+
"--react-version-17.0.3",
665+
"--profiler-version-1",
666+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
667+
"--layout-effects-start-16",
668+
"--layout-effects-stop",
669+
"--commit-stop",
670+
"--passive-effects-start-16",
671+
"--component-passive-effect-mount-start-Example",
672+
"--schedule-state-update-16-Example",
673+
"--component-passive-effect-mount-stop",
674+
"--passive-effects-stop",
675+
"--render-start-16",
676+
"--component-render-start-Example",
677+
"--component-render-stop",
678+
"--render-stop",
679+
"--commit-start-16",
680+
"--react-version-17.0.3",
681+
"--profiler-version-1",
682+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
683+
"--commit-stop",
684+
]
685+
`);
682686
}
683687
});
684688

@@ -854,4 +858,141 @@ describe('SchedulingProfiler', () => {
854858
`);
855859
}
856860
});
861+
862+
it('should mark passive and layout effects', async () => {
863+
function ComponentWithEffects() {
864+
React.useLayoutEffect(() => {
865+
Scheduler.unstable_yieldValue('layout 1 mount');
866+
return () => {
867+
Scheduler.unstable_yieldValue('layout 1 unmount');
868+
};
869+
}, []);
870+
871+
React.useEffect(() => {
872+
Scheduler.unstable_yieldValue('passive 1 mount');
873+
return () => {
874+
Scheduler.unstable_yieldValue('passive 1 unmount');
875+
};
876+
}, []);
877+
878+
React.useLayoutEffect(() => {
879+
Scheduler.unstable_yieldValue('layout 2 mount');
880+
return () => {
881+
Scheduler.unstable_yieldValue('layout 2 unmount');
882+
};
883+
}, []);
884+
885+
React.useEffect(() => {
886+
Scheduler.unstable_yieldValue('passive 2 mount');
887+
return () => {
888+
Scheduler.unstable_yieldValue('passive 2 unmount');
889+
};
890+
}, []);
891+
892+
React.useEffect(() => {
893+
Scheduler.unstable_yieldValue('passive 3 mount');
894+
return () => {
895+
Scheduler.unstable_yieldValue('passive 3 unmount');
896+
};
897+
}, []);
898+
899+
return null;
900+
}
901+
902+
const renderer = ReactTestRenderer.create(<ComponentWithEffects />, {
903+
unstable_isConcurrent: true,
904+
});
905+
906+
expect(Scheduler).toFlushUntilNextPaint([
907+
'layout 1 mount',
908+
'layout 2 mount',
909+
]);
910+
911+
if (gate(flags => flags.enableSchedulingProfiler)) {
912+
expect(getMarks()).toMatchInlineSnapshot(`
913+
Array [
914+
"--schedule-render-16",
915+
"--render-start-16",
916+
"--component-render-start-ComponentWithEffects",
917+
"--component-render-stop",
918+
"--render-stop",
919+
"--commit-start-16",
920+
"--react-version-17.0.3",
921+
"--profiler-version-1",
922+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
923+
"--layout-effects-start-16",
924+
"--component-layout-effect-mount-start-ComponentWithEffects",
925+
"--component-layout-effect-mount-stop",
926+
"--component-layout-effect-mount-start-ComponentWithEffects",
927+
"--component-layout-effect-mount-stop",
928+
"--layout-effects-stop",
929+
"--commit-stop",
930+
]
931+
`);
932+
}
933+
934+
clearPendingMarks();
935+
936+
expect(Scheduler).toFlushAndYield([
937+
'passive 1 mount',
938+
'passive 2 mount',
939+
'passive 3 mount',
940+
]);
941+
942+
if (gate(flags => flags.enableSchedulingProfiler)) {
943+
expect(getMarks()).toMatchInlineSnapshot(`
944+
Array [
945+
"--passive-effects-start-16",
946+
"--component-passive-effect-mount-start-ComponentWithEffects",
947+
"--component-passive-effect-mount-stop",
948+
"--component-passive-effect-mount-start-ComponentWithEffects",
949+
"--component-passive-effect-mount-stop",
950+
"--component-passive-effect-mount-start-ComponentWithEffects",
951+
"--component-passive-effect-mount-stop",
952+
"--passive-effects-stop",
953+
]
954+
`);
955+
}
956+
957+
clearPendingMarks();
958+
959+
renderer.unmount();
960+
961+
expect(Scheduler).toFlushAndYield([
962+
'layout 1 unmount',
963+
'layout 2 unmount',
964+
'passive 1 unmount',
965+
'passive 2 unmount',
966+
'passive 3 unmount',
967+
]);
968+
969+
if (gate(flags => flags.enableSchedulingProfiler)) {
970+
expect(getMarks()).toMatchInlineSnapshot(`
971+
Array [
972+
"--schedule-render-16",
973+
"--render-start-16",
974+
"--render-stop",
975+
"--commit-start-16",
976+
"--react-version-17.0.3",
977+
"--profiler-version-1",
978+
"--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
979+
"--component-layout-effect-unmount-start-ComponentWithEffects",
980+
"--component-layout-effect-unmount-stop",
981+
"--component-layout-effect-unmount-start-ComponentWithEffects",
982+
"--component-layout-effect-unmount-stop",
983+
"--layout-effects-start-16",
984+
"--layout-effects-stop",
985+
"--commit-stop",
986+
"--passive-effects-start-16",
987+
"--component-passive-effect-unmount-start-ComponentWithEffects",
988+
"--component-passive-effect-unmount-stop",
989+
"--component-passive-effect-unmount-start-ComponentWithEffects",
990+
"--component-passive-effect-unmount-stop",
991+
"--component-passive-effect-unmount-start-ComponentWithEffects",
992+
"--component-passive-effect-unmount-stop",
993+
"--passive-effects-stop",
994+
]
995+
`);
996+
}
997+
});
857998
});

0 commit comments

Comments
 (0)
Please sign in to comment.