main
js 1,432 lines 38.6 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 {FrontendBridge} from 'react-devtools-shared/src/bridge';
11 import type Store from 'react-devtools-shared/src/devtools/store';
12
13 import {getVersionedRenderImplementation} from './utils';
14
15 describe('ProfilingCache', () => {
16 let React;
17 let ReactDOM;
18 let ReactDOMClient;
19 let Scheduler;
20 let bridge: FrontendBridge;
21 let legacyRender;
22 let store: Store;
23 let utils;
24
25 beforeEach(() => {
26 utils = require('./utils');
27 utils.beforeEachProfiling();
28
29 legacyRender = utils.legacyRender;
30
31 bridge = global.bridge;
32 store = global.store;
33 store.collapseNodesByDefault = false;
34 store.recordChangeDescriptions = true;
35
36 React = require('react');
37 ReactDOM = require('react-dom');
38 ReactDOMClient = require('react-dom/client');
39 Scheduler = require('scheduler');
40 });
41
42 const {render, getContainer} = getVersionedRenderImplementation();
43
44 // @reactVersion >= 16.9
45 // @reactVersion <= 18.2
46 it('should collect data for each root (including ones added or mounted after profiling started) (legacy render)', () => {
47 const Parent = ({count}) => {
48 Scheduler.unstable_advanceTime(10);
49 const children = new Array(count)
50 .fill(true)
51 .map((_, index) => <Child key={index} duration={index} />);
52 return (
53 <React.Fragment>
54 {children}
55 <MemoizedChild duration={1} />
56 </React.Fragment>
57 );
58 };
59 const Child = ({duration}) => {
60 Scheduler.unstable_advanceTime(duration);
61 return null;
62 };
63 const MemoizedChild = React.memo(Child);
64
65 const RootA = ({children}) => children;
66 const RootB = ({children}) => children;
67 const RootC = ({children}) => children;
68
69 const containerA = document.createElement('div');
70 const containerB = document.createElement('div');
71 const containerC = document.createElement('div');
72
73 utils.act(() =>
74 legacyRender(
75 <RootA>
76 <Parent count={2} />
77 </RootA>,
78 containerA,
79 ),
80 );
81 utils.act(() =>
82 legacyRender(
83 <RootB>
84 <Parent count={1} />
85 </RootB>,
86 containerB,
87 ),
88 );
89 utils.act(() => store.profilerStore.startProfiling());
90 utils.act(() =>
91 legacyRender(
92 <RootA>
93 <Parent count={3} />
94 </RootA>,
95 containerA,
96 ),
97 );
98 utils.act(() =>
99 legacyRender(
100 <RootC>
101 <Parent count={1} />
102 </RootC>,
103 containerC,
104 ),
105 );
106 utils.act(() =>
107 legacyRender(
108 <RootA>
109 <Parent count={1} />
110 </RootA>,
111 containerA,
112 ),
113 );
114 utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
115 utils.act(() =>
116 legacyRender(
117 <RootA>
118 <Parent count={0} />
119 </RootA>,
120 containerA,
121 ),
122 );
123 utils.act(() => store.profilerStore.stopProfiling());
124 utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
125
126 const rootIDs = Array.from(
127 store.profilerStore.profilingData.dataForRoots.values(),
128 ).map(({rootID}) => rootID);
129 expect(rootIDs).toHaveLength(3);
130
131 const originalProfilingDataForRoot = [];
132
133 let data = store.profilerStore.getDataForRoot(rootIDs[0]);
134 expect(data.displayName).toMatchInlineSnapshot(`"RootA"`);
135 expect(data.commitData).toHaveLength(3);
136 originalProfilingDataForRoot.push(data);
137
138 data = store.profilerStore.getDataForRoot(rootIDs[1]);
139 expect(data.displayName).toMatchInlineSnapshot(`"RootC"`);
140 expect(data.commitData).toHaveLength(1);
141 originalProfilingDataForRoot.push(data);
142
143 data = store.profilerStore.getDataForRoot(rootIDs[2]);
144 expect(data.displayName).toMatchInlineSnapshot(`"RootB"`);
145 expect(data.commitData).toHaveLength(1);
146 originalProfilingDataForRoot.push(data);
147
148 utils.exportImportHelper(bridge, store);
149
150 rootIDs.forEach((rootID, index) => {
151 const current = store.profilerStore.getDataForRoot(rootID);
152 const prev = originalProfilingDataForRoot[index];
153 expect(current).toEqual(prev);
154 });
155 });
156
157 // @reactVersion >= 18
158 it('should collect data for each root (including ones added or mounted after profiling started) (createRoot)', () => {
159 const Parent = ({count}) => {
160 Scheduler.unstable_advanceTime(10);
161 const children = new Array(count)
162 .fill(true)
163 .map((_, index) => <Child key={index} duration={index} />);
164 return (
165 <React.Fragment>
166 {children}
167 <MemoizedChild duration={1} />
168 </React.Fragment>
169 );
170 };
171 const Child = ({duration}) => {
172 Scheduler.unstable_advanceTime(duration);
173 return null;
174 };
175 const MemoizedChild = React.memo(Child);
176
177 const RootA = ({children}) => children;
178 const RootB = ({children}) => children;
179 const RootC = ({children}) => children;
180
181 const containerA = document.createElement('div');
182 const containerB = document.createElement('div');
183 const containerC = document.createElement('div');
184
185 const rootA = ReactDOMClient.createRoot(containerA);
186 const rootB = ReactDOMClient.createRoot(containerB);
187 const rootC = ReactDOMClient.createRoot(containerC);
188
189 utils.act(() =>
190 rootA.render(
191 <RootA>
192 <Parent count={2} />
193 </RootA>,
194 ),
195 );
196 utils.act(() =>
197 rootB.render(
198 <RootB>
199 <Parent count={1} />
200 </RootB>,
201 ),
202 );
203 utils.act(() => store.profilerStore.startProfiling());
204 utils.act(() =>
205 rootA.render(
206 <RootA>
207 <Parent count={3} />
208 </RootA>,
209 ),
210 );
211 utils.act(() =>
212 rootC.render(
213 <RootC>
214 <Parent count={1} />
215 </RootC>,
216 ),
217 );
218 utils.act(() =>
219 rootA.render(
220 <RootA>
221 <Parent count={1} />
222 </RootA>,
223 ),
224 );
225 utils.act(() => rootB.unmount());
226 utils.act(() =>
227 rootA.render(
228 <RootA>
229 <Parent count={0} />
230 </RootA>,
231 ),
232 );
233 utils.act(() => store.profilerStore.stopProfiling());
234 utils.act(() => rootA.unmount());
235
236 const rootIDs = Array.from(
237 store.profilerStore.profilingData.dataForRoots.values(),
238 ).map(({rootID}) => rootID);
239 expect(rootIDs).toHaveLength(3);
240
241 const originalProfilingDataForRoot = [];
242
243 let data = store.profilerStore.getDataForRoot(rootIDs[0]);
244 expect(data.displayName).toMatchInlineSnapshot(`"RootA"`);
245 expect(data.commitData).toHaveLength(3);
246 originalProfilingDataForRoot.push(data);
247
248 data = store.profilerStore.getDataForRoot(rootIDs[1]);
249 expect(data.displayName).toMatchInlineSnapshot(`"RootC"`);
250 expect(data.commitData).toHaveLength(1);
251 originalProfilingDataForRoot.push(data);
252
253 data = store.profilerStore.getDataForRoot(rootIDs[2]);
254 expect(data.displayName).toMatchInlineSnapshot(`"RootB"`);
255 expect(data.commitData).toHaveLength(1);
256 originalProfilingDataForRoot.push(data);
257
258 utils.exportImportHelper(bridge, store);
259
260 rootIDs.forEach((rootID, index) => {
261 const current = store.profilerStore.getDataForRoot(rootID);
262 const prev = originalProfilingDataForRoot[index];
263 expect(current).toEqual(prev);
264 });
265 });
266
267 // @reactVersion >= 16.9
268 it('should collect data for each commit', () => {
269 const Parent = ({count}) => {
270 Scheduler.unstable_advanceTime(10);
271 const children = new Array(count)
272 .fill(true)
273 .map((_, index) => <Child key={index} duration={index} />);
274 return (
275 <React.Fragment>
276 {children}
277 <MemoizedChild duration={1} />
278 </React.Fragment>
279 );
280 };
281 const Child = ({duration}) => {
282 Scheduler.unstable_advanceTime(duration);
283 return null;
284 };
285 const MemoizedChild = React.memo(Child);
286
287 utils.act(() => store.profilerStore.startProfiling());
288 utils.act(() => render(<Parent count={2} />));
289 utils.act(() => render(<Parent count={3} />));
290 utils.act(() => render(<Parent count={1} />));
291 utils.act(() => render(<Parent count={0} />));
292 utils.act(() => store.profilerStore.stopProfiling());
293
294 const rootID = store.roots[0];
295
296 const prevCommitData =
297 store.profilerStore.getDataForRoot(rootID).commitData;
298 expect(prevCommitData).toHaveLength(4);
299
300 utils.exportImportHelper(bridge, store);
301
302 const nextCommitData =
303 store.profilerStore.getDataForRoot(rootID).commitData;
304 expect(nextCommitData).toHaveLength(4);
305 nextCommitData.forEach((commitData, index) => {
306 expect(commitData).toEqual(prevCommitData[index]);
307 });
308 });
309
310 // @reactVersion >= 18.0
311 it('should properly detect changed hooks', () => {
312 const Context = React.createContext(0);
313
314 function reducer(state, action) {
315 switch (action.type) {
316 case 'invert':
317 return {value: !state.value};
318 default:
319 throw new Error();
320 }
321 }
322
323 let snapshot = 0;
324 function getServerSnapshot() {
325 return snapshot;
326 }
327 function getClientSnapshot() {
328 return snapshot;
329 }
330
331 let syncExternalStoreCallback;
332 function subscribe(callback) {
333 syncExternalStoreCallback = callback;
334 }
335
336 let dispatch = null;
337 let setState = null;
338
339 const Component = ({count, string}) => {
340 // These hooks may change and initiate re-renders.
341 setState = React.useState('abc')[1];
342 dispatch = React.useReducer(reducer, {value: true})[1];
343 React.useSyncExternalStore(
344 subscribe,
345 getClientSnapshot,
346 getServerSnapshot,
347 );
348
349 // This hook's return value may change between renders,
350 // but the hook itself isn't stateful.
351 React.useContext(Context);
352
353 // These hooks never change in a way that schedules an update.
354 React.useCallback(() => () => {}, [string]);
355 React.useMemo(() => string, [string]);
356 React.useCallback(() => () => {}, [count]);
357 React.useMemo(() => count, [count]);
358 React.useCallback(() => () => {});
359 React.useMemo(() => string);
360
361 // These hooks never change in a way that schedules an update.
362 React.useEffect(() => {}, [string]);
363 React.useLayoutEffect(() => {}, [string]);
364 React.useEffect(() => {}, [count]);
365 React.useLayoutEffect(() => {}, [count]);
366 React.useEffect(() => {});
367 React.useLayoutEffect(() => {});
368
369 return null;
370 };
371
372 utils.act(() => store.profilerStore.startProfiling());
373 utils.act(() =>
374 render(
375 <Context.Provider value={true}>
376 <Component count={1} />
377 </Context.Provider>,
378 ),
379 );
380
381 // Save references to the real dispatch/setState functions.
382 // inspectHooks() re-runs the component with a mock dispatcher,
383 // which would overwrite these variables with mock functions that do nothing.
384 const realDispatch = dispatch;
385 const realSetState = setState;
386
387 // Second render has no changed hooks, only changed props.
388 utils.act(() =>
389 render(
390 <Context.Provider value={true}>
391 <Component count={2} />
392 </Context.Provider>,
393 ),
394 );
395
396 // Third render has a changed reducer hook.
397 utils.act(() => realDispatch({type: 'invert'}));
398
399 // Fourth render has a changed state hook.
400 utils.act(() => realSetState('def'));
401
402 // Fifth render has a changed context value, but no changed hook.
403 utils.act(() =>
404 render(
405 <Context.Provider value={false}>
406 <Component count={2} />
407 </Context.Provider>,
408 ),
409 );
410
411 // 6th renderer is triggered by a sync external store change.
412 utils.act(() => {
413 snapshot++;
414 syncExternalStoreCallback();
415 });
416
417 utils.act(() => store.profilerStore.stopProfiling());
418
419 const rootID = store.roots[0];
420
421 const changeDescriptions = store.profilerStore
422 .getDataForRoot(rootID)
423 .commitData.map(commitData => commitData.changeDescriptions);
424 expect(changeDescriptions).toHaveLength(6);
425
426 // 1st render: No change
427 expect(changeDescriptions[0]).toMatchInlineSnapshot(`
428 Map {
429 3 => {
430 "context": null,
431 "didHooksChange": false,
432 "isFirstMount": true,
433 "props": null,
434 "state": null,
435 },
436 }
437 `);
438
439 // 2nd render: Changed props
440 expect(changeDescriptions[1]).toMatchInlineSnapshot(`
441 Map {
442 3 => {
443 "context": false,
444 "didHooksChange": false,
445 "hooks": [],
446 "isFirstMount": false,
447 "props": [
448 "count",
449 ],
450 "state": null,
451 },
452 }
453 `);
454
455 // 3rd render: Changed useReducer
456 expect(changeDescriptions[2]).toMatchInlineSnapshot(`
457 Map {
458 3 => {
459 "context": false,
460 "didHooksChange": true,
461 "hooks": [
462 1,
463 ],
464 "isFirstMount": false,
465 "props": [],
466 "state": null,
467 },
468 }
469 `);
470
471 // 4th render: Changed useState
472 expect(changeDescriptions[3]).toMatchInlineSnapshot(`
473 Map {
474 3 => {
475 "context": false,
476 "didHooksChange": true,
477 "hooks": [
478 0,
479 ],
480 "isFirstMount": false,
481 "props": [],
482 "state": null,
483 },
484 }
485 `);
486
487 // 5th render: Changed context
488 expect(changeDescriptions[4]).toMatchInlineSnapshot(`
489 Map {
490 3 => {
491 "context": true,
492 "didHooksChange": false,
493 "hooks": [],
494 "isFirstMount": false,
495 "props": [],
496 "state": null,
497 },
498 }
499 `);
500
501 // 6th render: Sync external store
502 expect(changeDescriptions[5]).toMatchInlineSnapshot(`
503 Map {
504 3 => {
505 "context": false,
506 "didHooksChange": true,
507 "hooks": [
508 2,
509 ],
510 "isFirstMount": false,
511 "props": [],
512 "state": null,
513 },
514 }
515 `);
516
517 expect(changeDescriptions).toHaveLength(6);
518
519 // Export and re-import profile data and make sure it is retained.
520 utils.exportImportHelper(bridge, store);
521
522 for (let commitIndex = 0; commitIndex < 6; commitIndex++) {
523 const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
524 expect(commitData.changeDescriptions).toEqual(
525 changeDescriptions[commitIndex],
526 );
527 }
528 });
529
530 // @reactVersion >= 19.0
531 it('should detect what hooks changed in a render with custom and composite hooks', () => {
532 let snapshot = 0;
533 let syncExternalStoreCallback;
534
535 function subscribe(callback) {
536 syncExternalStoreCallback = callback;
537 return () => {};
538 }
539
540 function getSnapshot() {
541 return snapshot;
542 }
543
544 // Custom hook wrapping multiple primitive hooks
545 function useCustomHook() {
546 const [value, setValue] = React.useState('custom');
547 React.useEffect(() => {}, [value]);
548 return [value, setValue];
549 }
550
551 let setState = null;
552 let startTransition = null;
553 let actionStateDispatch = null;
554 let setCustomValue = null;
555 let setFinalState = null;
556
557 const Component = () => {
558 // Hook 0: useState
559 const [state, _setState] = React.useState('initial');
560 setState = _setState;
561
562 // Hook 1: useSyncExternalStore (composite hook - internally uses multiple hooks)
563 const storeValue = React.useSyncExternalStore(
564 subscribe,
565 getSnapshot,
566 getSnapshot,
567 );
568
569 // Hook 2: useTransition (composite hook - internally uses multiple hooks)
570 const [isPending, _startTransition] = React.useTransition();
571 startTransition = _startTransition;
572
573 // Hook 3: useActionState (composite hook - internally uses multiple hooks)
574 const [actionState, _actionStateDispatch] = React.useActionState(
575 (_prev, action) => action,
576 'action-initial',
577 );
578 actionStateDispatch = _actionStateDispatch;
579
580 // Hook 4: useState inside custom hook (flattened)
581 // Hook 5: useEffect inside custom hook (not stateful, won't show in changes)
582 const [customValue, _setCustomValue] = useCustomHook();
583 setCustomValue = _setCustomValue;
584
585 // Hook 6: direct useState at the end
586 const [finalState, _setFinalState] = React.useState('final');
587 setFinalState = _setFinalState;
588
589 return `${state}-${storeValue}-${isPending}-${actionState}-${customValue}-${finalState}`;
590 };
591
592 utils.act(() => store.profilerStore.startProfiling());
593 utils.act(() => render(<Component />));
594
595 // Save references before inspectHooks() overwrites them
596 const realSetState = setState;
597 const realStartTransition = startTransition;
598 const realActionStateDispatch = actionStateDispatch;
599 const realSetCustomValue = setCustomValue;
600 const realSetFinalState = setFinalState;
601
602 // 2nd render: change useState (hook 0)
603 utils.act(() => realSetState('changed'));
604
605 // 3rd render: change useSyncExternalStore (hook 1)
606 utils.act(() => {
607 snapshot = 1;
608 syncExternalStoreCallback();
609 });
610
611 // 4th render: trigger useTransition (hook 2)
612 // Note: useTransition triggers two renders - one when isPending becomes true,
613 // and another when isPending becomes false after the transition completes
614 utils.act(() => {
615 realStartTransition(() => {});
616 });
617
618 // 6th render: change useActionState (hook 3)
619 utils.act(() => realActionStateDispatch('action-changed'));
620
621 // 7th render: change custom hook's useState (hook 4)
622 utils.act(() => realSetCustomValue('custom-changed'));
623
624 // 8th render: change final useState (hook 6)
625 utils.act(() => realSetFinalState('final-changed'));
626
627 utils.act(() => store.profilerStore.stopProfiling());
628
629 const rootID = store.roots[0];
630
631 const changeDescriptions = store.profilerStore
632 .getDataForRoot(rootID)
633 .commitData.map(commitData => commitData.changeDescriptions);
634 expect(changeDescriptions).toHaveLength(8);
635
636 // 1st render: Initial mount
637 expect(changeDescriptions[0]).toMatchInlineSnapshot(`
638 Map {
639 2 => {
640 "context": null,
641 "didHooksChange": false,
642 "isFirstMount": true,
643 "props": null,
644 "state": null,
645 },
646 }
647 `);
648
649 // 2nd render: Changed hook 0 (useState)
650 expect(changeDescriptions[1]).toMatchInlineSnapshot(`
651 Map {
652 2 => {
653 "context": false,
654 "didHooksChange": true,
655 "hooks": [
656 0,
657 ],
658 "isFirstMount": false,
659 "props": [],
660 "state": null,
661 },
662 }
663 `);
664
665 // 3rd render: Changed hook 1 (useSyncExternalStore)
666 expect(changeDescriptions[2]).toMatchInlineSnapshot(`
667 Map {
668 2 => {
669 "context": false,
670 "didHooksChange": true,
671 "hooks": [
672 1,
673 ],
674 "isFirstMount": false,
675 "props": [],
676 "state": null,
677 },
678 }
679 `);
680
681 // 4th render: Changed hook 2 (useTransition - isPending becomes true)
682 expect(changeDescriptions[3]).toMatchInlineSnapshot(`
683 Map {
684 2 => {
685 "context": false,
686 "didHooksChange": true,
687 "hooks": [
688 2,
689 ],
690 "isFirstMount": false,
691 "props": [],
692 "state": null,
693 },
694 }
695 `);
696
697 // 5th render: Changed hook 2 (useTransition - isPending becomes false)
698 expect(changeDescriptions[4]).toMatchInlineSnapshot(`
699 Map {
700 2 => {
701 "context": false,
702 "didHooksChange": true,
703 "hooks": [
704 2,
705 ],
706 "isFirstMount": false,
707 "props": [],
708 "state": null,
709 },
710 }
711 `);
712
713 // 6th render: Changed hook 3 (useActionState)
714 expect(changeDescriptions[5]).toMatchInlineSnapshot(`
715 Map {
716 2 => {
717 "context": false,
718 "didHooksChange": true,
719 "hooks": [
720 3,
721 ],
722 "isFirstMount": false,
723 "props": [],
724 "state": null,
725 },
726 }
727 `);
728
729 // 7th render: Changed hook 4 (useState inside useCustomHook)
730 expect(changeDescriptions[6]).toMatchInlineSnapshot(`
731 Map {
732 2 => {
733 "context": false,
734 "didHooksChange": true,
735 "hooks": [
736 4,
737 ],
738 "isFirstMount": false,
739 "props": [],
740 "state": null,
741 },
742 }
743 `);
744
745 // 8th render: Changed hook 6 (final useState)
746 expect(changeDescriptions[7]).toMatchInlineSnapshot(`
747 Map {
748 2 => {
749 "context": false,
750 "didHooksChange": true,
751 "hooks": [
752 6,
753 ],
754 "isFirstMount": false,
755 "props": [],
756 "state": null,
757 },
758 }
759 `);
760 });
761
762 // @reactVersion >= 19.0
763 it('should detect context changes or lack of changes with conditional use()', () => {
764 const ContextA = React.createContext(0);
765 const ContextB = React.createContext(1);
766 let setState = null;
767
768 const Component = () => {
769 // These hooks may change and initiate re-renders.
770 let state;
771 [state, setState] = React.useState('abc');
772
773 let result = state;
774
775 if (state.includes('a')) {
776 result += React.use(ContextA);
777 }
778
779 result += React.use(ContextB);
780
781 return result;
782 };
783
784 utils.act(() =>
785 render(
786 <ContextA.Provider value={1}>
787 <ContextB.Provider value={1}>
788 <Component />
789 </ContextB.Provider>
790 </ContextA.Provider>,
791 ),
792 );
793
794 // Save reference to the real setState function before profiling starts.
795 // inspectHooks() re-runs the component with a mock dispatcher,
796 // which would overwrite setState with a mock function that does nothing.
797 const realSetState = setState;
798
799 utils.act(() => store.profilerStore.startProfiling());
800
801 // First render changes Context.
802 utils.act(() =>
803 render(
804 <ContextA.Provider value={0}>
805 <ContextB.Provider value={1}>
806 <Component />
807 </ContextB.Provider>
808 </ContextA.Provider>,
809 ),
810 );
811
812 // Second render has no changed Context, only changed state.
813 utils.act(() => realSetState('def'));
814
815 utils.act(() => store.profilerStore.stopProfiling());
816
817 const rootID = store.roots[0];
818
819 const changeDescriptions = store.profilerStore
820 .getDataForRoot(rootID)
821 .commitData.map(commitData => commitData.changeDescriptions);
822 expect(changeDescriptions).toHaveLength(2);
823
824 // 1st render: Change to Context
825 expect(changeDescriptions[0]).toMatchInlineSnapshot(`
826 Map {
827 4 => {
828 "context": true,
829 "didHooksChange": false,
830 "hooks": [],
831 "isFirstMount": false,
832 "props": [],
833 "state": null,
834 },
835 }
836 `);
837
838 // 2nd render: Change to State
839 expect(changeDescriptions[1]).toMatchInlineSnapshot(`
840 Map {
841 4 => {
842 "context": false,
843 "didHooksChange": true,
844 "hooks": [
845 0,
846 ],
847 "isFirstMount": false,
848 "props": [],
849 "state": null,
850 },
851 }
852 `);
853
854 expect(changeDescriptions).toHaveLength(2);
855
856 // Export and re-import profile data and make sure it is retained.
857 utils.exportImportHelper(bridge, store);
858
859 for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
860 const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
861 expect(commitData.changeDescriptions).toEqual(
862 changeDescriptions[commitIndex],
863 );
864 }
865 });
866
867 // @reactVersion >= 18.0
868 it('should calculate durations based on actual children (not filtered children)', () => {
869 store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
870
871 const Grandparent = () => {
872 Scheduler.unstable_advanceTime(10);
873 return (
874 <React.Fragment>
875 <Parent key="one" />
876 <Parent key="two" />
877 </React.Fragment>
878 );
879 };
880 const Parent = () => {
881 Scheduler.unstable_advanceTime(2);
882 return <Child />;
883 };
884 const Child = () => {
885 Scheduler.unstable_advanceTime(1);
886 return null;
887 };
888
889 utils.act(() => store.profilerStore.startProfiling());
890 utils.act(() => render(<Grandparent />));
891 utils.act(() => store.profilerStore.stopProfiling());
892
893 expect(store).toMatchInlineSnapshot(`
894 [root]
895 ▾ <Grandparent>
896 <Child>
897 <Child>
898 `);
899
900 const rootID = store.roots[0];
901 const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
902 expect(commitData).toHaveLength(1);
903
904 // Actual duration should also include both filtered <Parent> components.
905 expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
906 Map {
907 1 => 16,
908 2 => 16,
909 3 => 1,
910 4 => 1,
911 }
912 `);
913
914 expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
915 Map {
916 1 => 0,
917 2 => 10,
918 3 => 1,
919 4 => 1,
920 }
921 `);
922 });
923
924 // @reactVersion >= 17.0
925 it('should calculate durations correctly for suspended views', async () => {
926 let data;
927 const getData = () => {
928 if (React.use) {
929 if (!data) {
930 data = new Promise(resolve => {
931 resolve('abc');
932 });
933 }
934 return React.use(data);
935 }
936 if (data) {
937 return data;
938 } else {
939 throw new Promise(resolve => {
940 data = 'abc';
941 resolve(data);
942 });
943 }
944 };
945
946 const Parent = () => {
947 Scheduler.unstable_advanceTime(10);
948 return (
949 <React.Suspense fallback={<Fallback />}>
950 <Async />
951 </React.Suspense>
952 );
953 };
954 const Fallback = () => {
955 Scheduler.unstable_advanceTime(2);
956 return 'Fallback...';
957 };
958 const Async = () => {
959 Scheduler.unstable_advanceTime(3);
960 return getData();
961 };
962
963 utils.act(() => store.profilerStore.startProfiling());
964 await utils.actAsync(() => render(<Parent />));
965 utils.act(() => store.profilerStore.stopProfiling());
966
967 const rootID = store.roots[0];
968 const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
969 expect(commitData).toHaveLength(2);
970
971 if (React.version.startsWith('17')) {
972 // React 17 will mount all children until it suspends in a LegacyHidden
973 // The ID gap is from the Fiber for <Async> that's in the disconnected tree.
974 expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
975 Map {
976 1 => 15,
977 2 => 15,
978 3 => 5,
979 5 => 2,
980 }
981 `);
982 expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
983 Map {
984 1 => 0,
985 2 => 10,
986 3 => 3,
987 5 => 2,
988 }
989 `);
990 expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
991 Map {
992 6 => 3,
993 3 => 3,
994 }
995 `);
996 expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
997 Map {
998 6 => 3,
999 3 => 0,
1000 }
1001 `);
1002 } else {
1003 expect(commitData[0].fiberActualDurations).toMatchInlineSnapshot(`
1004 Map {
1005 1 => 15,
1006 2 => 15,
1007 3 => 5,
1008 4 => 2,
1009 }
1010 `);
1011 expect(commitData[0].fiberSelfDurations).toMatchInlineSnapshot(`
1012 Map {
1013 1 => 0,
1014 2 => 10,
1015 3 => 3,
1016 4 => 2,
1017 }
1018 `);
1019 expect(commitData[1].fiberActualDurations).toMatchInlineSnapshot(`
1020 Map {
1021 5 => 3,
1022 3 => 3,
1023 }
1024 `);
1025 expect(commitData[1].fiberSelfDurations).toMatchInlineSnapshot(`
1026 Map {
1027 5 => 3,
1028 3 => 0,
1029 }
1030 `);
1031 }
1032 });
1033
1034 // @reactVersion >= 16.9
1035 it('should collect data for each rendered fiber', () => {
1036 const Parent = ({count}) => {
1037 Scheduler.unstable_advanceTime(10);
1038 const children = new Array(count)
1039 .fill(true)
1040 .map((_, index) => <Child key={index} duration={index} />);
1041 return (
1042 <React.Fragment>
1043 {children}
1044 <MemoizedChild duration={1} />
1045 </React.Fragment>
1046 );
1047 };
1048 const Child = ({duration}) => {
1049 Scheduler.unstable_advanceTime(duration);
1050 return null;
1051 };
1052 const MemoizedChild = React.memo(Child);
1053
1054 utils.act(() => store.profilerStore.startProfiling());
1055 utils.act(() => render(<Parent count={1} />));
1056 utils.act(() => render(<Parent count={2} />));
1057 utils.act(() => render(<Parent count={3} />));
1058 utils.act(() => store.profilerStore.stopProfiling());
1059
1060 const rootID = store.roots[0];
1061 const allFiberCommits = [];
1062 for (let index = 0; index < store.numElements; index++) {
1063 const fiberID = store.getElementIDAtIndex(index);
1064 const fiberCommits = store.profilerStore.profilingCache.getFiberCommits({
1065 fiberID,
1066 rootID,
1067 });
1068
1069 allFiberCommits.push(fiberCommits);
1070 }
1071
1072 expect(allFiberCommits).toMatchInlineSnapshot(`
1073 [
1074 [
1075 0,
1076 1,
1077 2,
1078 ],
1079 [
1080 0,
1081 1,
1082 2,
1083 ],
1084 [
1085 1,
1086 2,
1087 ],
1088 [
1089 2,
1090 ],
1091 [
1092 0,
1093 ],
1094 ]
1095 `);
1096
1097 utils.exportImportHelper(bridge, store);
1098
1099 for (let index = 0; index < store.numElements; index++) {
1100 const fiberID = store.getElementIDAtIndex(index);
1101 const fiberCommits = store.profilerStore.profilingCache.getFiberCommits({
1102 fiberID,
1103 rootID,
1104 });
1105
1106 expect(fiberCommits).toEqual(allFiberCommits[index]);
1107 }
1108 });
1109
1110 // @reactVersion >= 18.0.0
1111 // @reactVersion <= 18.2.0
1112 it('should handle unexpectedly shallow suspense trees for react v[18.0.0 - 18.2.0] (legacy render)', () => {
1113 utils.act(() => store.profilerStore.startProfiling());
1114 utils.act(() =>
1115 legacyRender(<React.Suspense />, document.createElement('div')),
1116 );
1117 utils.act(() => store.profilerStore.stopProfiling());
1118
1119 const rootID = store.roots[0];
1120 const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
1121 expect(commitData).toMatchInlineSnapshot(`
1122 [
1123 {
1124 "changeDescriptions": Map {},
1125 "duration": 0,
1126 "effectDuration": null,
1127 "fiberActualDurations": Map {
1128 1 => 0,
1129 2 => 0,
1130 },
1131 "fiberSelfDurations": Map {
1132 1 => 0,
1133 2 => 0,
1134 },
1135 "passiveEffectDuration": null,
1136 "priorityLevel": "Immediate",
1137 "timestamp": 0,
1138 "updaters": [
1139 {
1140 "compiledWithForget": false,
1141 "displayName": "render()",
1142 "env": null,
1143 "hocDisplayNames": null,
1144 "id": 1,
1145 "key": null,
1146 "stack": null,
1147 "type": 11,
1148 },
1149 ],
1150 },
1151 ]
1152 `);
1153 });
1154
1155 // @reactVersion >= 18.0.0
1156 // @reactVersion <= 18.2.0
1157 it('should handle unexpectedly shallow suspense trees for react v[18.0.0 - 18.2.0] (createRoot)', () => {
1158 utils.act(() => store.profilerStore.startProfiling());
1159 utils.act(() => render(<React.Suspense />));
1160 utils.act(() => store.profilerStore.stopProfiling());
1161
1162 const rootID = store.roots[0];
1163 const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
1164 expect(commitData).toMatchInlineSnapshot(`
1165 [
1166 {
1167 "changeDescriptions": Map {},
1168 "duration": 0,
1169 "effectDuration": null,
1170 "fiberActualDurations": Map {
1171 1 => 0,
1172 2 => 0,
1173 },
1174 "fiberSelfDurations": Map {
1175 1 => 0,
1176 2 => 0,
1177 },
1178 "passiveEffectDuration": null,
1179 "priorityLevel": "Normal",
1180 "timestamp": 0,
1181 "updaters": [
1182 {
1183 "compiledWithForget": false,
1184 "displayName": "createRoot()",
1185 "env": null,
1186 "hocDisplayNames": null,
1187 "id": 1,
1188 "key": null,
1189 "stack": null,
1190 "type": 11,
1191 },
1192 ],
1193 },
1194 ]
1195 `);
1196 });
1197
1198 // @reactVersion > 18.2.0
1199 it('should handle unexpectedly shallow suspense trees', () => {
1200 utils.act(() => store.profilerStore.startProfiling());
1201 utils.act(() => render(<React.Suspense />));
1202 utils.act(() => store.profilerStore.stopProfiling());
1203
1204 const rootID = store.roots[0];
1205 const commitData = store.profilerStore.getDataForRoot(rootID).commitData;
1206 expect(commitData).toMatchInlineSnapshot(`
1207 [
1208 {
1209 "changeDescriptions": Map {},
1210 "duration": 0,
1211 "effectDuration": null,
1212 "fiberActualDurations": Map {
1213 1 => 0,
1214 2 => 0,
1215 },
1216 "fiberSelfDurations": Map {
1217 1 => 0,
1218 2 => 0,
1219 },
1220 "passiveEffectDuration": null,
1221 "priorityLevel": "Normal",
1222 "timestamp": 0,
1223 "updaters": [
1224 {
1225 "compiledWithForget": false,
1226 "displayName": "createRoot()",
1227 "env": null,
1228 "hocDisplayNames": null,
1229 "id": 1,
1230 "key": null,
1231 "stack": null,
1232 "type": 11,
1233 },
1234 ],
1235 },
1236 ]
1237 `);
1238 });
1239
1240 // See https://github.com/facebook/react/issues/18831
1241 // @reactVersion >= 16.9
1242 it('should not crash during route transitions with Suspense', () => {
1243 const RouterContext = React.createContext();
1244
1245 function App() {
1246 return (
1247 <Router>
1248 <Switch>
1249 <Route path="/">
1250 <Home />
1251 </Route>
1252 <Route path="/about">
1253 <About />
1254 </Route>
1255 </Switch>
1256 </Router>
1257 );
1258 }
1259
1260 const Home = () => {
1261 return (
1262 <React.Suspense>
1263 <Link path="/about">Home</Link>
1264 </React.Suspense>
1265 );
1266 };
1267
1268 const About = () => <div>About</div>;
1269
1270 // Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Router.js
1271 function Router({children}) {
1272 const [path, setPath] = React.useState('/');
1273 return (
1274 <RouterContext.Provider value={{path, setPath}}>
1275 {children}
1276 </RouterContext.Provider>
1277 );
1278 }
1279
1280 // Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Switch.js
1281 function Switch({children}) {
1282 return (
1283 <RouterContext.Consumer>
1284 {context => {
1285 let element = null;
1286 React.Children.forEach(children, child => {
1287 if (context.path === child.props.path) {
1288 element = child.props.children;
1289 }
1290 });
1291 return element ? React.cloneElement(element) : null;
1292 }}
1293 </RouterContext.Consumer>
1294 );
1295 }
1296
1297 // Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js
1298 function Route({children, path}) {
1299 return null;
1300 }
1301
1302 const linkRef = React.createRef();
1303
1304 // Mimics https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js
1305 function Link({children, path}) {
1306 return (
1307 <RouterContext.Consumer>
1308 {context => {
1309 return (
1310 <button ref={linkRef} onClick={() => context.setPath(path)}>
1311 {children}
1312 </button>
1313 );
1314 }}
1315 </RouterContext.Consumer>
1316 );
1317 }
1318
1319 utils.act(() => render(<App />));
1320 expect(getContainer().textContent).toBe('Home');
1321 utils.act(() => store.profilerStore.startProfiling());
1322 utils.act(() =>
1323 linkRef.current.dispatchEvent(
1324 new MouseEvent('click', {bubbles: true, cancelable: true}),
1325 ),
1326 );
1327 utils.act(() => store.profilerStore.stopProfiling());
1328 expect(getContainer().textContent).toBe('About');
1329 });
1330
1331 // @reactVersion >= 18.0
1332 it('components that were deleted and added to updaters during the layout phase should not crash', () => {
1333 let setChildUnmounted;
1334 function Child() {
1335 const [, setState] = React.useState(false);
1336
1337 React.useLayoutEffect(() => {
1338 return () => setState(true);
1339 });
1340
1341 return null;
1342 }
1343
1344 function App() {
1345 const [childUnmounted, _setChildUnmounted] = React.useState(false);
1346 setChildUnmounted = _setChildUnmounted;
1347 return <>{!childUnmounted && <Child />}</>;
1348 }
1349
1350 const root = ReactDOMClient.createRoot(document.createElement('div'));
1351 utils.act(() => root.render(<App />));
1352 utils.act(() => store.profilerStore.startProfiling());
1353 utils.act(() => setChildUnmounted(true));
1354 utils.act(() => store.profilerStore.stopProfiling());
1355
1356 const updaters = store.profilerStore.getCommitData(
1357 store.roots[0],
1358 0,
1359 ).updaters;
1360 expect(updaters.length).toEqual(1);
1361 expect(updaters[0].displayName).toEqual('App');
1362 });
1363
1364 // @reactVersion >= 18.0
1365 it('components in a deleted subtree and added to updaters during the layout phase should not crash', () => {
1366 let setChildUnmounted;
1367 function Child() {
1368 return <GrandChild />;
1369 }
1370
1371 function GrandChild() {
1372 const [, setState] = React.useState(false);
1373
1374 React.useLayoutEffect(() => {
1375 return () => setState(true);
1376 });
1377
1378 return null;
1379 }
1380
1381 function App() {
1382 const [childUnmounted, _setChildUnmounted] = React.useState(false);
1383 setChildUnmounted = _setChildUnmounted;
1384 return <>{!childUnmounted && <Child />}</>;
1385 }
1386
1387 const root = ReactDOMClient.createRoot(document.createElement('div'));
1388 utils.act(() => root.render(<App />));
1389 utils.act(() => store.profilerStore.startProfiling());
1390 utils.act(() => setChildUnmounted(true));
1391 utils.act(() => store.profilerStore.stopProfiling());
1392
1393 const updaters = store.profilerStore.getCommitData(
1394 store.roots[0],
1395 0,
1396 ).updaters;
1397 expect(updaters.length).toEqual(1);
1398 expect(updaters[0].displayName).toEqual('App');
1399 });
1400
1401 // @reactVersion >= 18.0
1402 it('components that were deleted should not be added to updaters during the passive phase', () => {
1403 let setChildUnmounted;
1404 function Child() {
1405 const [, setState] = React.useState(false);
1406 React.useEffect(() => {
1407 return () => setState(true);
1408 });
1409
1410 return null;
1411 }
1412
1413 function App() {
1414 const [childUnmounted, _setChildUnmounted] = React.useState(false);
1415 setChildUnmounted = _setChildUnmounted;
1416 return <>{!childUnmounted && <Child />}</>;
1417 }
1418
1419 const root = ReactDOMClient.createRoot(document.createElement('div'));
1420 utils.act(() => root.render(<App />));
1421 utils.act(() => store.profilerStore.startProfiling());
1422 utils.act(() => setChildUnmounted(true));
1423 utils.act(() => store.profilerStore.stopProfiling());
1424
1425 const updaters = store.profilerStore.getCommitData(
1426 store.roots[0],
1427 0,
1428 ).updaters;
1429 expect(updaters.length).toEqual(1);
1430 expect(updaters[0].displayName).toEqual('App');
1431 });
1432 });