main
js 1,724 lines 43.7 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 /* eslint-disable react-internal/no-production-logging */
11
12 import type {Fiber} from './ReactInternalTypes';
13
14 import type {Lanes} from './ReactFiberLane';
15
16 import type {CapturedValue} from './ReactCapturedValue';
17
18 import {SuspenseComponent} from './ReactWorkTags';
19
20 import getComponentNameFromFiber from './getComponentNameFromFiber';
21
22 import {
23 getGroupNameOfHighestPriorityLane,
24 includesOnlyHydrationLanes,
25 includesOnlyOffscreenLanes,
26 includesOnlyHydrationOrOffscreenLanes,
27 includesSomeLane,
28 } from './ReactFiberLane';
29
30 import {
31 addValueToProperties,
32 addObjectToProperties,
33 addObjectDiffToProperties,
34 } from 'shared/ReactPerformanceTrackProperties';
35
36 import {
37 enableProfilerTimer,
38 enableGestureTransition,
39 enablePerformanceIssueReporting,
40 } from 'shared/ReactFeatureFlags';
41
42 const supportsUserTiming =
43 enableProfilerTimer &&
44 typeof console !== 'undefined' &&
45 typeof console.timeStamp === 'function' &&
46 (!__DEV__ ||
47 // In DEV we also rely on performance.measure
48 (typeof performance !== 'undefined' &&
49 // $FlowFixMe[method-unbinding]
50 typeof performance.measure === 'function'));
51
52 const COMPONENTS_TRACK = 'Components ⚛';
53 const LANES_TRACK_GROUP = 'Scheduler ⚛';
54
55 let currentTrack: string = 'Blocking'; // Lane
56
57 export function setCurrentTrackFromLanes(lanes: Lanes): void {
58 currentTrack = getGroupNameOfHighestPriorityLane(lanes);
59 }
60
61 export function markAllLanesInOrder() {
62 if (supportsUserTiming) {
63 // Ensure we create all tracks in priority order. Currently performance.mark() are in
64 // first insertion order but performance.measure() are in the reverse order. We can
65 // always add the 0 time slot even if it's in the past. That's still considered for
66 // ordering.
67 console.timeStamp(
68 'Blocking Track',
69 0.003,
70 0.003,
71 'Blocking',
72 LANES_TRACK_GROUP,
73 'primary-light',
74 );
75 if (enableGestureTransition) {
76 console.timeStamp(
77 'Gesture Track',
78 0.003,
79 0.003,
80 'Gesture',
81 LANES_TRACK_GROUP,
82 'primary-light',
83 );
84 }
85 console.timeStamp(
86 'Transition Track',
87 0.003,
88 0.003,
89 'Transition',
90 LANES_TRACK_GROUP,
91 'primary-light',
92 );
93 console.timeStamp(
94 'Suspense Track',
95 0.003,
96 0.003,
97 'Suspense',
98 LANES_TRACK_GROUP,
99 'primary-light',
100 );
101 console.timeStamp(
102 'Idle Track',
103 0.003,
104 0.003,
105 'Idle',
106 LANES_TRACK_GROUP,
107 'primary-light',
108 );
109 }
110 }
111
112 function logComponentTrigger(
113 fiber: Fiber,
114 startTime: number,
115 endTime: number,
116 trigger: string,
117 ) {
118 if (supportsUserTiming) {
119 reusableComponentOptions.start = startTime;
120 reusableComponentOptions.end = endTime;
121 reusableComponentDevToolDetails.color = 'warning';
122 reusableComponentDevToolDetails.tooltipText = trigger;
123 reusableComponentDevToolDetails.properties = null;
124 const debugTask = fiber._debugTask;
125 if (__DEV__ && debugTask) {
126 debugTask.run(
127 // $FlowFixMe[method-unbinding]
128 performance.measure.bind(
129 performance,
130 trigger,
131 reusableComponentOptions,
132 ),
133 );
134 } else {
135 performance.measure(trigger, reusableComponentOptions);
136 }
137 performance.clearMeasures(trigger);
138 }
139 }
140
141 export function logComponentMount(
142 fiber: Fiber,
143 startTime: number,
144 endTime: number,
145 ): void {
146 logComponentTrigger(fiber, startTime, endTime, 'Mount');
147 }
148
149 export function logComponentUnmount(
150 fiber: Fiber,
151 startTime: number,
152 endTime: number,
153 ): void {
154 logComponentTrigger(fiber, startTime, endTime, 'Unmount');
155 }
156
157 export function logComponentReappeared(
158 fiber: Fiber,
159 startTime: number,
160 endTime: number,
161 ): void {
162 logComponentTrigger(fiber, startTime, endTime, 'Reconnect');
163 }
164
165 export function logComponentDisappeared(
166 fiber: Fiber,
167 startTime: number,
168 endTime: number,
169 ): void {
170 logComponentTrigger(fiber, startTime, endTime, 'Disconnect');
171 }
172
173 let alreadyWarnedForDeepEquality = false;
174
175 export function pushDeepEquality(): boolean {
176 if (__DEV__) {
177 // If this is true then we don't reset it to false because we're tracking if any
178 // parent already warned about having deep equality props in this subtree.
179 return alreadyWarnedForDeepEquality;
180 }
181 return false;
182 }
183
184 export function popDeepEquality(prev: boolean): void {
185 if (__DEV__) {
186 alreadyWarnedForDeepEquality = prev;
187 }
188 }
189
190 const reusableComponentDevToolDetails = {
191 color: 'primary',
192 properties: null as null | Array<[string, string]>,
193 tooltipText: '',
194 track: COMPONENTS_TRACK,
195 };
196
197 const reusableComponentOptions: PerformanceMeasureOptions = {
198 start: -0,
199 end: -0,
200 detail: {
201 devtools: reusableComponentDevToolDetails,
202 },
203 };
204
205 const reusableChangedPropsEntry = ['Changed Props', ''];
206
207 const reusableCascadingUpdateIssue = {
208 name: 'React: Cascading Update',
209 severity: 'warning',
210 description:
211 'A cascading update is an update that is triggered during an ongoing render. This can lead to performance issues.',
212 learnMoreUrl:
213 'https://react.dev/reference/dev-tools/react-performance-tracks#cascading-updates',
214 };
215
216 const DEEP_EQUALITY_WARNING =
217 'This component received deeply equal props. It might benefit from useMemo or the React Compiler in its owner.';
218
219 const reusableDeeplyEqualPropsEntry = ['Changed Props', DEEP_EQUALITY_WARNING];
220
221 export function logComponentRender(
222 fiber: Fiber,
223 startTime: number,
224 endTime: number,
225 wasHydrated: boolean,
226 committedLanes: Lanes,
227 ): void {
228 const name = getComponentNameFromFiber(fiber);
229 if (name === null) {
230 // Skip
231 return;
232 }
233 if (supportsUserTiming) {
234 const alternate = fiber.alternate;
235 let selfTime: number = fiber.actualDuration as any;
236 if (alternate === null || alternate.child !== fiber.child) {
237 for (let child = fiber.child; child !== null; child = child.sibling) {
238 selfTime -= child.actualDuration as any;
239 }
240 }
241 const color =
242 selfTime < 0.5
243 ? wasHydrated
244 ? 'tertiary-light'
245 : 'primary-light'
246 : selfTime < 10
247 ? wasHydrated
248 ? 'tertiary'
249 : 'primary'
250 : selfTime < 100
251 ? wasHydrated
252 ? 'tertiary-dark'
253 : 'primary-dark'
254 : 'error';
255
256 if (!__DEV__) {
257 console.timeStamp(
258 name,
259 startTime,
260 endTime,
261 COMPONENTS_TRACK,
262 undefined,
263 color,
264 );
265 } else {
266 const props = fiber.memoizedProps;
267 const debugTask = fiber._debugTask;
268
269 if (
270 props !== null &&
271 alternate !== null &&
272 alternate.memoizedProps !== props
273 ) {
274 // If this is an update, we'll diff the props and emit which ones changed.
275 const properties: Array<[string, string]> = [reusableChangedPropsEntry];
276 const isDeeplyEqual = addObjectDiffToProperties(
277 alternate.memoizedProps,
278 props,
279 properties,
280 0,
281 );
282 if (properties.length > 1) {
283 if (
284 isDeeplyEqual &&
285 !alreadyWarnedForDeepEquality &&
286 !includesSomeLane(alternate.lanes, committedLanes) &&
287 (fiber.actualDuration as any) > 100
288 ) {
289 alreadyWarnedForDeepEquality = true;
290 // This is the first component in a subtree which rerendered with deeply equal props
291 // and didn't have its own work scheduled and took a non-trivial amount of time.
292 // We highlight this for further inspection.
293 // Note that we only consider this case if properties.length > 1 which it will only
294 // be if we have emitted any diffs. We'd only emit diffs if there were any nested
295 // equal objects. Therefore, we don't warn for simple shallow equality.
296 properties[0] = reusableDeeplyEqualPropsEntry;
297 reusableComponentDevToolDetails.color = 'warning';
298 reusableComponentDevToolDetails.tooltipText = DEEP_EQUALITY_WARNING;
299 } else {
300 reusableComponentDevToolDetails.color = color;
301 reusableComponentDevToolDetails.tooltipText = name;
302 }
303 reusableComponentDevToolDetails.properties = properties;
304 reusableComponentOptions.start = startTime;
305 reusableComponentOptions.end = endTime;
306
307 const measureName = '\u200b' + name;
308 if (debugTask != null) {
309 debugTask.run(
310 // $FlowFixMe[method-unbinding]
311 performance.measure.bind(
312 performance,
313 measureName,
314 reusableComponentOptions,
315 ),
316 );
317 } else {
318 performance.measure(measureName, reusableComponentOptions);
319 }
320 performance.clearMeasures(measureName);
321 } else {
322 if (debugTask != null) {
323 debugTask.run(
324 // $FlowFixMe[method-unbinding]
325 console.timeStamp.bind(
326 console,
327 name,
328 startTime,
329 endTime,
330 COMPONENTS_TRACK,
331 undefined,
332 color,
333 ),
334 );
335 } else {
336 console.timeStamp(
337 name,
338 startTime,
339 endTime,
340 COMPONENTS_TRACK,
341 undefined,
342 color,
343 );
344 }
345 }
346 } else {
347 if (debugTask != null) {
348 debugTask.run(
349 // $FlowFixMe[method-unbinding]
350 console.timeStamp.bind(
351 console,
352 name,
353 startTime,
354 endTime,
355 COMPONENTS_TRACK,
356 undefined,
357 color,
358 ),
359 );
360 } else {
361 console.timeStamp(
362 name,
363 startTime,
364 endTime,
365 COMPONENTS_TRACK,
366 undefined,
367 color,
368 );
369 }
370 }
371 }
372 }
373 }
374
375 export function logComponentErrored(
376 fiber: Fiber,
377 startTime: number,
378 endTime: number,
379 errors: Array<CapturedValue<mixed>>,
380 ): void {
381 if (supportsUserTiming) {
382 const name = getComponentNameFromFiber(fiber);
383 if (name === null) {
384 // Skip
385 return;
386 }
387 if (__DEV__) {
388 let debugTask: ?ConsoleTask = null;
389 const properties: Array<[string, string]> = [];
390 for (let i = 0; i < errors.length; i++) {
391 const capturedValue = errors[i];
392 if (debugTask == null && capturedValue.source !== null) {
393 // If the captured value has a source Fiber, use its debugTask for
394 // the stack instead of the error boundary's stack. So you can find
395 // which component errored since we don't show the errored render tree.
396 // TODO: Ideally we should instead, store the failed fibers and log the
397 // whole subtree including the component that errored.
398 debugTask = capturedValue.source._debugTask;
399 }
400 const error = capturedValue.value;
401 const message =
402 typeof error === 'object' &&
403 error !== null &&
404 typeof error.message === 'string'
405 ? // eslint-disable-next-line react-internal/safe-string-coercion
406 String(error.message)
407 : // eslint-disable-next-line react-internal/safe-string-coercion
408 String(error);
409 properties.push(['Error', message]);
410 }
411 if (fiber.key !== null) {
412 addValueToProperties('key', fiber.key, properties, 0, '');
413 }
414 if (fiber.memoizedProps !== null) {
415 addObjectToProperties(fiber.memoizedProps, properties, 0, '');
416 }
417 if (debugTask == null) {
418 // If the captured values don't have a debug task, fallback to the
419 // error boundary itself.
420 debugTask = fiber._debugTask;
421 }
422 const options: PerformanceMeasureOptions = {
423 start: startTime,
424 end: endTime,
425 detail: {
426 devtools: {
427 color: 'error',
428 track: COMPONENTS_TRACK,
429 tooltipText:
430 fiber.tag === SuspenseComponent
431 ? 'Hydration failed'
432 : 'Error boundary caught an error',
433 properties,
434 },
435 },
436 };
437
438 const measureName = '\u200b' + name;
439 if (__DEV__ && debugTask) {
440 debugTask.run(
441 // $FlowFixMe[method-unbinding]
442 performance.measure.bind(performance, measureName, options),
443 );
444 } else {
445 performance.measure(measureName, options);
446 }
447 performance.clearMeasures(measureName);
448 } else {
449 console.timeStamp(
450 name,
451 startTime,
452 endTime,
453 COMPONENTS_TRACK,
454 undefined,
455 'error',
456 );
457 }
458 }
459 }
460
461 function logComponentEffectErrored(
462 fiber: Fiber,
463 startTime: number,
464 endTime: number,
465 errors: Array<CapturedValue<mixed>>,
466 ): void {
467 if (supportsUserTiming) {
468 const name = getComponentNameFromFiber(fiber);
469 if (name === null) {
470 // Skip
471 return;
472 }
473 if (__DEV__) {
474 const properties: Array<[string, string]> = [];
475 for (let i = 0; i < errors.length; i++) {
476 const capturedValue = errors[i];
477 const error = capturedValue.value;
478 const message =
479 typeof error === 'object' &&
480 error !== null &&
481 typeof error.message === 'string'
482 ? // eslint-disable-next-line react-internal/safe-string-coercion
483 String(error.message)
484 : // eslint-disable-next-line react-internal/safe-string-coercion
485 String(error);
486 properties.push(['Error', message]);
487 }
488 if (fiber.key !== null) {
489 addValueToProperties('key', fiber.key, properties, 0, '');
490 }
491 if (fiber.memoizedProps !== null) {
492 addObjectToProperties(fiber.memoizedProps, properties, 0, '');
493 }
494 const options = {
495 start: startTime,
496 end: endTime,
497 detail: {
498 devtools: {
499 color: 'error',
500 track: COMPONENTS_TRACK,
501 tooltipText: 'A lifecycle or effect errored',
502 properties,
503 },
504 },
505 };
506 const debugTask = fiber._debugTask;
507 const measureName = '\u200b' + name;
508 if (debugTask) {
509 debugTask.run(
510 // $FlowFixMe[method-unbinding]
511 performance.measure.bind(performance, measureName, options),
512 );
513 } else {
514 // $FlowFixMe[incompatible-type]
515 performance.measure(measureName, options);
516 }
517 performance.clearMeasures(measureName);
518 } else {
519 console.timeStamp(
520 name,
521 startTime,
522 endTime,
523 COMPONENTS_TRACK,
524 undefined,
525 'error',
526 );
527 }
528 }
529 }
530
531 export function logComponentEffect(
532 fiber: Fiber,
533 startTime: number,
534 endTime: number,
535 selfTime: number,
536 errors: null | Array<CapturedValue<mixed>>,
537 ): void {
538 if (errors !== null) {
539 logComponentEffectErrored(fiber, startTime, endTime, errors);
540 return;
541 }
542 const name = getComponentNameFromFiber(fiber);
543 if (name === null) {
544 // Skip
545 return;
546 }
547 if (supportsUserTiming) {
548 const color =
549 selfTime < 1
550 ? 'secondary-light'
551 : selfTime < 100
552 ? 'secondary'
553 : selfTime < 500
554 ? 'secondary-dark'
555 : 'error';
556 const debugTask = fiber._debugTask;
557 if (__DEV__ && debugTask) {
558 debugTask.run(
559 // $FlowFixMe[method-unbinding]
560 console.timeStamp.bind(
561 console,
562 name,
563 startTime,
564 endTime,
565 COMPONENTS_TRACK,
566 undefined,
567 color,
568 ),
569 );
570 } else {
571 console.timeStamp(
572 name,
573 startTime,
574 endTime,
575 COMPONENTS_TRACK,
576 undefined,
577 color,
578 );
579 }
580 }
581 }
582
583 export function logYieldTime(startTime: number, endTime: number): void {
584 if (supportsUserTiming) {
585 const yieldDuration = endTime - startTime;
586 if (yieldDuration < 3) {
587 // Skip sub-millisecond yields. This happens all the time and is not interesting.
588 return;
589 }
590 // Being blocked on CPU is potentially bad so we color it by how long it took.
591 const color =
592 yieldDuration < 5
593 ? 'primary-light'
594 : yieldDuration < 10
595 ? 'primary'
596 : yieldDuration < 100
597 ? 'primary-dark'
598 : 'error';
599 // This get logged in the components track if we don't commit which leaves them
600 // hanging by themselves without context. It's a useful indicator for why something
601 // might be starving this render though.
602 // TODO: Considering adding these to a queue and only logging them if we commit.
603 console.timeStamp(
604 'Blocked',
605 startTime,
606 endTime,
607 COMPONENTS_TRACK,
608 undefined,
609 color,
610 );
611 }
612 }
613
614 export function logSuspendedYieldTime(
615 startTime: number,
616 endTime: number,
617 suspendedFiber: Fiber,
618 ): void {
619 if (supportsUserTiming) {
620 const debugTask = suspendedFiber._debugTask;
621 if (__DEV__ && debugTask) {
622 debugTask.run(
623 // $FlowFixMe[method-unbinding]
624 console.timeStamp.bind(
625 console,
626 'Suspended',
627 startTime,
628 endTime,
629 COMPONENTS_TRACK,
630 undefined,
631 'primary-light',
632 ),
633 );
634 } else {
635 console.timeStamp(
636 'Suspended',
637 startTime,
638 endTime,
639 COMPONENTS_TRACK,
640 undefined,
641 'primary-light',
642 );
643 }
644 }
645 }
646
647 export function logActionYieldTime(
648 startTime: number,
649 endTime: number,
650 suspendedFiber: Fiber,
651 ): void {
652 if (supportsUserTiming) {
653 const debugTask = suspendedFiber._debugTask;
654 if (__DEV__ && debugTask) {
655 debugTask.run(
656 // $FlowFixMe[method-unbinding]
657 console.timeStamp.bind(
658 console,
659 'Action',
660 startTime,
661 endTime,
662 COMPONENTS_TRACK,
663 undefined,
664 'primary-light',
665 ),
666 );
667 } else {
668 console.timeStamp(
669 'Action',
670 startTime,
671 endTime,
672 COMPONENTS_TRACK,
673 undefined,
674 'primary-light',
675 );
676 }
677 }
678 }
679
680 export function logBlockingStart(
681 updateTime: number,
682 eventTime: number,
683 eventType: null | string,
684 eventIsRepeat: boolean,
685 isSpawnedUpdate: boolean,
686 isPingedUpdate: boolean,
687 renderStartTime: number,
688 lanes: Lanes,
689 debugTask: null | ConsoleTask, // DEV-only
690 updateMethodName: null | string,
691 updateComponentName: null | string,
692 ): void {
693 if (supportsUserTiming) {
694 currentTrack = 'Blocking';
695 // Clamp start times
696 if (updateTime > 0) {
697 if (updateTime > renderStartTime) {
698 updateTime = renderStartTime;
699 }
700 } else {
701 updateTime = renderStartTime;
702 }
703 if (eventTime > 0) {
704 if (eventTime > updateTime) {
705 eventTime = updateTime;
706 }
707 } else {
708 eventTime = updateTime;
709 }
710 // If a blocking update was spawned within render or an effect, that's considered a cascading render.
711 // If you have a second blocking update within the same event, that suggests multiple flushSync or
712 // setState in a microtask which is also considered a cascade.
713 if (eventType !== null && updateTime > eventTime) {
714 // Log the time from the event timeStamp until we called setState.
715 const color = eventIsRepeat ? 'secondary-light' : 'warning';
716 if (__DEV__ && debugTask) {
717 debugTask.run(
718 // $FlowFixMe[method-unbinding]
719 console.timeStamp.bind(
720 console,
721 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
722 eventTime,
723 updateTime,
724 currentTrack,
725 LANES_TRACK_GROUP,
726 color,
727 ),
728 );
729 } else {
730 console.timeStamp(
731 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
732 eventTime,
733 updateTime,
734 currentTrack,
735 LANES_TRACK_GROUP,
736 color,
737 );
738 }
739 }
740 if (renderStartTime > updateTime) {
741 // Log the time from when we called setState until we started rendering.
742 const color = isSpawnedUpdate
743 ? 'error'
744 : includesOnlyHydrationOrOffscreenLanes(lanes)
745 ? 'tertiary-light'
746 : 'primary-light';
747 const label = isPingedUpdate
748 ? 'Promise Resolved'
749 : isSpawnedUpdate
750 ? 'Cascading Update'
751 : renderStartTime - updateTime > 5
752 ? 'Update Blocked'
753 : 'Update';
754
755 if (__DEV__) {
756 const properties = [];
757 if (updateComponentName != null) {
758 properties.push(['Component name', updateComponentName]);
759 }
760 if (updateMethodName != null) {
761 properties.push(['Method name', updateMethodName]);
762 }
763 const measureOptions = {
764 start: updateTime,
765 end: renderStartTime,
766 detail: {
767 devtools: {
768 properties,
769 track: currentTrack,
770 trackGroup: LANES_TRACK_GROUP,
771 color,
772 },
773 },
774 };
775 if (enablePerformanceIssueReporting && isSpawnedUpdate) {
776 // $FlowFixMe[prop-missing] - detail is untyped
777 measureOptions.detail.devtools.performanceIssue =
778 reusableCascadingUpdateIssue;
779 }
780
781 if (debugTask) {
782 debugTask.run(
783 // $FlowFixMe[method-unbinding]
784 performance.measure.bind(performance, label, measureOptions),
785 );
786 } else {
787 // $FlowFixMe[incompatible-type]
788 performance.measure(label, measureOptions);
789 }
790 performance.clearMeasures(label);
791 } else {
792 console.timeStamp(
793 label,
794 updateTime,
795 renderStartTime,
796 currentTrack,
797 LANES_TRACK_GROUP,
798 color,
799 );
800 }
801 }
802 }
803 }
804
805 export function logGestureStart(
806 updateTime: number,
807 eventTime: number,
808 eventType: null | string,
809 eventIsRepeat: boolean,
810 isPingedUpdate: boolean,
811 renderStartTime: number,
812 debugTask: null | ConsoleTask, // DEV-only
813 updateMethodName: null | string,
814 updateComponentName: null | string,
815 ): void {
816 if (supportsUserTiming) {
817 currentTrack = 'Gesture';
818 // Clamp start times
819 if (updateTime > 0) {
820 if (updateTime > renderStartTime) {
821 updateTime = renderStartTime;
822 }
823 } else {
824 updateTime = renderStartTime;
825 }
826 if (eventTime > 0) {
827 if (eventTime > updateTime) {
828 eventTime = updateTime;
829 }
830 } else {
831 eventTime = updateTime;
832 }
833
834 if (updateTime > eventTime && eventType !== null) {
835 // Log the time from the event timeStamp until we started a gesture.
836 const color = eventIsRepeat ? 'secondary-light' : 'warning';
837 if (__DEV__ && debugTask) {
838 debugTask.run(
839 console.timeStamp.bind(
840 console,
841 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
842 eventTime,
843 updateTime,
844 currentTrack,
845 LANES_TRACK_GROUP,
846 color,
847 ),
848 );
849 } else {
850 console.timeStamp(
851 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
852 eventTime,
853 updateTime,
854 currentTrack,
855 LANES_TRACK_GROUP,
856 color,
857 );
858 }
859 }
860 if (renderStartTime > updateTime) {
861 // Log the time from when we called setState until we started rendering.
862 const label = isPingedUpdate
863 ? 'Promise Resolved'
864 : renderStartTime - updateTime > 5
865 ? 'Gesture Blocked'
866 : 'Gesture';
867 if (__DEV__) {
868 const properties = [];
869 if (updateComponentName != null) {
870 properties.push(['Component name', updateComponentName]);
871 }
872 if (updateMethodName != null) {
873 properties.push(['Method name', updateMethodName]);
874 }
875 const measureOptions = {
876 start: updateTime,
877 end: renderStartTime,
878 detail: {
879 devtools: {
880 properties,
881 track: currentTrack,
882 trackGroup: LANES_TRACK_GROUP,
883 color: 'primary-light',
884 },
885 },
886 };
887
888 if (debugTask) {
889 debugTask.run(
890 // $FlowFixMe[method-unbinding]
891 performance.measure.bind(performance, label, measureOptions),
892 );
893 } else {
894 // $FlowFixMe[incompatible-type]
895 performance.measure(label, measureOptions);
896 }
897 performance.clearMeasures(label);
898 } else {
899 console.timeStamp(
900 label,
901 updateTime,
902 renderStartTime,
903 currentTrack,
904 LANES_TRACK_GROUP,
905 'primary-light',
906 );
907 }
908 }
909 }
910 }
911
912 export function logTransitionStart(
913 startTime: number,
914 updateTime: number,
915 eventTime: number,
916 eventType: null | string,
917 eventIsRepeat: boolean,
918 isPingedUpdate: boolean,
919 renderStartTime: number,
920 debugTask: null | ConsoleTask, // DEV-only
921 updateMethodName: null | string,
922 updateComponentName: null | string,
923 ): void {
924 if (supportsUserTiming) {
925 currentTrack = 'Transition';
926 // Clamp start times
927 if (updateTime > 0) {
928 if (updateTime > renderStartTime) {
929 updateTime = renderStartTime;
930 }
931 } else {
932 updateTime = renderStartTime;
933 }
934 if (startTime > 0) {
935 if (startTime > updateTime) {
936 startTime = updateTime;
937 }
938 } else {
939 startTime = updateTime;
940 }
941 if (eventTime > 0) {
942 if (eventTime > startTime) {
943 eventTime = startTime;
944 }
945 } else {
946 eventTime = startTime;
947 }
948
949 if (startTime > eventTime && eventType !== null) {
950 // Log the time from the event timeStamp until we started a transition.
951 const color = eventIsRepeat ? 'secondary-light' : 'warning';
952 if (__DEV__ && debugTask) {
953 debugTask.run(
954 console.timeStamp.bind(
955 console,
956 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
957 eventTime,
958 startTime,
959 currentTrack,
960 LANES_TRACK_GROUP,
961 color,
962 ),
963 );
964 } else {
965 console.timeStamp(
966 eventIsRepeat ? 'Consecutive' : 'Event: ' + eventType,
967 eventTime,
968 startTime,
969 currentTrack,
970 LANES_TRACK_GROUP,
971 color,
972 );
973 }
974 }
975 if (updateTime > startTime) {
976 // Log the time from when we started an async transition until we called setState or started rendering.
977 // TODO: Ideally this would use the debugTask of the startTransition call perhaps.
978 if (__DEV__ && debugTask) {
979 debugTask.run(
980 // $FlowFixMe[method-unbinding]
981 console.timeStamp.bind(
982 console,
983 'Action',
984 startTime,
985 updateTime,
986 currentTrack,
987 LANES_TRACK_GROUP,
988 'primary-dark',
989 ),
990 );
991 } else {
992 console.timeStamp(
993 'Action',
994 startTime,
995 updateTime,
996 currentTrack,
997 LANES_TRACK_GROUP,
998 'primary-dark',
999 );
1000 }
1001 }
1002 if (renderStartTime > updateTime) {
1003 // Log the time from when we called setState until we started rendering.
1004 const label = isPingedUpdate
1005 ? 'Promise Resolved'
1006 : renderStartTime - updateTime > 5
1007 ? 'Update Blocked'
1008 : 'Update';
1009 if (__DEV__) {
1010 const properties = [];
1011 if (updateComponentName != null) {
1012 properties.push(['Component name', updateComponentName]);
1013 }
1014 if (updateMethodName != null) {
1015 properties.push(['Method name', updateMethodName]);
1016 }
1017 const measureOptions = {
1018 start: updateTime,
1019 end: renderStartTime,
1020 detail: {
1021 devtools: {
1022 properties,
1023 track: currentTrack,
1024 trackGroup: LANES_TRACK_GROUP,
1025 color: 'primary-light',
1026 },
1027 },
1028 };
1029
1030 if (debugTask) {
1031 debugTask.run(
1032 // $FlowFixMe[method-unbinding]
1033 performance.measure.bind(performance, label, measureOptions),
1034 );
1035 } else {
1036 // $FlowFixMe[incompatible-type]
1037 performance.measure(label, measureOptions);
1038 }
1039 performance.clearMeasures(label);
1040 } else {
1041 console.timeStamp(
1042 label,
1043 updateTime,
1044 renderStartTime,
1045 currentTrack,
1046 LANES_TRACK_GROUP,
1047 'primary-light',
1048 );
1049 }
1050 }
1051 }
1052 }
1053
1054 export function logRenderPhase(
1055 startTime: number,
1056 endTime: number,
1057 lanes: Lanes,
1058 debugTask: null | ConsoleTask,
1059 ): void {
1060 if (supportsUserTiming) {
1061 if (endTime <= startTime) {
1062 return;
1063 }
1064 const color = includesOnlyHydrationOrOffscreenLanes(lanes)
1065 ? 'tertiary-dark'
1066 : 'primary-dark';
1067 const label = includesOnlyOffscreenLanes(lanes)
1068 ? 'Prepared'
1069 : includesOnlyHydrationLanes(lanes)
1070 ? 'Hydrated'
1071 : 'Render';
1072 if (__DEV__ && debugTask) {
1073 debugTask.run(
1074 // $FlowFixMe[method-unbinding]
1075 console.timeStamp.bind(
1076 console,
1077 label,
1078 startTime,
1079 endTime,
1080 currentTrack,
1081 LANES_TRACK_GROUP,
1082 color,
1083 ),
1084 );
1085 } else {
1086 console.timeStamp(
1087 label,
1088 startTime,
1089 endTime,
1090 currentTrack,
1091 LANES_TRACK_GROUP,
1092 color,
1093 );
1094 }
1095 }
1096 }
1097
1098 export function logInterruptedRenderPhase(
1099 startTime: number,
1100 endTime: number,
1101 lanes: Lanes,
1102 debugTask: null | ConsoleTask,
1103 ): void {
1104 if (supportsUserTiming) {
1105 if (endTime <= startTime) {
1106 return;
1107 }
1108 const color = includesOnlyHydrationOrOffscreenLanes(lanes)
1109 ? 'tertiary-dark'
1110 : 'primary-dark';
1111 const label = includesOnlyOffscreenLanes(lanes)
1112 ? 'Prewarm'
1113 : includesOnlyHydrationLanes(lanes)
1114 ? 'Interrupted Hydration'
1115 : 'Interrupted Render';
1116 if (__DEV__ && debugTask) {
1117 debugTask.run(
1118 // $FlowFixMe[method-unbinding]
1119 console.timeStamp.bind(
1120 console,
1121 label,
1122 startTime,
1123 endTime,
1124 currentTrack,
1125 LANES_TRACK_GROUP,
1126 color,
1127 ),
1128 );
1129 } else {
1130 console.timeStamp(
1131 label,
1132 startTime,
1133 endTime,
1134 currentTrack,
1135 LANES_TRACK_GROUP,
1136 color,
1137 );
1138 }
1139 }
1140 }
1141
1142 export function logSuspendedRenderPhase(
1143 startTime: number,
1144 endTime: number,
1145 lanes: Lanes,
1146 debugTask: null | ConsoleTask,
1147 ): void {
1148 if (supportsUserTiming) {
1149 if (endTime <= startTime) {
1150 return;
1151 }
1152 const color = includesOnlyHydrationOrOffscreenLanes(lanes)
1153 ? 'tertiary-dark'
1154 : 'primary-dark';
1155 if (__DEV__ && debugTask) {
1156 debugTask.run(
1157 // $FlowFixMe[method-unbinding]
1158 console.timeStamp.bind(
1159 console,
1160 'Prewarm',
1161 startTime,
1162 endTime,
1163 currentTrack,
1164 LANES_TRACK_GROUP,
1165 color,
1166 ),
1167 );
1168 } else {
1169 console.timeStamp(
1170 'Prewarm',
1171 startTime,
1172 endTime,
1173 currentTrack,
1174 LANES_TRACK_GROUP,
1175 color,
1176 );
1177 }
1178 }
1179 }
1180
1181 export function logSuspendedWithDelayPhase(
1182 startTime: number,
1183 endTime: number,
1184 lanes: Lanes,
1185 debugTask: null | ConsoleTask,
1186 ): void {
1187 // This means the render was suspended and cannot commit until it gets unblocked.
1188 if (supportsUserTiming) {
1189 if (endTime <= startTime) {
1190 return;
1191 }
1192 const color = includesOnlyHydrationOrOffscreenLanes(lanes)
1193 ? 'tertiary-dark'
1194 : 'primary-dark';
1195 if (__DEV__ && debugTask) {
1196 debugTask.run(
1197 // $FlowFixMe[method-unbinding]
1198 console.timeStamp.bind(
1199 console,
1200 'Suspended',
1201 startTime,
1202 endTime,
1203 currentTrack,
1204 LANES_TRACK_GROUP,
1205 color,
1206 ),
1207 );
1208 } else {
1209 console.timeStamp(
1210 'Suspended',
1211 startTime,
1212 endTime,
1213 currentTrack,
1214 LANES_TRACK_GROUP,
1215 color,
1216 );
1217 }
1218 }
1219 }
1220
1221 export function logRecoveredRenderPhase(
1222 startTime: number,
1223 endTime: number,
1224 lanes: Lanes,
1225 recoverableErrors: Array<CapturedValue<mixed>>,
1226 hydrationFailed: boolean,
1227 debugTask: null | ConsoleTask,
1228 ): void {
1229 if (supportsUserTiming) {
1230 if (endTime <= startTime) {
1231 return;
1232 }
1233 if (__DEV__) {
1234 const properties: Array<[string, string]> = [];
1235 for (let i = 0; i < recoverableErrors.length; i++) {
1236 const capturedValue = recoverableErrors[i];
1237 const error = capturedValue.value;
1238 const message =
1239 typeof error === 'object' &&
1240 error !== null &&
1241 typeof error.message === 'string'
1242 ? // eslint-disable-next-line react-internal/safe-string-coercion
1243 String(error.message)
1244 : // eslint-disable-next-line react-internal/safe-string-coercion
1245 String(error);
1246 properties.push(['Recoverable Error', message]);
1247 }
1248 const options: PerformanceMeasureOptions = {
1249 start: startTime,
1250 end: endTime,
1251 detail: {
1252 devtools: {
1253 color: 'primary-dark',
1254 track: currentTrack,
1255 trackGroup: LANES_TRACK_GROUP,
1256 tooltipText: hydrationFailed
1257 ? 'Hydration Failed'
1258 : 'Recovered after Error',
1259 properties,
1260 },
1261 },
1262 };
1263 if (debugTask) {
1264 debugTask.run(
1265 // $FlowFixMe[method-unbinding]
1266 performance.measure.bind(performance, 'Recovered', options),
1267 );
1268 } else {
1269 performance.measure('Recovered', options);
1270 }
1271 performance.clearMeasures('Recovered');
1272 } else {
1273 console.timeStamp(
1274 'Recovered',
1275 startTime,
1276 endTime,
1277 currentTrack,
1278 LANES_TRACK_GROUP,
1279 'error',
1280 );
1281 }
1282 }
1283 }
1284
1285 export function logErroredRenderPhase(
1286 startTime: number,
1287 endTime: number,
1288 lanes: Lanes,
1289 debugTask: null | ConsoleTask,
1290 ): void {
1291 if (supportsUserTiming) {
1292 if (endTime <= startTime) {
1293 return;
1294 }
1295 if (__DEV__ && debugTask) {
1296 debugTask.run(
1297 // $FlowFixMe[method-unbinding]
1298 console.timeStamp.bind(
1299 console,
1300 'Errored',
1301 startTime,
1302 endTime,
1303 currentTrack,
1304 LANES_TRACK_GROUP,
1305 'error',
1306 ),
1307 );
1308 } else {
1309 console.timeStamp(
1310 'Errored',
1311 startTime,
1312 endTime,
1313 currentTrack,
1314 LANES_TRACK_GROUP,
1315 'error',
1316 );
1317 }
1318 }
1319 }
1320
1321 export function logInconsistentRender(
1322 startTime: number,
1323 endTime: number,
1324 debugTask: null | ConsoleTask,
1325 ): void {
1326 if (supportsUserTiming) {
1327 if (endTime <= startTime) {
1328 return;
1329 }
1330 if (__DEV__ && debugTask) {
1331 debugTask.run(
1332 // $FlowFixMe[method-unbinding]
1333 console.timeStamp.bind(
1334 console,
1335 'Teared Render',
1336 startTime,
1337 endTime,
1338 currentTrack,
1339 LANES_TRACK_GROUP,
1340 'error',
1341 ),
1342 );
1343 } else {
1344 console.timeStamp(
1345 'Teared Render',
1346 startTime,
1347 endTime,
1348 currentTrack,
1349 LANES_TRACK_GROUP,
1350 'error',
1351 );
1352 }
1353 }
1354 }
1355
1356 export function logSuspendedCommitPhase(
1357 startTime: number,
1358 endTime: number,
1359 reason: string,
1360 debugTask: null | ConsoleTask,
1361 ): void {
1362 // This means the commit was suspended on CSS or images.
1363 if (supportsUserTiming) {
1364 if (endTime <= startTime) {
1365 return;
1366 }
1367 // TODO: Include the exact reason and URLs of what resources suspended.
1368 // TODO: This might also be Suspended while waiting on a View Transition.
1369 if (__DEV__ && debugTask) {
1370 debugTask.run(
1371 // $FlowFixMe[method-unbinding]
1372 console.timeStamp.bind(
1373 console,
1374 reason,
1375 startTime,
1376 endTime,
1377 currentTrack,
1378 LANES_TRACK_GROUP,
1379 'secondary-light',
1380 ),
1381 );
1382 } else {
1383 console.timeStamp(
1384 reason,
1385 startTime,
1386 endTime,
1387 currentTrack,
1388 LANES_TRACK_GROUP,
1389 'secondary-light',
1390 );
1391 }
1392 }
1393 }
1394
1395 export function logSuspendedViewTransitionPhase(
1396 startTime: number,
1397 endTime: number,
1398 reason: string,
1399 debugTask: null | ConsoleTask,
1400 ): void {
1401 // This means the commit was suspended on CSS or images.
1402 if (supportsUserTiming) {
1403 if (endTime <= startTime) {
1404 return;
1405 }
1406 // TODO: Include the exact reason and URLs of what resources suspended.
1407 // TODO: This might also be Suspended while waiting on a View Transition.
1408 if (__DEV__ && debugTask) {
1409 debugTask.run(
1410 // $FlowFixMe[method-unbinding]
1411 console.timeStamp.bind(
1412 console,
1413 reason,
1414 startTime,
1415 endTime,
1416 currentTrack,
1417 LANES_TRACK_GROUP,
1418 'secondary-light',
1419 ),
1420 );
1421 } else {
1422 console.timeStamp(
1423 reason,
1424 startTime,
1425 endTime,
1426 currentTrack,
1427 LANES_TRACK_GROUP,
1428 'secondary-light',
1429 );
1430 }
1431 }
1432 }
1433
1434 export function logCommitErrored(
1435 startTime: number,
1436 endTime: number,
1437 errors: Array<CapturedValue<mixed>>,
1438 passive: boolean,
1439 debugTask: null | ConsoleTask,
1440 ): void {
1441 if (supportsUserTiming) {
1442 if (endTime <= startTime) {
1443 return;
1444 }
1445 if (__DEV__) {
1446 const properties: Array<[string, string]> = [];
1447 for (let i = 0; i < errors.length; i++) {
1448 const capturedValue = errors[i];
1449 const error = capturedValue.value;
1450 const message =
1451 typeof error === 'object' &&
1452 error !== null &&
1453 typeof error.message === 'string'
1454 ? // eslint-disable-next-line react-internal/safe-string-coercion
1455 String(error.message)
1456 : // eslint-disable-next-line react-internal/safe-string-coercion
1457 String(error);
1458 properties.push(['Error', message]);
1459 }
1460 const options: PerformanceMeasureOptions = {
1461 start: startTime,
1462 end: endTime,
1463 detail: {
1464 devtools: {
1465 color: 'error',
1466 track: currentTrack,
1467 trackGroup: LANES_TRACK_GROUP,
1468 tooltipText: passive
1469 ? 'Remaining Effects Errored'
1470 : 'Commit Errored',
1471 properties,
1472 },
1473 },
1474 };
1475 if (debugTask) {
1476 debugTask.run(
1477 // $FlowFixMe[method-unbinding]
1478 performance.measure.bind(performance, 'Errored', options),
1479 );
1480 } else {
1481 performance.measure('Errored', options);
1482 }
1483 performance.clearMeasures('Errored');
1484 } else {
1485 console.timeStamp(
1486 'Errored',
1487 startTime,
1488 endTime,
1489 currentTrack,
1490 LANES_TRACK_GROUP,
1491 'error',
1492 );
1493 }
1494 }
1495 }
1496
1497 export function logCommitPhase(
1498 startTime: number,
1499 endTime: number,
1500 errors: null | Array<CapturedValue<mixed>>,
1501 abortedViewTransition: boolean,
1502 debugTask: null | ConsoleTask,
1503 ): void {
1504 if (errors !== null) {
1505 logCommitErrored(startTime, endTime, errors, false, debugTask);
1506 return;
1507 }
1508 if (supportsUserTiming) {
1509 if (endTime <= startTime) {
1510 return;
1511 }
1512 if (__DEV__ && debugTask) {
1513 debugTask.run(
1514 // $FlowFixMe[method-unbinding]
1515 console.timeStamp.bind(
1516 console,
1517 abortedViewTransition
1518 ? 'Commit Interrupted View Transition'
1519 : 'Commit',
1520 startTime,
1521 endTime,
1522 currentTrack,
1523 LANES_TRACK_GROUP,
1524 abortedViewTransition ? 'error' : 'secondary-dark',
1525 ),
1526 );
1527 } else {
1528 console.timeStamp(
1529 abortedViewTransition ? 'Commit Interrupted View Transition' : 'Commit',
1530 startTime,
1531 endTime,
1532 currentTrack,
1533 LANES_TRACK_GROUP,
1534 abortedViewTransition ? 'error' : 'secondary-dark',
1535 );
1536 }
1537 }
1538 }
1539
1540 export function logPaintYieldPhase(
1541 startTime: number,
1542 endTime: number,
1543 delayedUntilPaint: boolean,
1544 debugTask: null | ConsoleTask,
1545 ): void {
1546 if (supportsUserTiming) {
1547 if (endTime <= startTime) {
1548 return;
1549 }
1550 if (__DEV__ && debugTask) {
1551 debugTask.run(
1552 // $FlowFixMe[method-unbinding]
1553 console.timeStamp.bind(
1554 console,
1555 delayedUntilPaint ? 'Waiting for Paint' : 'Waiting',
1556 startTime,
1557 endTime,
1558 currentTrack,
1559 LANES_TRACK_GROUP,
1560 'secondary-light',
1561 ),
1562 );
1563 } else {
1564 console.timeStamp(
1565 delayedUntilPaint ? 'Waiting for Paint' : 'Waiting',
1566 startTime,
1567 endTime,
1568 currentTrack,
1569 LANES_TRACK_GROUP,
1570 'secondary-light',
1571 );
1572 }
1573 }
1574 }
1575
1576 export function logApplyGesturePhase(
1577 startTime: number,
1578 endTime: number,
1579 debugTask: null | ConsoleTask,
1580 ): void {
1581 if (supportsUserTiming) {
1582 if (endTime <= startTime) {
1583 return;
1584 }
1585 if (__DEV__ && debugTask) {
1586 debugTask.run(
1587 // $FlowFixMe[method-unbinding]
1588 console.timeStamp.bind(
1589 console,
1590 'Create Ghost Tree',
1591 startTime,
1592 endTime,
1593 currentTrack,
1594 LANES_TRACK_GROUP,
1595 'secondary-dark',
1596 ),
1597 );
1598 } else {
1599 console.timeStamp(
1600 'Create Ghost Tree',
1601 startTime,
1602 endTime,
1603 currentTrack,
1604 LANES_TRACK_GROUP,
1605 'secondary-dark',
1606 );
1607 }
1608 }
1609 }
1610
1611 export function logStartViewTransitionYieldPhase(
1612 startTime: number,
1613 endTime: number,
1614 abortedViewTransition: boolean,
1615 debugTask: null | ConsoleTask,
1616 ): void {
1617 if (supportsUserTiming) {
1618 if (endTime <= startTime) {
1619 return;
1620 }
1621 if (__DEV__ && debugTask) {
1622 debugTask.run(
1623 // $FlowFixMe[method-unbinding]
1624 console.timeStamp.bind(
1625 console,
1626 abortedViewTransition
1627 ? 'Interrupted View Transition'
1628 : 'Starting Animation',
1629 startTime,
1630 endTime,
1631 currentTrack,
1632 LANES_TRACK_GROUP,
1633 abortedViewTransition ? 'error' : 'secondary-light',
1634 ),
1635 );
1636 } else {
1637 console.timeStamp(
1638 abortedViewTransition
1639 ? 'Interrupted View Transition'
1640 : 'Starting Animation',
1641 startTime,
1642 endTime,
1643 currentTrack,
1644 LANES_TRACK_GROUP,
1645 abortedViewTransition ? ' error' : 'secondary-light',
1646 );
1647 }
1648 }
1649 }
1650
1651 export function logAnimatingPhase(
1652 startTime: number,
1653 endTime: number,
1654 debugTask: null | ConsoleTask,
1655 ): void {
1656 if (supportsUserTiming) {
1657 if (endTime <= startTime) {
1658 return;
1659 }
1660 if (__DEV__ && debugTask) {
1661 debugTask.run(
1662 // $FlowFixMe[method-unbinding]
1663 console.timeStamp.bind(
1664 console,
1665 'Animating',
1666 startTime,
1667 endTime,
1668 currentTrack,
1669 LANES_TRACK_GROUP,
1670 'secondary-dark',
1671 ),
1672 );
1673 } else {
1674 console.timeStamp(
1675 'Animating',
1676 startTime,
1677 endTime,
1678 currentTrack,
1679 LANES_TRACK_GROUP,
1680 'secondary-dark',
1681 );
1682 }
1683 }
1684 }
1685
1686 export function logPassiveCommitPhase(
1687 startTime: number,
1688 endTime: number,
1689 errors: null | Array<CapturedValue<mixed>>,
1690 debugTask: null | ConsoleTask,
1691 ): void {
1692 if (errors !== null) {
1693 logCommitErrored(startTime, endTime, errors, true, debugTask);
1694 return;
1695 }
1696 if (supportsUserTiming) {
1697 if (endTime <= startTime) {
1698 return;
1699 }
1700 if (__DEV__ && debugTask) {
1701 debugTask.run(
1702 // $FlowFixMe[method-unbinding]
1703 console.timeStamp.bind(
1704 console,
1705 'Remaining Effects',
1706 startTime,
1707 endTime,
1708 currentTrack,
1709 LANES_TRACK_GROUP,
1710 'secondary-dark',
1711 ),
1712 );
1713 } else {
1714 console.timeStamp(
1715 'Remaining Effects',
1716 startTime,
1717 endTime,
1718 currentTrack,
1719 LANES_TRACK_GROUP,
1720 'secondary-dark',
1721 );
1722 }
1723 }
1724 }