main
js 2,267 lines 66.5 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 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactFeatureFlags;
15 let ReactNoop;
16 let Scheduler;
17 let act;
18 let AdvanceTime;
19 let assertLog;
20 let waitFor;
21 let waitForAll;
22 let waitForThrow;
23
24 function loadModules({
25 enableProfilerTimer = true,
26 enableProfilerCommitHooks = true,
27 enableProfilerNestedUpdatePhase = true,
28 } = {}) {
29 ReactFeatureFlags = require('shared/ReactFeatureFlags');
30
31 ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
32 ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks;
33 ReactFeatureFlags.enableProfilerNestedUpdatePhase =
34 enableProfilerNestedUpdatePhase;
35
36 React = require('react');
37 Scheduler = require('scheduler');
38 ReactNoop = require('react-noop-renderer');
39 const InternalTestUtils = require('internal-test-utils');
40 act = InternalTestUtils.act;
41 assertLog = InternalTestUtils.assertLog;
42 waitFor = InternalTestUtils.waitFor;
43 waitForAll = InternalTestUtils.waitForAll;
44 waitForThrow = InternalTestUtils.waitForThrow;
45
46 AdvanceTime = class extends React.Component {
47 static defaultProps = {
48 byAmount: 10,
49 shouldComponentUpdate: true,
50 };
51 shouldComponentUpdate(nextProps) {
52 return nextProps.shouldComponentUpdate;
53 }
54 render() {
55 // Simulate time passing when this component is rendered
56 Scheduler.unstable_advanceTime(this.props.byAmount);
57 return this.props.children || null;
58 }
59 };
60 }
61
62 describe(`onRender`, () => {
63 beforeEach(() => {
64 jest.resetModules();
65 loadModules();
66 });
67
68 it('should handle errors thrown', async () => {
69 const callback = jest.fn(id => {
70 if (id === 'throw') {
71 throw Error('expected');
72 }
73 });
74
75 let didMount = false;
76 class ClassComponent extends React.Component {
77 componentDidMount() {
78 didMount = true;
79 }
80 render() {
81 return this.props.children;
82 }
83 }
84
85 // Errors thrown from onRender should not break the commit phase,
86 // Or prevent other lifecycles from being called.
87 await expect(
88 act(() => {
89 ReactNoop.render(
90 <ClassComponent>
91 <React.Profiler id="do-not-throw" onRender={callback}>
92 <React.Profiler id="throw" onRender={callback}>
93 <div />
94 </React.Profiler>
95 </React.Profiler>
96 </ClassComponent>,
97 );
98 }),
99 ).rejects.toThrow('expected');
100 expect(didMount).toBe(true);
101 expect(callback).toHaveBeenCalledTimes(2);
102 });
103
104 it('is not invoked until the commit phase', async () => {
105 const callback = jest.fn();
106
107 const Yield = ({value}) => {
108 Scheduler.log(value);
109 return null;
110 };
111
112 React.startTransition(() => {
113 ReactNoop.render(
114 <React.Profiler id="test" onRender={callback}>
115 <Yield value="first" />
116 <Yield value="last" />
117 </React.Profiler>,
118 );
119 });
120
121 // Times are logged until a render is committed.
122 await waitFor(['first']);
123 expect(callback).toHaveBeenCalledTimes(0);
124 await waitForAll(['last']);
125 expect(callback).toHaveBeenCalledTimes(1);
126 });
127
128 // @gate !__DEV__
129 it('does not record times for components outside of Profiler tree', async () => {
130 // Mock the Scheduler module so we can track how many times the current
131 // time is read
132 jest.mock('scheduler', obj => {
133 const ActualScheduler = jest.requireActual('scheduler/unstable_mock');
134 return {
135 ...ActualScheduler,
136 unstable_now: function mockUnstableNow() {
137 ActualScheduler.log('read current time');
138 return ActualScheduler.unstable_now();
139 },
140 };
141 });
142
143 jest.resetModules();
144
145 loadModules();
146
147 // Clear yields in case the current time is read during initialization.
148 Scheduler.unstable_clearLog();
149
150 await act(() => {
151 ReactNoop.render(
152 <div>
153 <AdvanceTime />
154 <AdvanceTime />
155 <AdvanceTime />
156 <AdvanceTime />
157 <AdvanceTime />
158 </div>,
159 );
160 });
161
162 // Restore original mock
163 jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
164
165 if (gate(flags => flags.enableComponentPerformanceTrack)) {
166 assertLog([
167 'read current time',
168 'read current time',
169 'read current time',
170 'read current time',
171 'read current time',
172 'read current time',
173 'read current time',
174 'read current time',
175 'read current time',
176 'read current time',
177 'read current time',
178 'read current time',
179 'read current time',
180 'read current time',
181 'read current time',
182 'read current time',
183 ]);
184 } else {
185 assertLog([
186 'read current time',
187 'read current time',
188 'read current time',
189 'read current time',
190 'read current time',
191 'read current time',
192 'read current time',
193 'read current time',
194 'read current time',
195 'read current time',
196 'read current time',
197 ]);
198 }
199 });
200
201 it('does not report work done on a sibling', async () => {
202 const callback = jest.fn();
203
204 const DoesNotUpdate = React.memo(
205 function DoesNotUpdateInner() {
206 Scheduler.unstable_advanceTime(10);
207 return null;
208 },
209 () => true,
210 );
211
212 let updateProfilerSibling;
213
214 function ProfilerSibling() {
215 const [count, setCount] = React.useState(0);
216 updateProfilerSibling = () => setCount(count + 1);
217 return null;
218 }
219
220 function App() {
221 return (
222 <React.Fragment>
223 <React.Profiler id="test" onRender={callback}>
224 <DoesNotUpdate />
225 </React.Profiler>
226 <ProfilerSibling />
227 </React.Fragment>
228 );
229 }
230
231 const root = ReactNoop.createRoot();
232 await act(() => {
233 root.render(<App />);
234 });
235
236 expect(callback).toHaveBeenCalledTimes(1);
237
238 let call = callback.mock.calls[0];
239
240 expect(call).toHaveLength(6);
241 expect(call[0]).toBe('test');
242 expect(call[1]).toBe('mount');
243 expect(call[2]).toBe(10); // actual time
244 expect(call[3]).toBe(10); // base time
245 expect(call[4]).toBe(0); // start time
246 expect(call[5]).toBe(10); // commit time
247
248 callback.mockReset();
249
250 Scheduler.unstable_advanceTime(20); // 10 -> 30
251
252 await act(() => {
253 root.render(<App />);
254 });
255
256 if (gate(flags => flags.enableUseJSStackToTrackPassiveDurations)) {
257 // None of the Profiler's subtree was rendered because App bailed out before the Profiler.
258 // So we expect onRender not to be called.
259 expect(callback).not.toHaveBeenCalled();
260 } else {
261 // Updating a parent reports a re-render,
262 // since React technically did a little bit of work between the Profiler and the bailed out subtree.
263 // This is not optimal but it's how the old reconciler fork works.
264 expect(callback).toHaveBeenCalledTimes(1);
265
266 call = callback.mock.calls[0];
267
268 expect(call).toHaveLength(6);
269 expect(call[0]).toBe('test');
270 expect(call[1]).toBe('update');
271 expect(call[2]).toBe(0); // actual time
272 expect(call[3]).toBe(10); // base time
273 expect(call[4]).toBe(30); // start time
274 expect(call[5]).toBe(30); // commit time
275
276 callback.mockReset();
277 }
278
279 Scheduler.unstable_advanceTime(20); // 30 -> 50
280
281 // Updating a sibling should not report a re-render.
282 await act(() => updateProfilerSibling());
283
284 expect(callback).not.toHaveBeenCalled();
285 });
286
287 it('logs render times for both mount and update', async () => {
288 const callback = jest.fn();
289
290 Scheduler.unstable_advanceTime(5); // 0 -> 5
291
292 const root = ReactNoop.createRoot();
293 await act(() => {
294 root.render(
295 <React.Profiler id="test" onRender={callback}>
296 <AdvanceTime />
297 </React.Profiler>,
298 );
299 });
300
301 expect(callback).toHaveBeenCalledTimes(1);
302
303 let [call] = callback.mock.calls;
304
305 expect(call).toHaveLength(6);
306 expect(call[0]).toBe('test');
307 expect(call[1]).toBe('mount');
308 expect(call[2]).toBe(10); // actual time
309 expect(call[3]).toBe(10); // base time
310 expect(call[4]).toBe(5); // start time
311 expect(call[5]).toBe(15); // commit time
312
313 callback.mockReset();
314
315 Scheduler.unstable_advanceTime(20); // 15 -> 35
316
317 await act(() => {
318 root.render(
319 <React.Profiler id="test" onRender={callback}>
320 <AdvanceTime />
321 </React.Profiler>,
322 );
323 });
324
325 expect(callback).toHaveBeenCalledTimes(1);
326
327 [call] = callback.mock.calls;
328
329 expect(call).toHaveLength(6);
330 expect(call[0]).toBe('test');
331 expect(call[1]).toBe('update');
332 expect(call[2]).toBe(10); // actual time
333 expect(call[3]).toBe(10); // base time
334 expect(call[4]).toBe(35); // start time
335 expect(call[5]).toBe(45); // commit time
336
337 callback.mockReset();
338
339 Scheduler.unstable_advanceTime(20); // 45 -> 65
340
341 await act(() => {
342 root.render(
343 <React.Profiler id="test" onRender={callback}>
344 <AdvanceTime byAmount={4} />
345 </React.Profiler>,
346 );
347 });
348
349 expect(callback).toHaveBeenCalledTimes(1);
350
351 [call] = callback.mock.calls;
352
353 expect(call).toHaveLength(6);
354 expect(call[0]).toBe('test');
355 expect(call[1]).toBe('update');
356 expect(call[2]).toBe(4); // actual time
357 expect(call[3]).toBe(4); // base time
358 expect(call[4]).toBe(65); // start time
359 expect(call[5]).toBe(69); // commit time
360 });
361
362 it('includes render times of nested Profilers in their parent times', async () => {
363 const callback = jest.fn();
364
365 Scheduler.unstable_advanceTime(5); // 0 -> 5
366
367 const root = ReactNoop.createRoot();
368 await act(() => {
369 root.render(
370 <React.Fragment>
371 <React.Profiler id="parent" onRender={callback}>
372 <AdvanceTime byAmount={10}>
373 <React.Profiler id="child" onRender={callback}>
374 <AdvanceTime byAmount={20} />
375 </React.Profiler>
376 </AdvanceTime>
377 </React.Profiler>
378 </React.Fragment>,
379 );
380 });
381
382 expect(callback).toHaveBeenCalledTimes(2);
383
384 // Callbacks bubble (reverse order).
385 const [childCall, parentCall] = callback.mock.calls;
386 expect(childCall[0]).toBe('child');
387 expect(parentCall[0]).toBe('parent');
388
389 // Parent times should include child times
390 expect(childCall[2]).toBe(20); // actual time
391 expect(childCall[3]).toBe(20); // base time
392 expect(childCall[4]).toBe(15); // start time
393 expect(childCall[5]).toBe(35); // commit time
394 expect(parentCall[2]).toBe(30); // actual time
395 expect(parentCall[3]).toBe(30); // base time
396 expect(parentCall[4]).toBe(5); // start time
397 expect(parentCall[5]).toBe(35); // commit time
398 });
399
400 it('traces sibling Profilers separately', async () => {
401 const callback = jest.fn();
402
403 Scheduler.unstable_advanceTime(5); // 0 -> 5
404
405 const root = ReactNoop.createRoot();
406 await act(() => {
407 root.render(
408 <React.Fragment>
409 <React.Profiler id="first" onRender={callback}>
410 <AdvanceTime byAmount={20} />
411 </React.Profiler>
412 <React.Profiler id="second" onRender={callback}>
413 <AdvanceTime byAmount={5} />
414 </React.Profiler>
415 </React.Fragment>,
416 );
417 });
418
419 expect(callback).toHaveBeenCalledTimes(2);
420
421 const [firstCall, secondCall] = callback.mock.calls;
422 expect(firstCall[0]).toBe('first');
423 expect(secondCall[0]).toBe('second');
424
425 // Parent times should include child times
426 expect(firstCall[2]).toBe(20); // actual time
427 expect(firstCall[3]).toBe(20); // base time
428 expect(firstCall[4]).toBe(5); // start time
429 expect(firstCall[5]).toBe(30); // commit time
430 expect(secondCall[2]).toBe(5); // actual time
431 expect(secondCall[3]).toBe(5); // base time
432 expect(secondCall[4]).toBe(25); // start time
433 expect(secondCall[5]).toBe(30); // commit time
434 });
435
436 it('does not include time spent outside of profile root', async () => {
437 const callback = jest.fn();
438
439 Scheduler.unstable_advanceTime(5); // 0 -> 5
440
441 const root = ReactNoop.createRoot();
442 await act(() => {
443 root.render(
444 <React.Fragment>
445 <AdvanceTime byAmount={20} />
446 <React.Profiler id="test" onRender={callback}>
447 <AdvanceTime byAmount={5} />
448 </React.Profiler>
449 <AdvanceTime byAmount={20} />
450 </React.Fragment>,
451 );
452 });
453
454 expect(callback).toHaveBeenCalledTimes(1);
455
456 const [call] = callback.mock.calls;
457 expect(call[0]).toBe('test');
458 expect(call[2]).toBe(5); // actual time
459 expect(call[3]).toBe(5); // base time
460 expect(call[4]).toBe(25); // start time
461 expect(call[5]).toBe(50); // commit time
462 });
463
464 it('is not called when blocked by sCU false', async () => {
465 const callback = jest.fn();
466
467 let instance;
468 class Updater extends React.Component {
469 state = {};
470 render() {
471 instance = this;
472 return this.props.children;
473 }
474 }
475
476 const root = ReactNoop.createRoot();
477 await act(() => {
478 root.render(
479 <React.Profiler id="outer" onRender={callback}>
480 <Updater>
481 <React.Profiler id="inner" onRender={callback}>
482 <div />
483 </React.Profiler>
484 </Updater>
485 </React.Profiler>,
486 );
487 });
488 // All profile callbacks are called for initial render
489 expect(callback).toHaveBeenCalledTimes(2);
490
491 callback.mockReset();
492
493 ReactNoop.flushSync(() => {
494 instance.setState({
495 count: 1,
496 });
497 });
498
499 // Only call onRender for paths that have re-rendered.
500 // Since the Updater's props didn't change,
501 // React does not re-render its children.
502 expect(callback).toHaveBeenCalledTimes(1);
503 expect(callback.mock.calls[0][0]).toBe('outer');
504 });
505
506 it('decreases actual time but not base time when sCU prevents an update', async () => {
507 const callback = jest.fn();
508
509 Scheduler.unstable_advanceTime(5); // 0 -> 5
510
511 const root = ReactNoop.createRoot();
512 await act(() => {
513 root.render(
514 <React.Profiler id="test" onRender={callback}>
515 <AdvanceTime byAmount={10}>
516 <AdvanceTime byAmount={13} shouldComponentUpdate={false} />
517 </AdvanceTime>
518 </React.Profiler>,
519 );
520 });
521
522 expect(callback).toHaveBeenCalledTimes(1);
523
524 Scheduler.unstable_advanceTime(30); // 28 -> 58
525
526 await act(() => {
527 root.render(
528 <React.Profiler id="test" onRender={callback}>
529 <AdvanceTime byAmount={4}>
530 <AdvanceTime byAmount={7} shouldComponentUpdate={false} />
531 </AdvanceTime>
532 </React.Profiler>,
533 );
534 });
535
536 expect(callback).toHaveBeenCalledTimes(2);
537
538 const [mountCall, updateCall] = callback.mock.calls;
539
540 expect(mountCall[1]).toBe('mount');
541 expect(mountCall[2]).toBe(23); // actual time
542 expect(mountCall[3]).toBe(23); // base time
543 expect(mountCall[4]).toBe(5); // start time
544 expect(mountCall[5]).toBe(28); // commit time
545
546 expect(updateCall[1]).toBe('update');
547 expect(updateCall[2]).toBe(4); // actual time
548 expect(updateCall[3]).toBe(17); // base time
549 expect(updateCall[4]).toBe(58); // start time
550 expect(updateCall[5]).toBe(62); // commit time
551 });
552
553 it('includes time spent in render phase lifecycles', async () => {
554 class WithLifecycles extends React.Component {
555 state = {};
556 static getDerivedStateFromProps() {
557 Scheduler.unstable_advanceTime(3);
558 return null;
559 }
560 shouldComponentUpdate() {
561 Scheduler.unstable_advanceTime(7);
562 return true;
563 }
564 render() {
565 Scheduler.unstable_advanceTime(5);
566 return null;
567 }
568 }
569
570 const callback = jest.fn();
571
572 Scheduler.unstable_advanceTime(5); // 0 -> 5
573
574 const root = ReactNoop.createRoot();
575 await act(() => {
576 root.render(
577 <React.Profiler id="test" onRender={callback}>
578 <WithLifecycles />
579 </React.Profiler>,
580 );
581 });
582
583 Scheduler.unstable_advanceTime(15); // 13 -> 28
584
585 await act(() => {
586 root.render(
587 <React.Profiler id="test" onRender={callback}>
588 <WithLifecycles />
589 </React.Profiler>,
590 );
591 });
592
593 expect(callback).toHaveBeenCalledTimes(2);
594
595 const [mountCall, updateCall] = callback.mock.calls;
596
597 expect(mountCall[1]).toBe('mount');
598 expect(mountCall[2]).toBe(8); // actual time
599 expect(mountCall[3]).toBe(8); // base time
600 expect(mountCall[4]).toBe(5); // start time
601 expect(mountCall[5]).toBe(13); // commit time
602
603 expect(updateCall[1]).toBe('update');
604 expect(updateCall[2]).toBe(15); // actual time
605 expect(updateCall[3]).toBe(15); // base time
606 expect(updateCall[4]).toBe(28); // start time
607 expect(updateCall[5]).toBe(43); // commit time
608 });
609
610 it('should clear nested-update flag when multiple cascading renders are scheduled', async () => {
611 jest.resetModules();
612 loadModules();
613
614 function Component() {
615 const [didMount, setDidMount] = React.useState(false);
616 const [didMountAndUpdate, setDidMountAndUpdate] = React.useState(false);
617
618 React.useLayoutEffect(() => {
619 setDidMount(true);
620 }, []);
621
622 React.useEffect(() => {
623 if (didMount && !didMountAndUpdate) {
624 setDidMountAndUpdate(true);
625 }
626 }, [didMount, didMountAndUpdate]);
627
628 Scheduler.log(`${didMount}:${didMountAndUpdate}`);
629
630 return null;
631 }
632
633 const onRender = jest.fn();
634
635 await act(() => {
636 ReactNoop.render(
637 <React.Profiler id="root" onRender={onRender}>
638 <Component />
639 </React.Profiler>,
640 );
641 });
642 assertLog(['false:false', 'true:false', 'true:true']);
643
644 expect(onRender).toHaveBeenCalledTimes(3);
645 expect(onRender.mock.calls[0][1]).toBe('mount');
646 expect(onRender.mock.calls[1][1]).toBe('nested-update');
647 expect(onRender.mock.calls[2][1]).toBe('update');
648 });
649
650 it('is properly distinguish updates and nested-updates when there is more than sync remaining work', () => {
651 jest.resetModules();
652 loadModules();
653
654 function Component() {
655 const [didMount, setDidMount] = React.useState(false);
656
657 React.useLayoutEffect(() => {
658 setDidMount(true);
659 }, []);
660 Scheduler.log(didMount);
661 return didMount;
662 }
663
664 const onRender = jest.fn();
665
666 // Schedule low-priority work.
667 React.startTransition(() =>
668 ReactNoop.render(
669 <React.Profiler id="root" onRender={onRender}>
670 <Component />
671 </React.Profiler>,
672 ),
673 );
674
675 // Flush sync work with a nested update
676 ReactNoop.flushSync(() => {
677 ReactNoop.render(
678 <React.Profiler id="root" onRender={onRender}>
679 <Component />
680 </React.Profiler>,
681 );
682 });
683 assertLog([false, true]);
684
685 // Verify that the nested update inside of the sync work is appropriately tagged.
686 expect(onRender).toHaveBeenCalledTimes(2);
687 expect(onRender.mock.calls[0][1]).toBe('mount');
688 expect(onRender.mock.calls[1][1]).toBe('nested-update');
689 });
690
691 describe('with regard to interruptions', () => {
692 it('should accumulate actual time after a scheduling interruptions', async () => {
693 const callback = jest.fn();
694
695 const Yield = ({renderTime}) => {
696 Scheduler.unstable_advanceTime(renderTime);
697 Scheduler.log('Yield:' + renderTime);
698 return null;
699 };
700
701 Scheduler.unstable_advanceTime(5); // 0 -> 5
702
703 const root = ReactNoop.createRoot();
704 // Render partially, but run out of time before completing.
705 React.startTransition(() => {
706 root.render(
707 <React.Profiler id="test" onRender={callback}>
708 <Yield renderTime={2} />
709 <Yield renderTime={3} />
710 </React.Profiler>,
711 );
712 });
713
714 await waitFor(['Yield:2']);
715 expect(callback).toHaveBeenCalledTimes(0);
716
717 // Resume render for remaining children.
718 await waitForAll(['Yield:3']);
719
720 // Verify that logged times include both durations above.
721 expect(callback).toHaveBeenCalledTimes(1);
722 const [call] = callback.mock.calls;
723 expect(call[2]).toBe(5); // actual time
724 expect(call[3]).toBe(5); // base time
725 expect(call[4]).toBe(5); // start time
726 expect(call[5]).toBe(10); // commit time
727 });
728
729 it('should not include time between frames', async () => {
730 const callback = jest.fn();
731
732 const Yield = ({renderTime}) => {
733 Scheduler.unstable_advanceTime(renderTime);
734 Scheduler.log('Yield:' + renderTime);
735 return null;
736 };
737
738 Scheduler.unstable_advanceTime(5); // 0 -> 5
739
740 const root = ReactNoop.createRoot();
741 // Render partially, but don't finish.
742 // This partial render should take 5ms of simulated time.
743 React.startTransition(() => {
744 root.render(
745 <React.Profiler id="outer" onRender={callback}>
746 <Yield renderTime={5} />
747 <Yield renderTime={10} />
748 <React.Profiler id="inner" onRender={callback}>
749 <Yield renderTime={17} />
750 </React.Profiler>
751 </React.Profiler>,
752 );
753 });
754
755 await waitFor(['Yield:5']);
756 expect(callback).toHaveBeenCalledTimes(0);
757
758 // Simulate time moving forward while frame is paused.
759 Scheduler.unstable_advanceTime(50); // 10 -> 60
760
761 // Flush the remaining work,
762 // Which should take an additional 10ms of simulated time.
763 await waitForAll(['Yield:10', 'Yield:17']);
764 expect(callback).toHaveBeenCalledTimes(2);
765
766 const [innerCall, outerCall] = callback.mock.calls;
767
768 // Verify that the actual time includes all work times,
769 // But not the time that elapsed between frames.
770 expect(innerCall[0]).toBe('inner');
771 expect(innerCall[2]).toBe(17); // actual time
772 expect(innerCall[3]).toBe(17); // base time
773 expect(innerCall[4]).toBe(70); // start time
774 expect(innerCall[5]).toBe(87); // commit time
775 expect(outerCall[0]).toBe('outer');
776 expect(outerCall[2]).toBe(32); // actual time
777 expect(outerCall[3]).toBe(32); // base time
778 expect(outerCall[4]).toBe(5); // start time
779 expect(outerCall[5]).toBe(87); // commit time
780 });
781
782 it('should report the expected times when a high-pri update replaces a mount in-progress', async () => {
783 const callback = jest.fn();
784
785 const Yield = ({renderTime}) => {
786 Scheduler.unstable_advanceTime(renderTime);
787 Scheduler.log('Yield:' + renderTime);
788 return null;
789 };
790
791 Scheduler.unstable_advanceTime(5); // 0 -> 5
792
793 const root = ReactNoop.createRoot();
794 // Render a partially update, but don't finish.
795 // This partial render should take 10ms of simulated time.
796 React.startTransition(() => {
797 root.render(
798 <React.Profiler id="test" onRender={callback}>
799 <Yield renderTime={10} />
800 <Yield renderTime={20} />
801 </React.Profiler>,
802 );
803 });
804
805 await waitFor(['Yield:10']);
806 expect(callback).toHaveBeenCalledTimes(0);
807
808 // Simulate time moving forward while frame is paused.
809 Scheduler.unstable_advanceTime(100); // 15 -> 115
810
811 // Interrupt with higher priority work.
812 // The interrupted work simulates an additional 5ms of time.
813 ReactNoop.flushSync(() => {
814 root.render(
815 <React.Profiler id="test" onRender={callback}>
816 <Yield renderTime={5} />
817 </React.Profiler>,
818 );
819 });
820 assertLog(['Yield:5']);
821
822 // The initial work was thrown away in this case,
823 // So the actual and base times should only include the final rendered tree times.
824 expect(callback).toHaveBeenCalledTimes(1);
825 const call = callback.mock.calls[0];
826 expect(call[2]).toBe(5); // actual time
827 expect(call[3]).toBe(5); // base time
828 expect(call[4]).toBe(115); // start time
829 expect(call[5]).toBe(120); // commit time
830
831 callback.mockReset();
832
833 // Verify no more unexpected callbacks from low priority work
834 await waitForAll([]);
835 expect(callback).toHaveBeenCalledTimes(0);
836 });
837
838 it('should report the expected times when a high-priority update replaces a low-priority update', async () => {
839 const callback = jest.fn();
840
841 const Yield = ({renderTime}) => {
842 Scheduler.unstable_advanceTime(renderTime);
843 Scheduler.log('Yield:' + renderTime);
844 return null;
845 };
846
847 Scheduler.unstable_advanceTime(5); // 0 -> 5
848 const root = ReactNoop.createRoot();
849 root.render(
850 <React.Profiler id="test" onRender={callback}>
851 <Yield renderTime={6} />
852 <Yield renderTime={15} />
853 </React.Profiler>,
854 );
855
856 // Render everything initially.
857 // This should take 21 seconds of actual and base time.
858 await waitForAll(['Yield:6', 'Yield:15']);
859 expect(callback).toHaveBeenCalledTimes(1);
860 let call = callback.mock.calls[0];
861 expect(call[2]).toBe(21); // actual time
862 expect(call[3]).toBe(21); // base time
863 expect(call[4]).toBe(5); // start time
864 expect(call[5]).toBe(26); // commit time
865
866 callback.mockReset();
867
868 Scheduler.unstable_advanceTime(30); // 26 -> 56
869
870 // Render a partially update, but don't finish.
871 // This partial render should take 3ms of simulated time.
872 React.startTransition(() => {
873 root.render(
874 <React.Profiler id="test" onRender={callback}>
875 <Yield renderTime={3} />
876 <Yield renderTime={5} />
877 <Yield renderTime={9} />
878 </React.Profiler>,
879 );
880 });
881
882 await waitFor(['Yield:3']);
883 expect(callback).toHaveBeenCalledTimes(0);
884
885 // Simulate time moving forward while frame is paused.
886 Scheduler.unstable_advanceTime(100); // 59 -> 159
887
888 // Render another 5ms of simulated time.
889 await waitFor(['Yield:5']);
890 expect(callback).toHaveBeenCalledTimes(0);
891
892 // Simulate time moving forward while frame is paused.
893 Scheduler.unstable_advanceTime(100); // 164 -> 264
894
895 // Interrupt with higher priority work.
896 // The interrupted work simulates an additional 11ms of time.
897 ReactNoop.flushSync(() => {
898 root.render(
899 <React.Profiler id="test" onRender={callback}>
900 <Yield renderTime={11} />
901 </React.Profiler>,
902 );
903 });
904 assertLog(['Yield:11']);
905
906 // The actual time should include only the most recent render,
907 // Because this lets us avoid a lot of commit phase reset complexity.
908 // The base time includes only the final rendered tree times.
909 expect(callback).toHaveBeenCalledTimes(1);
910 call = callback.mock.calls[0];
911 expect(call[2]).toBe(11); // actual time
912 expect(call[3]).toBe(11); // base time
913 expect(call[4]).toBe(264); // start time
914 expect(call[5]).toBe(275); // commit time
915
916 // Verify no more unexpected callbacks from low priority work
917 await waitForAll([]);
918 expect(callback).toHaveBeenCalledTimes(1);
919 });
920
921 it('should report the expected times when a high-priority update interrupts a low-priority update', async () => {
922 const callback = jest.fn();
923
924 const Yield = ({renderTime}) => {
925 Scheduler.unstable_advanceTime(renderTime);
926 Scheduler.log('Yield:' + renderTime);
927 return null;
928 };
929
930 let first;
931 class FirstComponent extends React.Component {
932 state = {renderTime: 1};
933 render() {
934 first = this;
935 Scheduler.unstable_advanceTime(this.state.renderTime);
936 Scheduler.log('FirstComponent:' + this.state.renderTime);
937 return <Yield renderTime={4} />;
938 }
939 }
940 let second;
941 class SecondComponent extends React.Component {
942 state = {renderTime: 2};
943 render() {
944 second = this;
945 Scheduler.unstable_advanceTime(this.state.renderTime);
946 Scheduler.log('SecondComponent:' + this.state.renderTime);
947 return <Yield renderTime={7} />;
948 }
949 }
950
951 Scheduler.unstable_advanceTime(5); // 0 -> 5
952
953 const root = ReactNoop.createRoot();
954 root.render(
955 <React.Profiler id="test" onRender={callback}>
956 <FirstComponent />
957 <SecondComponent />
958 </React.Profiler>,
959 );
960
961 // Render everything initially.
962 // This simulates a total of 14ms of actual render time.
963 // The base render time is also 14ms for the initial render.
964 await waitForAll([
965 'FirstComponent:1',
966 'Yield:4',
967 'SecondComponent:2',
968 'Yield:7',
969 ]);
970 expect(callback).toHaveBeenCalledTimes(1);
971 let call = callback.mock.calls[0];
972 expect(call[2]).toBe(14); // actual time
973 expect(call[3]).toBe(14); // base time
974 expect(call[4]).toBe(5); // start time
975 expect(call[5]).toBe(19); // commit time
976
977 callback.mockClear();
978
979 Scheduler.unstable_advanceTime(100); // 19 -> 119
980
981 // Render a partially update, but don't finish.
982 // This partial render will take 10ms of actual render time.
983 React.startTransition(() => {
984 first.setState({renderTime: 10});
985 });
986
987 await waitFor(['FirstComponent:10']);
988 expect(callback).toHaveBeenCalledTimes(0);
989
990 // Simulate time moving forward while frame is paused.
991 Scheduler.unstable_advanceTime(100); // 129 -> 229
992
993 // Interrupt with higher priority work.
994 // This simulates a total of 37ms of actual render time.
995 ReactNoop.flushSync(() => second.setState({renderTime: 30}));
996 assertLog(['SecondComponent:30', 'Yield:7']);
997
998 // The actual time should include only the most recent render (37ms),
999 // Because this greatly simplifies the commit phase logic.
1000 // The base time should include the more recent times for the SecondComponent subtree,
1001 // As well as the original times for the FirstComponent subtree.
1002 expect(callback).toHaveBeenCalledTimes(1);
1003 call = callback.mock.calls[0];
1004 expect(call[2]).toBe(37); // actual time
1005 expect(call[3]).toBe(42); // base time
1006 expect(call[4]).toBe(229); // start time
1007 expect(call[5]).toBe(266); // commit time
1008
1009 callback.mockClear();
1010
1011 // Simulate time moving forward while frame is paused.
1012 Scheduler.unstable_advanceTime(100); // 266 -> 366
1013
1014 // Resume the original low priority update, with rebased state.
1015 // This simulates a total of 14ms of actual render time,
1016 // And does not include the original (interrupted) 10ms.
1017 // The tree contains 42ms of base render time at this point,
1018 // Reflecting the most recent (longer) render durations.
1019 // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
1020 await waitForAll(['FirstComponent:10', 'Yield:4']);
1021 expect(callback).toHaveBeenCalledTimes(1);
1022 call = callback.mock.calls[0];
1023 expect(call[2]).toBe(14); // actual time
1024 expect(call[3]).toBe(51); // base time
1025 expect(call[4]).toBe(366); // start time
1026 expect(call[5]).toBe(380); // commit time
1027 });
1028
1029 it('should accumulate actual time after an error handled by componentDidCatch()', async () => {
1030 const callback = jest.fn();
1031
1032 const ThrowsError = ({unused}) => {
1033 Scheduler.unstable_advanceTime(3);
1034 throw Error('expected error');
1035 };
1036
1037 class ErrorBoundary extends React.Component {
1038 state = {error: null};
1039 componentDidCatch(error) {
1040 this.setState({error});
1041 }
1042 render() {
1043 Scheduler.unstable_advanceTime(2);
1044 return this.state.error === null ? (
1045 this.props.children
1046 ) : (
1047 <AdvanceTime byAmount={20} />
1048 );
1049 }
1050 }
1051
1052 Scheduler.unstable_advanceTime(5); // 0 -> 5
1053
1054 const root = ReactNoop.createRoot();
1055 await act(() => {
1056 root.render(
1057 <React.Profiler id="test" onRender={callback}>
1058 <ErrorBoundary>
1059 <AdvanceTime byAmount={9} />
1060 <ThrowsError />
1061 </ErrorBoundary>
1062 </React.Profiler>,
1063 );
1064 });
1065
1066 expect(callback).toHaveBeenCalledTimes(2);
1067
1068 // Callbacks bubble (reverse order).
1069 const [mountCall, updateCall] = callback.mock.calls;
1070
1071 // The initial mount only includes the ErrorBoundary (which takes 2)
1072 // But it spends time rendering all of the failed subtree also.
1073 expect(mountCall[1]).toBe('mount');
1074 // actual time includes: 2 (ErrorBoundary) + 9 (AdvanceTime) + 3 (ThrowsError)
1075 // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1076 expect(mountCall[2]).toBe(14);
1077 // base time includes: 2 (ErrorBoundary)
1078 // Since the tree is empty for the initial commit
1079 expect(mountCall[3]).toBe(2);
1080
1081 // start time: 5 initially + 14 of work
1082 // Add an additional 3 (ThrowsError) if we replayed the failed work
1083 expect(mountCall[4]).toBe(19);
1084 // commit time: 19 initially + 14 of work
1085 // Add an additional 6 (ThrowsError *2) if we replayed the failed work
1086 expect(mountCall[5]).toBe(33);
1087
1088 // The update includes the ErrorBoundary and its fallback child
1089 expect(updateCall[1]).toBe('nested-update');
1090 // actual time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1091 expect(updateCall[2]).toBe(22);
1092 // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1093 expect(updateCall[3]).toBe(22);
1094 // start time
1095 expect(updateCall[4]).toBe(33);
1096 // commit time: 19 (startTime) + 2 (ErrorBoundary) + 20 (AdvanceTime)
1097 // Add an additional 3 (ThrowsError) if we replayed the failed work
1098 expect(updateCall[5]).toBe(55);
1099 });
1100
1101 it('should accumulate actual time after an error handled by getDerivedStateFromError()', async () => {
1102 const callback = jest.fn();
1103
1104 const ThrowsError = ({unused}) => {
1105 Scheduler.unstable_advanceTime(10);
1106 throw Error('expected error');
1107 };
1108
1109 class ErrorBoundary extends React.Component {
1110 state = {error: null};
1111 static getDerivedStateFromError(error) {
1112 return {error};
1113 }
1114 render() {
1115 Scheduler.unstable_advanceTime(2);
1116 return this.state.error === null ? (
1117 this.props.children
1118 ) : (
1119 <AdvanceTime byAmount={20} />
1120 );
1121 }
1122 }
1123
1124 Scheduler.unstable_advanceTime(5); // 0 -> 5
1125
1126 await act(() => {
1127 const root = ReactNoop.createRoot();
1128 root.render(
1129 <React.Profiler id="test" onRender={callback}>
1130 <ErrorBoundary>
1131 <AdvanceTime byAmount={5} />
1132 <ThrowsError />
1133 </ErrorBoundary>
1134 </React.Profiler>,
1135 );
1136 });
1137
1138 expect(callback).toHaveBeenCalledTimes(1);
1139
1140 // Callbacks bubble (reverse order).
1141 const [mountCall] = callback.mock.calls;
1142
1143 // The initial mount includes the ErrorBoundary's error state,
1144 // But it also spends actual time rendering UI that fails and isn't included.
1145 expect(mountCall[1]).toBe('mount');
1146 // actual time includes: 2 (ErrorBoundary) + 5 (AdvanceTime) + 10 (ThrowsError)
1147 // Then the re-render: 2 (ErrorBoundary) + 20 (AdvanceTime)
1148 // We don't count the time spent in replaying the failed unit of work (ThrowsError)
1149 expect(mountCall[2]).toBe(39);
1150 // base time includes: 2 (ErrorBoundary) + 20 (AdvanceTime)
1151 expect(mountCall[3]).toBe(22);
1152 // start time
1153 expect(mountCall[4]).toBe(44);
1154 // commit time
1155 expect(mountCall[5]).toBe(83);
1156 });
1157
1158 it('should reset the fiber stack correct after a "complete" phase error', async () => {
1159 jest.resetModules();
1160
1161 loadModules({
1162 useNoopRenderer: true,
1163 });
1164
1165 // Simulate a renderer error during the "complete" phase.
1166 // This mimics behavior like React Native's View/Text nesting validation.
1167 ReactNoop.render(
1168 <React.Profiler id="profiler" onRender={jest.fn()}>
1169 <errorInCompletePhase>hi</errorInCompletePhase>
1170 </React.Profiler>,
1171 );
1172 await waitForThrow('Error in host config.');
1173
1174 // A similar case we've seen caused by an invariant in ReactDOM.
1175 // It didn't reproduce without a host component inside.
1176 ReactNoop.render(
1177 <React.Profiler id="profiler" onRender={jest.fn()}>
1178 <errorInCompletePhase>
1179 <span>hi</span>
1180 </errorInCompletePhase>
1181 </React.Profiler>,
1182 );
1183 await waitForThrow('Error in host config.');
1184
1185 // So long as the profiler timer's fiber stack is reset correctly,
1186 // Subsequent renders should not error.
1187 ReactNoop.render(
1188 <React.Profiler id="profiler" onRender={jest.fn()}>
1189 <span>hi</span>
1190 </React.Profiler>,
1191 );
1192 await waitForAll([]);
1193 });
1194 });
1195
1196 it('reflects the most recently rendered id value', async () => {
1197 const callback = jest.fn();
1198
1199 Scheduler.unstable_advanceTime(5); // 0 -> 5
1200
1201 const root = ReactNoop.createRoot();
1202 await act(() => {
1203 root.render(
1204 <React.Profiler id="one" onRender={callback}>
1205 <AdvanceTime byAmount={2} />
1206 </React.Profiler>,
1207 );
1208 });
1209
1210 expect(callback).toHaveBeenCalledTimes(1);
1211
1212 Scheduler.unstable_advanceTime(20); // 7 -> 27
1213
1214 await act(() => {
1215 root.render(
1216 <React.Profiler id="two" onRender={callback}>
1217 <AdvanceTime byAmount={1} />
1218 </React.Profiler>,
1219 );
1220 });
1221
1222 expect(callback).toHaveBeenCalledTimes(2);
1223
1224 const [mountCall, updateCall] = callback.mock.calls;
1225
1226 expect(mountCall[0]).toBe('one');
1227 expect(mountCall[1]).toBe('mount');
1228 expect(mountCall[2]).toBe(2); // actual time
1229 expect(mountCall[3]).toBe(2); // base time
1230 expect(mountCall[4]).toBe(5); // start time
1231
1232 expect(updateCall[0]).toBe('two');
1233 expect(updateCall[1]).toBe('update');
1234 expect(updateCall[2]).toBe(1); // actual time
1235 expect(updateCall[3]).toBe(1); // base time
1236 expect(updateCall[4]).toBe(27); // start time
1237 });
1238
1239 it('should not be called until after mutations', async () => {
1240 let classComponentMounted = false;
1241 const callback = jest.fn(
1242 (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
1243 // Don't call this hook until after mutations
1244 expect(classComponentMounted).toBe(true);
1245 // But the commit time should reflect pre-mutation
1246 expect(commitTime).toBe(2);
1247 },
1248 );
1249
1250 class ClassComponent extends React.Component {
1251 componentDidMount() {
1252 Scheduler.unstable_advanceTime(5);
1253 classComponentMounted = true;
1254 }
1255 render() {
1256 Scheduler.unstable_advanceTime(2);
1257 return null;
1258 }
1259 }
1260 const root = ReactNoop.createRoot();
1261 await act(() => {
1262 root.render(
1263 <React.Profiler id="test" onRender={callback}>
1264 <ClassComponent />
1265 </React.Profiler>,
1266 );
1267 });
1268
1269 expect(callback).toHaveBeenCalledTimes(1);
1270 });
1271 });
1272
1273 describe(`onCommit`, () => {
1274 beforeEach(() => {
1275 jest.resetModules();
1276
1277 loadModules();
1278 });
1279
1280 it('should report time spent in layout effects and commit lifecycles', async () => {
1281 const callback = jest.fn();
1282
1283 const ComponentWithEffects = () => {
1284 React.useLayoutEffect(() => {
1285 Scheduler.unstable_advanceTime(10);
1286 return () => {
1287 Scheduler.unstable_advanceTime(100);
1288 };
1289 }, []);
1290 React.useLayoutEffect(() => {
1291 Scheduler.unstable_advanceTime(1000);
1292 return () => {
1293 Scheduler.unstable_advanceTime(10000);
1294 };
1295 });
1296 React.useEffect(() => {
1297 // This passive effect is here to verify that its time isn't reported.
1298 Scheduler.unstable_advanceTime(5);
1299 return () => {
1300 Scheduler.unstable_advanceTime(7);
1301 };
1302 });
1303 return null;
1304 };
1305
1306 class ComponentWithCommitHooks extends React.Component {
1307 componentDidMount() {
1308 Scheduler.unstable_advanceTime(100000);
1309 }
1310 componentDidUpdate() {
1311 Scheduler.unstable_advanceTime(1000000);
1312 }
1313 render() {
1314 return null;
1315 }
1316 }
1317
1318 Scheduler.unstable_advanceTime(1);
1319 const root = ReactNoop.createRoot();
1320 await act(() => {
1321 root.render(
1322 <React.Profiler id="mount-test" onCommit={callback}>
1323 <ComponentWithEffects />
1324 <ComponentWithCommitHooks />
1325 </React.Profiler>,
1326 );
1327 });
1328
1329 expect(callback).toHaveBeenCalledTimes(1);
1330
1331 let call = callback.mock.calls[0];
1332
1333 expect(call).toHaveLength(4);
1334 expect(call[0]).toBe('mount-test');
1335 expect(call[1]).toBe('mount');
1336 expect(call[2]).toBe(101010); // durations
1337 expect(call[3]).toBe(1); // commit start time (before mutations or effects)
1338
1339 Scheduler.unstable_advanceTime(1);
1340
1341 await act(() => {
1342 root.render(
1343 <React.Profiler id="update-test" onCommit={callback}>
1344 <ComponentWithEffects />
1345 <ComponentWithCommitHooks />
1346 </React.Profiler>,
1347 );
1348 });
1349
1350 expect(callback).toHaveBeenCalledTimes(2);
1351
1352 call = callback.mock.calls[1];
1353
1354 expect(call).toHaveLength(4);
1355 expect(call[0]).toBe('update-test');
1356 expect(call[1]).toBe('update');
1357 expect(call[2]).toBe(1011000); // durations
1358 expect(call[3]).toBe(101017); // commit start time (before mutations or effects)
1359
1360 Scheduler.unstable_advanceTime(1);
1361
1362 await act(() => {
1363 root.render(<React.Profiler id="unmount-test" onCommit={callback} />);
1364 });
1365
1366 expect(callback).toHaveBeenCalledTimes(3);
1367
1368 call = callback.mock.calls[2];
1369
1370 expect(call).toHaveLength(4);
1371 expect(call[0]).toBe('unmount-test');
1372 expect(call[1]).toBe('update');
1373 expect(call[2]).toBe(10100); // durations
1374 expect(call[3]).toBe(1112030); // commit start time (before mutations or effects)
1375 });
1376
1377 it('should report time spent in layout effects and commit lifecycles with cascading renders', async () => {
1378 const callback = jest.fn();
1379
1380 const ComponentWithEffects = ({shouldCascade}) => {
1381 const [didCascade, setDidCascade] = React.useState(false);
1382 Scheduler.unstable_advanceTime(100000000);
1383 React.useLayoutEffect(() => {
1384 if (shouldCascade && !didCascade) {
1385 setDidCascade(true);
1386 }
1387 Scheduler.unstable_advanceTime(didCascade ? 30 : 10);
1388 return () => {
1389 Scheduler.unstable_advanceTime(100);
1390 };
1391 }, [didCascade, shouldCascade]);
1392 return null;
1393 };
1394
1395 class ComponentWithCommitHooks extends React.Component {
1396 state = {
1397 didCascade: false,
1398 };
1399 componentDidMount() {
1400 Scheduler.unstable_advanceTime(1000);
1401 }
1402 componentDidUpdate() {
1403 Scheduler.unstable_advanceTime(10000);
1404 if (this.props.shouldCascade && !this.state.didCascade) {
1405 this.setState({didCascade: true});
1406 }
1407 }
1408 render() {
1409 Scheduler.unstable_advanceTime(1000000000);
1410 return null;
1411 }
1412 }
1413
1414 Scheduler.unstable_advanceTime(1);
1415 const root = ReactNoop.createRoot();
1416 await act(() => {
1417 root.render(
1418 <React.Profiler id="mount-test" onCommit={callback}>
1419 <ComponentWithEffects shouldCascade={true} />
1420 <ComponentWithCommitHooks />
1421 </React.Profiler>,
1422 );
1423 });
1424
1425 expect(callback).toHaveBeenCalledTimes(2);
1426
1427 let call = callback.mock.calls[0];
1428
1429 expect(call).toHaveLength(4);
1430 expect(call[0]).toBe('mount-test');
1431 expect(call[1]).toBe('mount');
1432 expect(call[2]).toBe(1010); // durations
1433 expect(call[3]).toBe(1100000001); // commit start time (before mutations or effects)
1434
1435 call = callback.mock.calls[1];
1436
1437 expect(call).toHaveLength(4);
1438 expect(call[0]).toBe('mount-test');
1439 expect(call[1]).toBe('nested-update');
1440 expect(call[2]).toBe(130); // durations
1441 expect(call[3]).toBe(1200001011); // commit start time (before mutations or effects)
1442
1443 Scheduler.unstable_advanceTime(1);
1444
1445 await act(() => {
1446 root.render(
1447 <React.Profiler id="update-test" onCommit={callback}>
1448 <ComponentWithEffects />
1449 <ComponentWithCommitHooks shouldCascade={true} />
1450 </React.Profiler>,
1451 );
1452 });
1453
1454 expect(callback).toHaveBeenCalledTimes(4);
1455
1456 call = callback.mock.calls[2];
1457
1458 expect(call).toHaveLength(4);
1459 expect(call[0]).toBe('update-test');
1460 expect(call[1]).toBe('update');
1461 expect(call[2]).toBe(10130); // durations
1462 expect(call[3]).toBe(2300001142); // commit start time (before mutations or effects)
1463
1464 call = callback.mock.calls[3];
1465
1466 expect(call).toHaveLength(4);
1467 expect(call[0]).toBe('update-test');
1468 expect(call[1]).toBe('nested-update');
1469 expect(call[2]).toBe(10000); // durations
1470 expect(call[3]).toBe(3300011272); // commit start time (before mutations or effects)
1471 });
1472
1473 it('should include time spent in ref callbacks', async () => {
1474 const callback = jest.fn();
1475
1476 const refSetter = ref => {
1477 if (ref !== null) {
1478 Scheduler.unstable_advanceTime(10);
1479 } else {
1480 Scheduler.unstable_advanceTime(100);
1481 }
1482 };
1483
1484 class ClassComponent extends React.Component {
1485 render() {
1486 return null;
1487 }
1488 }
1489
1490 const Component = () => {
1491 Scheduler.unstable_advanceTime(1000);
1492 return <ClassComponent ref={refSetter} />;
1493 };
1494
1495 Scheduler.unstable_advanceTime(1);
1496
1497 const root = ReactNoop.createRoot();
1498 await act(() => {
1499 root.render(
1500 <React.Profiler id="root" onCommit={callback}>
1501 <Component />
1502 </React.Profiler>,
1503 );
1504 });
1505
1506 expect(callback).toHaveBeenCalledTimes(1);
1507
1508 let call = callback.mock.calls[0];
1509
1510 expect(call).toHaveLength(4);
1511 expect(call[0]).toBe('root');
1512 expect(call[1]).toBe('mount');
1513 expect(call[2]).toBe(10); // durations
1514 expect(call[3]).toBe(1001); // commit start time (before mutations or effects)
1515
1516 callback.mockClear();
1517
1518 await act(() => {
1519 root.render(<React.Profiler id="root" onCommit={callback} />);
1520 });
1521
1522 expect(callback).toHaveBeenCalledTimes(1);
1523
1524 call = callback.mock.calls[0];
1525
1526 expect(call).toHaveLength(4);
1527 expect(call[0]).toBe('root');
1528 expect(call[1]).toBe('update');
1529 expect(call[2]).toBe(100); // durations
1530 expect(call[3]).toBe(1011); // commit start time (before mutations or effects)
1531 });
1532
1533 it('should bubble time spent in layout effects to higher profilers', async () => {
1534 const callback = jest.fn();
1535
1536 const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => {
1537 const setCount = React.useState(0)[1];
1538 if (setCountRef != null) {
1539 setCountRef.current = setCount;
1540 }
1541 React.useLayoutEffect(() => {
1542 Scheduler.unstable_advanceTime(duration);
1543 return () => {
1544 Scheduler.unstable_advanceTime(cleanupDuration);
1545 };
1546 });
1547 Scheduler.unstable_advanceTime(1);
1548 return null;
1549 };
1550
1551 const setCountRef = React.createRef(null);
1552
1553 const root = ReactNoop.createRoot();
1554 await act(() => {
1555 root.render(
1556 <React.Profiler id="root-mount" onCommit={callback}>
1557 <React.Profiler id="a">
1558 <ComponentWithEffects
1559 duration={10}
1560 cleanupDuration={100}
1561 setCountRef={setCountRef}
1562 />
1563 </React.Profiler>
1564 <React.Profiler id="b">
1565 <ComponentWithEffects duration={1000} cleanupDuration={10000} />
1566 </React.Profiler>
1567 </React.Profiler>,
1568 );
1569 });
1570
1571 expect(callback).toHaveBeenCalledTimes(1);
1572
1573 let call = callback.mock.calls[0];
1574
1575 expect(call).toHaveLength(4);
1576 expect(call[0]).toBe('root-mount');
1577 expect(call[1]).toBe('mount');
1578 expect(call[2]).toBe(1010); // durations
1579 expect(call[3]).toBe(2); // commit start time (before mutations or effects)
1580
1581 await act(() => setCountRef.current(count => count + 1));
1582
1583 expect(callback).toHaveBeenCalledTimes(2);
1584
1585 call = callback.mock.calls[1];
1586
1587 expect(call).toHaveLength(4);
1588 expect(call[0]).toBe('root-mount');
1589 expect(call[1]).toBe('update');
1590 expect(call[2]).toBe(110); // durations
1591 expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
1592
1593 await act(() => {
1594 root.render(
1595 <React.Profiler id="root-update" onCommit={callback}>
1596 <React.Profiler id="b">
1597 <ComponentWithEffects duration={1000} cleanupDuration={10000} />
1598 </React.Profiler>
1599 </React.Profiler>,
1600 );
1601 });
1602
1603 expect(callback).toHaveBeenCalledTimes(3);
1604
1605 call = callback.mock.calls[2];
1606
1607 expect(call).toHaveLength(4);
1608 expect(call[0]).toBe('root-update');
1609 expect(call[1]).toBe('update');
1610 expect(call[2]).toBe(11100); // durations
1611 expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
1612 });
1613
1614 it('should properly report time in layout effects even when there are errors', async () => {
1615 const callback = jest.fn();
1616
1617 class ErrorBoundary extends React.Component {
1618 state = {error: null};
1619 static getDerivedStateFromError(error) {
1620 return {error};
1621 }
1622 render() {
1623 return this.state.error === null
1624 ? this.props.children
1625 : this.props.fallback;
1626 }
1627 }
1628
1629 const ComponentWithEffects = ({
1630 cleanupDuration,
1631 duration,
1632 effectDuration,
1633 shouldThrow,
1634 }) => {
1635 React.useLayoutEffect(() => {
1636 Scheduler.unstable_advanceTime(effectDuration);
1637 if (shouldThrow) {
1638 throw Error('expected');
1639 }
1640 return () => {
1641 Scheduler.unstable_advanceTime(cleanupDuration);
1642 };
1643 });
1644 Scheduler.unstable_advanceTime(duration);
1645 return null;
1646 };
1647
1648 Scheduler.unstable_advanceTime(1);
1649
1650 // Test an error that happens during an effect
1651
1652 const root = ReactNoop.createRoot();
1653 await act(() => {
1654 root.render(
1655 <React.Profiler id="root" onCommit={callback}>
1656 <ErrorBoundary
1657 fallback={
1658 <ComponentWithEffects
1659 duration={10000000}
1660 effectDuration={100000000}
1661 cleanupDuration={1000000000}
1662 />
1663 }>
1664 <ComponentWithEffects
1665 duration={10}
1666 effectDuration={100}
1667 cleanupDuration={1000}
1668 shouldThrow={true}
1669 />
1670 </ErrorBoundary>
1671 <ComponentWithEffects
1672 duration={10000}
1673 effectDuration={100000}
1674 cleanupDuration={1000000}
1675 />
1676 </React.Profiler>,
1677 );
1678 });
1679
1680 expect(callback).toHaveBeenCalledTimes(2);
1681
1682 let call = callback.mock.calls[0];
1683
1684 // Initial render (with error)
1685 expect(call).toHaveLength(4);
1686 expect(call[0]).toBe('root');
1687 expect(call[1]).toBe('mount');
1688 expect(call[2]).toBe(100100); // durations
1689 expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
1690
1691 call = callback.mock.calls[1];
1692
1693 // Cleanup render from error boundary
1694 expect(call).toHaveLength(4);
1695 expect(call[0]).toBe('root');
1696 expect(call[1]).toBe('nested-update');
1697 expect(call[2]).toBe(100000000); // durations
1698 expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
1699 });
1700
1701 it('should properly report time in layout effect cleanup functions even when there are errors', async () => {
1702 const callback = jest.fn();
1703
1704 class ErrorBoundary extends React.Component {
1705 state = {error: null};
1706 static getDerivedStateFromError(error) {
1707 return {error};
1708 }
1709 render() {
1710 return this.state.error === null
1711 ? this.props.children
1712 : this.props.fallback;
1713 }
1714 }
1715
1716 const ComponentWithEffects = ({
1717 cleanupDuration,
1718 duration,
1719 effectDuration,
1720 shouldThrow = false,
1721 }) => {
1722 React.useLayoutEffect(() => {
1723 Scheduler.unstable_advanceTime(effectDuration);
1724 return () => {
1725 Scheduler.unstable_advanceTime(cleanupDuration);
1726 if (shouldThrow) {
1727 throw Error('expected');
1728 }
1729 };
1730 });
1731 Scheduler.unstable_advanceTime(duration);
1732 return null;
1733 };
1734
1735 Scheduler.unstable_advanceTime(1);
1736
1737 const root = ReactNoop.createRoot();
1738 await act(() => {
1739 root.render(
1740 <React.Profiler id="root" onCommit={callback}>
1741 <ErrorBoundary
1742 fallback={
1743 <ComponentWithEffects
1744 duration={10000000}
1745 effectDuration={100000000}
1746 cleanupDuration={1000000000}
1747 />
1748 }>
1749 <ComponentWithEffects
1750 duration={10}
1751 effectDuration={100}
1752 cleanupDuration={1000}
1753 shouldThrow={true}
1754 />
1755 </ErrorBoundary>
1756 <ComponentWithEffects
1757 duration={10000}
1758 effectDuration={100000}
1759 cleanupDuration={1000000}
1760 />
1761 </React.Profiler>,
1762 );
1763 });
1764
1765 expect(callback).toHaveBeenCalledTimes(1);
1766
1767 let call = callback.mock.calls[0];
1768
1769 // Initial render
1770 expect(call).toHaveLength(4);
1771 expect(call[0]).toBe('root');
1772 expect(call[1]).toBe('mount');
1773 expect(call[2]).toBe(100100); // durations
1774 expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
1775
1776 callback.mockClear();
1777
1778 // Test an error that happens during an cleanup function
1779
1780 await act(() => {
1781 root.render(
1782 <React.Profiler id="root" onCommit={callback}>
1783 <ErrorBoundary
1784 fallback={
1785 <ComponentWithEffects
1786 duration={10000000}
1787 effectDuration={100000000}
1788 cleanupDuration={1000000000}
1789 />
1790 }>
1791 <ComponentWithEffects
1792 duration={10}
1793 effectDuration={100}
1794 cleanupDuration={1000}
1795 shouldThrow={false}
1796 />
1797 </ErrorBoundary>
1798 <ComponentWithEffects
1799 duration={10000}
1800 effectDuration={100000}
1801 cleanupDuration={1000000}
1802 />
1803 </React.Profiler>,
1804 );
1805 });
1806
1807 expect(callback).toHaveBeenCalledTimes(2);
1808
1809 call = callback.mock.calls[0];
1810
1811 // Update (that throws)
1812 expect(call).toHaveLength(4);
1813 expect(call[0]).toBe('root');
1814 expect(call[1]).toBe('update');
1815 expect(call[2]).toBe(1101100); // durations
1816 expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
1817
1818 call = callback.mock.calls[1];
1819
1820 // Cleanup render from error boundary
1821 expect(call).toHaveLength(4);
1822 expect(call[0]).toBe('root');
1823 expect(call[1]).toBe('nested-update');
1824 expect(call[2]).toBe(100001000); // durations
1825 expect(call[3]).toBe(11221221); // commit start time (before mutations or effects)
1826 });
1827 });
1828
1829 describe(`onPostCommit`, () => {
1830 beforeEach(() => {
1831 jest.resetModules();
1832
1833 loadModules();
1834 });
1835
1836 it('should report time spent in passive effects', async () => {
1837 const callback = jest.fn();
1838
1839 const ComponentWithEffects = () => {
1840 React.useLayoutEffect(() => {
1841 // This layout effect is here to verify that its time isn't reported.
1842 Scheduler.unstable_advanceTime(5);
1843 return () => {
1844 Scheduler.unstable_advanceTime(7);
1845 };
1846 });
1847 React.useEffect(() => {
1848 Scheduler.unstable_advanceTime(10);
1849 return () => {
1850 Scheduler.unstable_advanceTime(100);
1851 };
1852 }, []);
1853 React.useEffect(() => {
1854 Scheduler.unstable_advanceTime(1000);
1855 return () => {
1856 Scheduler.unstable_advanceTime(10000);
1857 };
1858 });
1859 return null;
1860 };
1861
1862 Scheduler.unstable_advanceTime(1);
1863
1864 const root = ReactNoop.createRoot();
1865 await act(() => {
1866 root.render(
1867 <React.Profiler id="mount-test" onPostCommit={callback}>
1868 <ComponentWithEffects />
1869 </React.Profiler>,
1870 );
1871 });
1872 await waitForAll([]);
1873
1874 expect(callback).toHaveBeenCalledTimes(1);
1875
1876 let call = callback.mock.calls[0];
1877
1878 expect(call).toHaveLength(4);
1879 expect(call[0]).toBe('mount-test');
1880 expect(call[1]).toBe('mount');
1881 expect(call[2]).toBe(1010); // durations
1882 expect(call[3]).toBe(1); // commit start time (before mutations or effects)
1883
1884 Scheduler.unstable_advanceTime(1);
1885
1886 await act(() => {
1887 root.render(
1888 <React.Profiler id="update-test" onPostCommit={callback}>
1889 <ComponentWithEffects />
1890 </React.Profiler>,
1891 );
1892 });
1893 await waitForAll([]);
1894
1895 expect(callback).toHaveBeenCalledTimes(2);
1896
1897 call = callback.mock.calls[1];
1898
1899 expect(call).toHaveLength(4);
1900 expect(call[0]).toBe('update-test');
1901 expect(call[1]).toBe('update');
1902 expect(call[2]).toBe(11000); // durations
1903 expect(call[3]).toBe(1017); // commit start time (before mutations or effects)
1904
1905 Scheduler.unstable_advanceTime(1);
1906
1907 await act(() => {
1908 root.render(<React.Profiler id="unmount-test" onPostCommit={callback} />);
1909 });
1910 await waitForAll([]);
1911
1912 expect(callback).toHaveBeenCalledTimes(3);
1913
1914 call = callback.mock.calls[2];
1915
1916 expect(call).toHaveLength(4);
1917 expect(call[0]).toBe('unmount-test');
1918 expect(call[1]).toBe('update');
1919 expect(call[2]).toBe(10100); // durations
1920 expect(call[3]).toBe(12030); // commit start time (before mutations or effects)
1921 });
1922
1923 it('should report time spent in passive effects with cascading renders', async () => {
1924 const callback = jest.fn();
1925
1926 const ComponentWithEffects = () => {
1927 const [didMount, setDidMount] = React.useState(false);
1928 Scheduler.unstable_advanceTime(1000);
1929 React.useEffect(() => {
1930 if (!didMount) {
1931 setDidMount(true);
1932 }
1933 Scheduler.unstable_advanceTime(didMount ? 30 : 10);
1934 return () => {
1935 Scheduler.unstable_advanceTime(100);
1936 };
1937 }, [didMount]);
1938 return null;
1939 };
1940
1941 Scheduler.unstable_advanceTime(1);
1942
1943 const root = ReactNoop.createRoot();
1944 await act(() => {
1945 root.render(
1946 <React.Profiler id="mount-test" onPostCommit={callback}>
1947 <ComponentWithEffects />
1948 </React.Profiler>,
1949 );
1950 });
1951
1952 expect(callback).toHaveBeenCalledTimes(2);
1953
1954 let call = callback.mock.calls[0];
1955
1956 expect(call).toHaveLength(4);
1957 expect(call[0]).toBe('mount-test');
1958 expect(call[1]).toBe('mount');
1959 expect(call[2]).toBe(10); // durations
1960 expect(call[3]).toBe(1001); // commit start time (before mutations or effects)
1961
1962 call = callback.mock.calls[1];
1963
1964 expect(call).toHaveLength(4);
1965 expect(call[0]).toBe('mount-test');
1966 expect(call[1]).toBe('update');
1967 expect(call[2]).toBe(130); // durations
1968 expect(call[3]).toBe(2011); // commit start time (before mutations or effects)
1969 });
1970
1971 it('should bubble time spent in effects to higher profilers', async () => {
1972 const callback = jest.fn();
1973
1974 const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => {
1975 const setCount = React.useState(0)[1];
1976 if (setCountRef != null) {
1977 setCountRef.current = setCount;
1978 }
1979 React.useEffect(() => {
1980 Scheduler.unstable_advanceTime(duration);
1981 return () => {
1982 Scheduler.unstable_advanceTime(cleanupDuration);
1983 };
1984 });
1985 Scheduler.unstable_advanceTime(1);
1986 return null;
1987 };
1988
1989 const setCountRef = React.createRef(null);
1990
1991 const root = ReactNoop.createRoot();
1992 await act(() => {
1993 root.render(
1994 <React.Profiler id="root-mount" onPostCommit={callback}>
1995 <React.Profiler id="a">
1996 <ComponentWithEffects
1997 duration={10}
1998 cleanupDuration={100}
1999 setCountRef={setCountRef}
2000 />
2001 </React.Profiler>
2002 <React.Profiler id="b">
2003 <ComponentWithEffects duration={1000} cleanupDuration={10000} />
2004 </React.Profiler>
2005 </React.Profiler>,
2006 );
2007 });
2008
2009 expect(callback).toHaveBeenCalledTimes(1);
2010
2011 let call = callback.mock.calls[0];
2012
2013 expect(call).toHaveLength(4);
2014 expect(call[0]).toBe('root-mount');
2015 expect(call[1]).toBe('mount');
2016 expect(call[2]).toBe(1010); // durations
2017 expect(call[3]).toBe(2); // commit start time (before mutations or effects)
2018
2019 await act(() => setCountRef.current(count => count + 1));
2020
2021 expect(callback).toHaveBeenCalledTimes(2);
2022
2023 call = callback.mock.calls[1];
2024
2025 expect(call).toHaveLength(4);
2026 expect(call[0]).toBe('root-mount');
2027 expect(call[1]).toBe('update');
2028 expect(call[2]).toBe(110); // durations
2029 expect(call[3]).toBe(1013); // commit start time (before mutations or effects)
2030
2031 await act(() => {
2032 root.render(
2033 <React.Profiler id="root-update" onPostCommit={callback}>
2034 <React.Profiler id="b">
2035 <ComponentWithEffects duration={1000} cleanupDuration={10000} />
2036 </React.Profiler>
2037 </React.Profiler>,
2038 );
2039 });
2040
2041 expect(callback).toHaveBeenCalledTimes(3);
2042
2043 call = callback.mock.calls[2];
2044
2045 expect(call).toHaveLength(4);
2046 expect(call[0]).toBe('root-update');
2047 expect(call[1]).toBe('update');
2048 expect(call[2]).toBe(11100); // durations
2049 expect(call[3]).toBe(1124); // commit start time (before mutations or effects)
2050 });
2051
2052 it('should properly report time in passive effects even when there are errors', async () => {
2053 const callback = jest.fn();
2054
2055 class ErrorBoundary extends React.Component {
2056 state = {error: null};
2057 static getDerivedStateFromError(error) {
2058 return {error};
2059 }
2060 render() {
2061 return this.state.error === null
2062 ? this.props.children
2063 : this.props.fallback;
2064 }
2065 }
2066
2067 const ComponentWithEffects = ({
2068 cleanupDuration,
2069 duration,
2070 effectDuration,
2071 shouldThrow,
2072 }) => {
2073 React.useEffect(() => {
2074 Scheduler.unstable_advanceTime(effectDuration);
2075 if (shouldThrow) {
2076 throw Error('expected');
2077 }
2078 return () => {
2079 Scheduler.unstable_advanceTime(cleanupDuration);
2080 };
2081 });
2082 Scheduler.unstable_advanceTime(duration);
2083 return null;
2084 };
2085
2086 Scheduler.unstable_advanceTime(1);
2087
2088 // Test an error that happens during an effect
2089 const root = ReactNoop.createRoot();
2090 await act(() => {
2091 root.render(
2092 <React.Profiler id="root" onPostCommit={callback}>
2093 <ErrorBoundary
2094 fallback={
2095 <ComponentWithEffects
2096 duration={10000000}
2097 effectDuration={100000000}
2098 cleanupDuration={1000000000}
2099 />
2100 }>
2101 <ComponentWithEffects
2102 duration={10}
2103 effectDuration={100}
2104 cleanupDuration={1000}
2105 shouldThrow={true}
2106 />
2107 </ErrorBoundary>
2108 <ComponentWithEffects
2109 duration={10000}
2110 effectDuration={100000}
2111 cleanupDuration={1000000}
2112 />
2113 </React.Profiler>,
2114 );
2115 });
2116
2117 expect(callback).toHaveBeenCalledTimes(2);
2118
2119 let call = callback.mock.calls[0];
2120
2121 // Initial render (with error)
2122 expect(call).toHaveLength(4);
2123 expect(call[0]).toBe('root');
2124 expect(call[1]).toBe('mount');
2125 expect(call[2]).toBe(100100); // durations
2126 expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
2127
2128 call = callback.mock.calls[1];
2129
2130 // Cleanup render from error boundary
2131 expect(call).toHaveLength(4);
2132 expect(call[0]).toBe('root');
2133 expect(call[1]).toBe('update');
2134 expect(call[2]).toBe(100000000); // durations
2135 expect(call[3]).toBe(10110111); // commit start time (before mutations or effects)
2136 });
2137
2138 it('should properly report time in passive effect cleanup functions even when there are errors', async () => {
2139 const callback = jest.fn();
2140
2141 class ErrorBoundary extends React.Component {
2142 state = {error: null};
2143 static getDerivedStateFromError(error) {
2144 return {error};
2145 }
2146 render() {
2147 return this.state.error === null
2148 ? this.props.children
2149 : this.props.fallback;
2150 }
2151 }
2152
2153 const ComponentWithEffects = ({
2154 cleanupDuration,
2155 duration,
2156 effectDuration,
2157 shouldThrow = false,
2158 id,
2159 }) => {
2160 React.useEffect(() => {
2161 Scheduler.unstable_advanceTime(effectDuration);
2162 return () => {
2163 Scheduler.unstable_advanceTime(cleanupDuration);
2164 if (shouldThrow) {
2165 throw Error('expected');
2166 }
2167 };
2168 });
2169 Scheduler.unstable_advanceTime(duration);
2170 return null;
2171 };
2172
2173 Scheduler.unstable_advanceTime(1);
2174
2175 const root = ReactNoop.createRoot();
2176 await act(() => {
2177 root.render(
2178 <React.Profiler id="root" onPostCommit={callback}>
2179 <ErrorBoundary
2180 fallback={
2181 <ComponentWithEffects
2182 duration={10000000}
2183 effectDuration={100000000}
2184 cleanupDuration={1000000000}
2185 />
2186 }>
2187 <ComponentWithEffects
2188 duration={10}
2189 effectDuration={100}
2190 cleanupDuration={1000}
2191 shouldThrow={true}
2192 />
2193 </ErrorBoundary>
2194 <ComponentWithEffects
2195 duration={10000}
2196 effectDuration={100000}
2197 cleanupDuration={1000000}
2198 />
2199 </React.Profiler>,
2200 );
2201 });
2202
2203 expect(callback).toHaveBeenCalledTimes(1);
2204
2205 let call = callback.mock.calls[0];
2206
2207 // Initial render
2208 expect(call).toHaveLength(4);
2209 expect(call[0]).toBe('root');
2210 expect(call[1]).toBe('mount');
2211 expect(call[2]).toBe(100100); // durations
2212 expect(call[3]).toBe(10011); // commit start time (before mutations or effects)
2213
2214 callback.mockClear();
2215
2216 // Test an error that happens during an cleanup function
2217
2218 await act(() => {
2219 root.render(
2220 <React.Profiler id="root" onPostCommit={callback}>
2221 <ErrorBoundary
2222 fallback={
2223 <ComponentWithEffects
2224 duration={10000000}
2225 effectDuration={100000000}
2226 cleanupDuration={1000000000}
2227 />
2228 }>
2229 <ComponentWithEffects
2230 duration={10}
2231 effectDuration={100}
2232 cleanupDuration={1000}
2233 shouldThrow={false}
2234 />
2235 </ErrorBoundary>
2236 <ComponentWithEffects
2237 duration={10000}
2238 effectDuration={100000}
2239 cleanupDuration={1000000}
2240 />
2241 </React.Profiler>,
2242 );
2243 });
2244
2245 expect(callback).toHaveBeenCalledTimes(2);
2246
2247 call = callback.mock.calls[0];
2248
2249 // Update (that throws)
2250 expect(call).toHaveLength(4);
2251 expect(call[0]).toBe('root');
2252 expect(call[1]).toBe('update');
2253 // We continue flushing pending effects even if one throws.
2254 expect(call[2]).toBe(1101100); // durations
2255 expect(call[3]).toBe(120121); // commit start time (before mutations or effects)
2256
2257 call = callback.mock.calls[1];
2258
2259 // Cleanup render from error boundary
2260 expect(call).toHaveLength(4);
2261 expect(call[0]).toBe('root');
2262 expect(call[1]).toBe('update');
2263 expect(call[2]).toBe(100001000); // durations
2264 // The commit time varies because the above duration time varies
2265 expect(call[3]).toBe(11221221); // commit start time (before mutations or effects)
2266 });
2267 });