@samitouri / QOS-React-2 / commits / 0e8a5aff3d

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.

Brian Vaughn committed Oct 21, 2021 at 14:41 UTC 0e8a5aff3ddf0f863839a924738f958fd940e3be
10 files changed +649 -127
packages/react-devtools-scheduling-profiler/src/EventTooltip.js
+19 -2
@@ -140,9 +140,26 @@ const TooltipReactComponentMeasure = ({
140 }: {|
141 componentMeasure: ReactComponentMeasure,
142 |}) => {
143 - const {componentName, duration, timestamp, warning} = componentMeasure;
143 + const {componentName, duration, timestamp, type, warning} = componentMeasure;
144
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 + }
163
164 return (
165 <>
packages/react-devtools-scheduling-profiler/src/content-views/ComponentMeasuresView.js
+51 -6
@@ -76,7 +76,13 @@ export class ComponentMeasuresView extends View {
76 showHoverHighlight: boolean,
77 ): boolean {
78 const {frame} = this;
79 - const {componentName, duration, timestamp, warning} = componentMeasure;
79 + const {
80 + componentName,
81 + duration,
82 + timestamp,
83 + type,
84 + warning,
85 + } = componentMeasure;
86
87 const xStart = timestampToPosition(timestamp, scaleFactor, frame);
88 const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
@@ -96,6 +102,9 @@ export class ComponentMeasuresView extends View {
102 return false; // Too small to render at this zoom level
103 }
104
105 + let textFillStyle = ((null: any): string);
106 + let typeLabel = ((null: any): string);
107 +
108 const drawableRect = intersectionOfRects(componentMeasureRect, rect);
109 context.beginPath();
110 if (warning !== null) {
@@ -103,9 +112,43 @@ export class ComponentMeasuresView extends View {
112 ? COLORS.WARNING_BACKGROUND_HOVER
113 : COLORS.WARNING_BACKGROUND;
114 } 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 + }
152 }
153 context.fillRect(
154 drawableRect.origin.x,
@@ -114,9 +157,11 @@ export class ComponentMeasuresView extends View {
157 drawableRect.size.height,
158 );
159
117 - const label = `${componentName} rendered - ${formatDuration(duration)}`;
160 + const label = `${componentName} ${typeLabel} - ${formatDuration(duration)}`;
161
119 - drawText(label, context, componentMeasureRect, drawableRect);
162 + drawText(label, context, componentMeasureRect, drawableRect, {
163 + fillStyle: textFillStyle,
164 + });
165
166 return true;
167 }
packages/react-devtools-scheduling-profiler/src/content-views/constants.js
-8
@@ -59,8 +59,6 @@ export let COLORS = {
59 PRIORITY_LABEL: '',
60 USER_TIMING: '',
61 USER_TIMING_HOVER: '',
62 - REACT_COMPONENT_MEASURE: '',
63 - REACT_COMPONENT_MEASURE_HOVER: '',
62 REACT_IDLE: '',
63 REACT_IDLE_HOVER: '',
64 REACT_RENDER: '',
@@ -150,12 +148,6 @@ export function updateColorsToMatchTheme(element: Element): boolean {
148 USER_TIMING_HOVER: computedStyle.getPropertyValue(
149 '--color-scheduling-profiler-user-timing-hover',
150 ),
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 - ),
151 REACT_IDLE: computedStyle.getPropertyValue(
152 '--color-scheduling-profiler-react-idle',
153 ),
packages/react-devtools-scheduling-profiler/src/import-worker/__tests__/preprocessData-test.internal.js
+39 -23
@@ -840,7 +840,7 @@ describe('preprocessData', () => {
840 Object {
841 "batchUID": 0,
842 "depth": 0,
843 - "duration": 0.0019999999999999983,
843 + "duration": 0.004,
844 "lanes": Array [
845 4,
846 ],
@@ -852,11 +852,11 @@ describe('preprocessData', () => {
852 Object {
853 "batchUID": 1,
854 "depth": 0,
855 - "duration": 0.010000000000000002,
855 + "duration": 0.009999999999999998,
856 "lanes": Array [
857 4,
858 ],
859 - "timestamp": 0.019,
859 + "timestamp": 0.021,
860 "type": "render-idle",
861 },
862 Object {
@@ -866,37 +866,37 @@ describe('preprocessData', () => {
866 "lanes": Array [
867 4,
868 ],
869 - "timestamp": 0.019,
869 + "timestamp": 0.021,
870 "type": "render",
871 },
872 Object {
873 "batchUID": 1,
874 "depth": 0,
875 - "duration": 0.006000000000000002,
875 + "duration": 0.005999999999999998,
876 "lanes": Array [
877 4,
878 ],
879 - "timestamp": 0.023,
879 + "timestamp": 0.025,
880 "type": "commit",
881 },
882 Object {
883 "batchUID": 1,
884 "depth": 1,
885 - "duration": 0.0010000000000000009,
885 + "duration": 0.0009999999999999974,
886 "lanes": Array [
887 4,
888 ],
889 - "timestamp": 0.027,
889 + "timestamp": 0.029,
890 "type": "layout-effects",
891 },
892 Object {
893 "batchUID": 1,
894 "depth": 0,
895 - "duration": 0.0010000000000000009,
895 + "duration": 0.0030000000000000027,
896 "lanes": Array [
897 4,
898 ],
899 - "timestamp": 0.03,
899 + "timestamp": 0.032,
900 "type": "passive-effects",
901 },
902 ],
@@ -906,16 +906,32 @@ describe('preprocessData', () => {
906 "componentName": "App",
907 "duration": 0.001,
908 "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",
917 "warning": null,
918 },
919 Object {
920 "componentName": "App",
921 "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",
931 "warning": null,
932 },
933 ],
918 - "duration": 0.031,
934 + "duration": 0.035,
935 "flamechart": Array [],
936 "internalModuleSourceToRanges": Map {},
937 "laneToLabelMap": Map {
@@ -1000,7 +1016,7 @@ describe('preprocessData', () => {
1016 Object {
1017 "batchUID": 0,
1018 "depth": 0,
1003 - "duration": 0.0019999999999999983,
1019 + "duration": 0.004,
1020 "lanes": Array [
1021 4,
1022 ],
@@ -1010,11 +1026,11 @@ describe('preprocessData', () => {
1026 Object {
1027 "batchUID": 1,
1028 "depth": 0,
1013 - "duration": 0.010000000000000002,
1029 + "duration": 0.009999999999999998,
1030 "lanes": Array [
1031 4,
1032 ],
1017 - "timestamp": 0.019,
1033 + "timestamp": 0.021,
1034 "type": "render-idle",
1035 },
1036 Object {
@@ -1024,37 +1040,37 @@ describe('preprocessData', () => {
1040 "lanes": Array [
1041 4,
1042 ],
1027 - "timestamp": 0.019,
1043 + "timestamp": 0.021,
1044 "type": "render",
1045 },
1046 Object {
1047 "batchUID": 1,
1048 "depth": 0,
1033 - "duration": 0.006000000000000002,
1049 + "duration": 0.005999999999999998,
1050 "lanes": Array [
1051 4,
1052 ],
1037 - "timestamp": 0.023,
1053 + "timestamp": 0.025,
1054 "type": "commit",
1055 },
1056 Object {
1057 "batchUID": 1,
1058 "depth": 1,
1043 - "duration": 0.0010000000000000009,
1059 + "duration": 0.0009999999999999974,
1060 "lanes": Array [
1061 4,
1062 ],
1047 - "timestamp": 0.027,
1063 + "timestamp": 0.029,
1064 "type": "layout-effects",
1065 },
1066 Object {
1067 "batchUID": 1,
1068 "depth": 0,
1053 - "duration": 0.0010000000000000009,
1069 + "duration": 0.0030000000000000027,
1070 "lanes": Array [
1071 4,
1072 ],
1057 - "timestamp": 0.03,
1073 + "timestamp": 0.032,
1074 "type": "passive-effects",
1075 },
1076 ],
@@ -1108,7 +1124,7 @@ describe('preprocessData', () => {
1124 "lanes": Array [
1125 4,
1126 ],
1111 - "timestamp": 0.017,
1127 + "timestamp": 0.018,
1128 "type": "schedule-state-update",
1129 "warning": null,
1130 },
packages/react-devtools-scheduling-profiler/src/import-worker/preprocessData.js
+156 -25
@@ -22,6 +22,7 @@ import type {
22 Phase,
23 ReactLane,
24 ReactComponentMeasure,
25 + ReactComponentMeasureType,
26 ReactMeasure,
27 ReactMeasureType,
28 ReactProfilerData,
@@ -484,31 +485,13 @@ function processTimelineEvent(
485 } else if (name.startsWith('--react-lane-labels-')) {
486 const [laneLabelTuplesString] = name.substr(20).split('-');
487 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 + );
495 } else if (name.startsWith('--schedule-render-')) {
496 const [laneBitmaskString] = name.substr(18).split('-');
497
@@ -868,6 +851,154 @@ function processTimelineEvent(
851 }
852 }
853
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 +
1002 function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart {
1003 let parsedData;
1004 try {
packages/react-devtools-scheduling-profiler/src/types.js
+8
@@ -119,10 +119,18 @@ export type NetworkMeasure = {|
119 url: string,
120 |};
121
122 +export type ReactComponentMeasureType =
123 + | 'render'
124 + | 'layout-effect-mount'
125 + | 'layout-effect-unmount'
126 + | 'passive-effect-mount'
127 + | 'passive-effect-unmount';
128 +
129 export type ReactComponentMeasure = {|
130 +componentName: string,
131 duration: Milliseconds,
132 +timestamp: Milliseconds,
133 + +type: ReactComponentMeasureType,
134 warning: string | null,
135 |};
136
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+56 -6
@@ -30,6 +30,7 @@ import {
30 enableProfilerTimer,
31 enableProfilerCommitHooks,
32 enableProfilerNestedUpdatePhase,
33 + enableSchedulingProfiler,
34 enableSuspenseServerRenderer,
35 enableSuspenseCallback,
36 enableScopeAPI,
@@ -142,6 +143,16 @@ import {
143 import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
144 import {doesFiberContain} from './ReactFiberTreeReflection';
145 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';
156
157 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
158 if (__DEV__) {
@@ -512,7 +523,23 @@ function commitHookEffectListUnmount(
523 const destroy = effect.destroy;
524 effect.destroy = undefined;
525 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 +
534 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 + }
543 }
544 }
545 effect = effect.next;
@@ -520,18 +547,34 @@ function commitHookEffectListUnmount(
547 }
548 }
549
523 -function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
550 +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
551 const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
552 const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
553 if (lastEffect !== null) {
554 const firstEffect = lastEffect.next;
555 let effect = firstEffect;
556 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 +
566 // Mount
567 const create = effect.create;
568 effect.destroy = create();
569
570 + if (enableSchedulingProfiler) {
571 + if ((flags & HookPassive) !== NoHookEffect) {
572 + markComponentPassiveEffectMountStopped();
573 + } else if ((flags & HookLayout) !== NoHookEffect) {
574 + markComponentLayoutEffectMountStopped();
575 + }
576 + }
577 +
578 if (__DEV__) {
579 const destroy = effect.destroy;
580 if (destroy !== undefined && typeof destroy !== 'function') {
@@ -1180,10 +1223,13 @@ function commitUnmount(
1223 do {
1224 const {destroy, tag} = effect;
1225 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 +
1233 if (
1234 enableProfilerTimer &&
1235 enableProfilerCommitHooks &&
@@ -1195,6 +1241,10 @@ function commitUnmount(
1241 } else {
1242 safelyCallDestroy(current, nearestMountedAncestor, destroy);
1243 }
1244 +
1245 + if (enableSchedulingProfiler) {
1246 + markComponentLayoutEffectUnmountStopped();
1247 + }
1248 }
1249 }
1250 effect = effect.next;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+56 -6
@@ -30,6 +30,7 @@ import {
30 enableProfilerTimer,
31 enableProfilerCommitHooks,
32 enableProfilerNestedUpdatePhase,
33 + enableSchedulingProfiler,
34 enableSuspenseServerRenderer,
35 enableSuspenseCallback,
36 enableScopeAPI,
@@ -142,6 +143,16 @@ import {
143 import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
144 import {doesFiberContain} from './ReactFiberTreeReflection';
145 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';
156
157 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
158 if (__DEV__) {
@@ -512,7 +523,23 @@ function commitHookEffectListUnmount(
523 const destroy = effect.destroy;
524 effect.destroy = undefined;
525 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 +
534 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 + }
543 }
544 }
545 effect = effect.next;
@@ -520,18 +547,34 @@ function commitHookEffectListUnmount(
547 }
548 }
549
523 -function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
550 +function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
551 const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
552 const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
553 if (lastEffect !== null) {
554 const firstEffect = lastEffect.next;
555 let effect = firstEffect;
556 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 +
566 // Mount
567 const create = effect.create;
568 effect.destroy = create();
569
570 + if (enableSchedulingProfiler) {
571 + if ((flags & HookPassive) !== NoHookEffect) {
572 + markComponentPassiveEffectMountStopped();
573 + } else if ((flags & HookLayout) !== NoHookEffect) {
574 + markComponentLayoutEffectMountStopped();
575 + }
576 + }
577 +
578 if (__DEV__) {
579 const destroy = effect.destroy;
580 if (destroy !== undefined && typeof destroy !== 'function') {
@@ -1180,10 +1223,13 @@ function commitUnmount(
1223 do {
1224 const {destroy, tag} = effect;
1225 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 +
1233 if (
1234 enableProfilerTimer &&
1235 enableProfilerCommitHooks &&
@@ -1195,6 +1241,10 @@ function commitUnmount(
1241 } else {
1242 safelyCallDestroy(current, nearestMountedAncestor, destroy);
1243 }
1244 +
1245 + if (enableSchedulingProfiler) {
1246 + markComponentLayoutEffectUnmountStopped();
1247 + }
1248 }
1249 }
1250 effect = effect.next;
packages/react-reconciler/src/SchedulingProfiler.js
+72
@@ -161,6 +161,78 @@ export function markComponentRenderStopped(): void {
161 }
162 }
163
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 +
236 export function markComponentErrored(
237 fiber: Fiber,
238 thrownValue: mixed,
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+192 -51
@@ -608,30 +608,32 @@ describe('SchedulingProfiler', () => {
608
609 if (gate(flags => flags.enableSchedulingProfiler)) {
610 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 + `);
637 }
638 });
639
@@ -652,33 +654,35 @@ describe('SchedulingProfiler', () => {
654
655 if (gate(flags => flags.enableSchedulingProfiler)) {
656 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 + `);
686 }
687 });
688
@@ -854,4 +858,141 @@ describe('SchedulingProfiler', () => {
858 `);
859 }
860 });
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 + });
998 });