main
js 279 lines 9.18 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 TransitionTracingCallbacks,
12 Fiber,
13 FiberRoot,
14 } from './ReactInternalTypes';
15 import type {Transition} from 'react/src/ReactStartTransition';
16 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
17 import type {StackCursor} from './ReactFiberStack';
18
19 import {enableTransitionTracing} from 'shared/ReactFeatureFlags';
20 import {createCursor, push, pop} from './ReactFiberStack';
21 import {getWorkInProgressTransitions} from './ReactFiberWorkLoop';
22
23 export type SuspenseInfo = {name: string | null};
24
25 export type PendingTransitionCallbacks = {
26 transitionStart: Array<Transition> | null,
27 transitionProgress: Map<Transition, PendingBoundaries> | null,
28 transitionComplete: Array<Transition> | null,
29 markerProgress: Map<
30 string,
31 {pendingBoundaries: PendingBoundaries, transitions: Set<Transition>},
32 > | null,
33 markerIncomplete: Map<
34 string,
35 {aborts: Array<TransitionAbort>, transitions: Set<Transition>},
36 > | null,
37 markerComplete: Map<string, Set<Transition>> | null,
38 };
39
40 // TODO: Is there a way to not include the tag or name here?
41 export type TracingMarkerInstance = {
42 tag?: TracingMarkerTag,
43 transitions: Set<Transition> | null,
44 pendingBoundaries: PendingBoundaries | null,
45 aborts: Array<TransitionAbort> | null,
46 name: string | null,
47 };
48
49 export type TransitionAbort = {
50 reason: 'error' | 'unknown' | 'marker' | 'suspense',
51 name?: string | null,
52 };
53
54 export const TransitionRoot = 0;
55 export const TransitionTracingMarker = 1;
56 export type TracingMarkerTag = 0 | 1;
57
58 export type PendingBoundaries = Map<OffscreenInstance, SuspenseInfo>;
59
60 export function processTransitionCallbacks(
61 pendingTransitions: PendingTransitionCallbacks,
62 endTime: number,
63 callbacks: TransitionTracingCallbacks,
64 ): void {
65 if (enableTransitionTracing) {
66 // $FlowFixMe[invalid-compare]
67 if (pendingTransitions !== null) {
68 const transitionStart = pendingTransitions.transitionStart;
69 const onTransitionStart = callbacks.onTransitionStart;
70 if (transitionStart !== null && onTransitionStart != null) {
71 transitionStart.forEach(transition => {
72 if (transition.name != null) {
73 onTransitionStart(transition.name, transition.startTime);
74 }
75 });
76 }
77
78 const markerProgress = pendingTransitions.markerProgress;
79 const onMarkerProgress = callbacks.onMarkerProgress;
80 if (onMarkerProgress != null && markerProgress !== null) {
81 markerProgress.forEach((markerInstance, markerName) => {
82 // $FlowFixMe[invalid-compare]
83 if (markerInstance.transitions !== null) {
84 // TODO: Clone the suspense object so users can't modify it
85 const pending =
86 // $FlowFixMe[invalid-compare]
87 markerInstance.pendingBoundaries !== null
88 ? Array.from(markerInstance.pendingBoundaries.values())
89 : [];
90 markerInstance.transitions.forEach(transition => {
91 if (transition.name != null) {
92 onMarkerProgress(
93 transition.name,
94 markerName,
95 transition.startTime,
96 endTime,
97 pending,
98 );
99 }
100 });
101 }
102 });
103 }
104
105 const markerComplete = pendingTransitions.markerComplete;
106 const onMarkerComplete = callbacks.onMarkerComplete;
107 if (markerComplete !== null && onMarkerComplete != null) {
108 markerComplete.forEach((transitions, markerName) => {
109 transitions.forEach(transition => {
110 if (transition.name != null) {
111 onMarkerComplete(
112 transition.name,
113 markerName,
114 transition.startTime,
115 endTime,
116 );
117 }
118 });
119 });
120 }
121
122 const markerIncomplete = pendingTransitions.markerIncomplete;
123 const onMarkerIncomplete = callbacks.onMarkerIncomplete;
124 if (onMarkerIncomplete != null && markerIncomplete !== null) {
125 markerIncomplete.forEach(({transitions, aborts}, markerName) => {
126 transitions.forEach(transition => {
127 const filteredAborts = [];
128 aborts.forEach(abort => {
129 switch (abort.reason) {
130 case 'marker': {
131 filteredAborts.push({
132 type: 'marker',
133 name: abort.name,
134 endTime,
135 });
136 break;
137 }
138 case 'suspense': {
139 filteredAborts.push({
140 type: 'suspense',
141 name: abort.name,
142 endTime,
143 });
144 break;
145 }
146 default: {
147 break;
148 }
149 }
150 });
151
152 if (filteredAborts.length > 0) {
153 if (transition.name != null) {
154 onMarkerIncomplete(
155 transition.name,
156 markerName,
157 transition.startTime,
158 filteredAborts,
159 );
160 }
161 }
162 });
163 });
164 }
165
166 const transitionProgress = pendingTransitions.transitionProgress;
167 const onTransitionProgress = callbacks.onTransitionProgress;
168 if (onTransitionProgress != null && transitionProgress !== null) {
169 transitionProgress.forEach((pending, transition) => {
170 if (transition.name != null) {
171 onTransitionProgress(
172 transition.name,
173 transition.startTime,
174 endTime,
175 Array.from(pending.values()),
176 );
177 }
178 });
179 }
180
181 const transitionComplete = pendingTransitions.transitionComplete;
182 const onTransitionComplete = callbacks.onTransitionComplete;
183 if (transitionComplete !== null && onTransitionComplete != null) {
184 transitionComplete.forEach(transition => {
185 if (transition.name != null) {
186 onTransitionComplete(
187 transition.name,
188 transition.startTime,
189 endTime,
190 );
191 }
192 });
193 }
194 }
195 }
196 }
197
198 // For every tracing marker, store a pointer to it. We will later access it
199 // to get the set of suspense boundaries that need to resolve before the
200 // tracing marker can be logged as complete
201 // This code lives separate from the ReactFiberTransition code because
202 // we push and pop on the tracing marker, not the suspense boundary
203 const markerInstanceStack: StackCursor<Array<TracingMarkerInstance> | null> =
204 createCursor(null);
205
206 export function pushRootMarkerInstance(workInProgress: Fiber): void {
207 if (enableTransitionTracing) {
208 // On the root, every transition gets mapped to it's own map of
209 // suspense boundaries. The transition is marked as complete when
210 // the suspense boundaries map is empty. We do this because every
211 // transition completes at different times and depends on different
212 // suspense boundaries to complete. We store all the transitions
213 // along with its map of suspense boundaries in the root incomplete
214 // transitions map. Each entry in this map functions like a tracing
215 // marker does, so we can push it onto the marker instance stack
216 const transitions = getWorkInProgressTransitions();
217 const root: FiberRoot = workInProgress.stateNode;
218
219 if (transitions !== null) {
220 transitions.forEach(transition => {
221 if (!root.incompleteTransitions.has(transition)) {
222 const markerInstance: TracingMarkerInstance = {
223 tag: TransitionRoot,
224 transitions: new Set([transition]),
225 pendingBoundaries: null,
226 aborts: null,
227 name: null,
228 };
229 root.incompleteTransitions.set(transition, markerInstance);
230 }
231 });
232 }
233
234 const markerInstances = [];
235 // For ever transition on the suspense boundary, we push the transition
236 // along with its map of pending suspense boundaries onto the marker
237 // instance stack.
238 root.incompleteTransitions.forEach(markerInstance => {
239 markerInstances.push(markerInstance);
240 });
241 push(markerInstanceStack, markerInstances, workInProgress);
242 }
243 }
244
245 export function popRootMarkerInstance(workInProgress: Fiber) {
246 if (enableTransitionTracing) {
247 pop(markerInstanceStack, workInProgress);
248 }
249 }
250
251 export function pushMarkerInstance(
252 workInProgress: Fiber,
253 markerInstance: TracingMarkerInstance,
254 ): void {
255 if (enableTransitionTracing) {
256 if (markerInstanceStack.current === null) {
257 push(markerInstanceStack, [markerInstance], workInProgress);
258 } else {
259 push(
260 markerInstanceStack,
261 markerInstanceStack.current.concat(markerInstance),
262 workInProgress,
263 );
264 }
265 }
266 }
267
268 export function popMarkerInstance(workInProgress: Fiber): void {
269 if (enableTransitionTracing) {
270 pop(markerInstanceStack, workInProgress);
271 }
272 }
273
274 export function getMarkerInstances(): Array<TracingMarkerInstance> | null {
275 if (enableTransitionTracing) {
276 return markerInstanceStack.current;
277 }
278 return null;
279 }