1
+/**
2
+ * Copyright (c) Facebook, Inc. and its 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
+'use strict';
11
+
12
+describe('Timeline profiler', () => {
13
+ let React;
14
+ let ReactDOM;
15
+ let Scheduler;
16
+ let renderHelper;
17
+ let renderRootHelper;
18
+ let unmountFns;
19
+ let utils;
20
+
21
+ let clearedMarks;
22
+ let featureDetectionMarkName = null;
23
+ let marks;
24
+ let setPerformanceMock;
25
+
26
+ function createUserTimingPolyfill() {
27
+ featureDetectionMarkName = null;
28
+
29
+ clearedMarks = [];
30
+ marks = [];
31
+
32
+ // Remove file-system specific bits or version-specific bits of information from the module range marks.
33
+ function filterMarkData(markName) {
34
+ if (markName.startsWith('--react-internal-module-start')) {
35
+ return `${markName.substr(0, 29)}-<filtered-file-system-path>`;
36
+ } else if (markName.startsWith('--react-internal-module-stop')) {
37
+ return `${markName.substr(0, 28)}-<filtered-file-system-path>`;
38
+ } else if (markName.startsWith('--react-version')) {
39
+ return `${markName.substr(0, 15)}-<filtered-version>`;
40
+ } else {
41
+ return markName;
42
+ }
43
+ }
44
+
45
+ // This is not a true polyfill, but it gives us enough to capture marks.
46
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
47
+ return {
48
+ clearMarks(markName) {
49
+ markName = filterMarkData(markName);
50
+
51
+ clearedMarks.push(markName);
52
+ marks = marks.filter(mark => mark !== markName);
53
+ },
54
+ mark(markName, markOptions) {
55
+ markName = filterMarkData(markName);
56
+
57
+ if (featureDetectionMarkName === null) {
58
+ featureDetectionMarkName = markName;
59
+ }
60
+
61
+ marks.push(markName);
62
+
63
+ if (markOptions != null) {
64
+ // This is triggers the feature detection.
65
+ markOptions.startTime++;
66
+ }
67
+ },
68
+ };
69
+ }
70
+
71
+ function clearPendingMarks() {
72
+ clearedMarks.splice(0);
73
+ }
74
+
75
+ function dispatchAndSetCurrentEvent(element, event) {
76
+ try {
77
+ window.event = event;
78
+ element.dispatchEvent(event);
79
+ } finally {
80
+ window.event = undefined;
81
+ }
82
+ }
83
+
84
+ beforeEach(() => {
85
+ utils = require('./utils');
86
+ utils.beforeEachProfiling();
87
+
88
+ unmountFns = [];
89
+ renderHelper = element => {
90
+ const unmountFn = utils.legacyRender(element);
91
+ unmountFns.push(unmountFn);
92
+ return unmountFn;
93
+ };
94
+ renderRootHelper = element => {
95
+ const container = document.createElement('div');
96
+ const root = ReactDOM.createRoot(container);
97
+ root.render(element);
98
+ const unmountFn = () => root.unmount();
99
+ unmountFns.push(unmountFn);
100
+ return unmountFn;
101
+ };
102
+
103
+ React = require('react');
104
+ ReactDOM = require('react-dom');
105
+ Scheduler = require('scheduler');
106
+
107
+ setPerformanceMock = require('react-devtools-shared/src/backend/profilingHooks')
108
+ .setPerformanceMock_ONLY_FOR_TESTING;
109
+ setPerformanceMock(createUserTimingPolyfill());
110
+ });
111
+
112
+ afterEach(() => {
113
+ // Verify all logged marks also get cleared.
114
+ expect(marks).toHaveLength(0);
115
+
116
+ unmountFns.forEach(unmountFn => unmountFn());
117
+
118
+ setPerformanceMock(null);
119
+ });
120
+
121
+ it('should mark sync render without suspends or state updates', () => {
122
+ renderHelper(<div />);
123
+
124
+ expect(clearedMarks).toMatchInlineSnapshot(`
125
+ Array [
126
+ "--schedule-render-1",
127
+ "--render-start-1",
128
+ "--render-stop",
129
+ "--commit-start-1",
130
+ "--react-version-<filtered-version>",
131
+ "--profiler-version-1",
132
+ "--react-internal-module-start-<filtered-file-system-path>",
133
+ "--react-internal-module-stop-<filtered-file-system-path>",
134
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
135
+ "--layout-effects-start-1",
136
+ "--layout-effects-stop",
137
+ "--commit-stop",
138
+ ]
139
+ `);
140
+ });
141
+
142
+ it('should mark concurrent render without suspends or state updates', () => {
143
+ renderRootHelper(<div />);
144
+
145
+ expect(clearedMarks).toMatchInlineSnapshot(`
146
+ Array [
147
+ "--schedule-render-16",
148
+ ]
149
+ `);
150
+
151
+ clearPendingMarks();
152
+
153
+ expect(Scheduler).toFlushUntilNextPaint([]);
154
+
155
+ expect(clearedMarks).toMatchInlineSnapshot(`
156
+ Array [
157
+ "--render-start-16",
158
+ "--render-stop",
159
+ "--commit-start-16",
160
+ "--react-version-<filtered-version>",
161
+ "--profiler-version-1",
162
+ "--react-internal-module-start-<filtered-file-system-path>",
163
+ "--react-internal-module-stop-<filtered-file-system-path>",
164
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
165
+ "--layout-effects-start-16",
166
+ "--layout-effects-stop",
167
+ "--commit-stop",
168
+ ]
169
+ `);
170
+ });
171
+
172
+ it('should mark render yields', async () => {
173
+ function Bar() {
174
+ Scheduler.unstable_yieldValue('Bar');
175
+ return null;
176
+ }
177
+
178
+ function Foo() {
179
+ Scheduler.unstable_yieldValue('Foo');
180
+ return <Bar />;
181
+ }
182
+
183
+ React.startTransition(() => {
184
+ renderRootHelper(<Foo />);
185
+ });
186
+
187
+ // Do one step of work.
188
+ expect(Scheduler).toFlushAndYieldThrough(['Foo']);
189
+
190
+ expect(clearedMarks).toMatchInlineSnapshot(`
191
+ Array [
192
+ "--schedule-render-64",
193
+ "--render-start-64",
194
+ "--component-render-start-Foo",
195
+ "--component-render-stop",
196
+ "--render-yield",
197
+ ]
198
+ `);
199
+ });
200
+
201
+ it('should mark sync render with suspense that resolves', async () => {
202
+ const fakeSuspensePromise = Promise.resolve(true);
203
+ function Example() {
204
+ throw fakeSuspensePromise;
205
+ }
206
+
207
+ renderHelper(
208
+ <React.Suspense fallback={null}>
209
+ <Example />
210
+ </React.Suspense>,
211
+ );
212
+
213
+ expect(clearedMarks).toMatchInlineSnapshot(`
214
+ Array [
215
+ "--schedule-render-1",
216
+ "--render-start-1",
217
+ "--component-render-start-Example",
218
+ "--component-render-stop",
219
+ "--suspense-suspend-0-Example-mount-1-",
220
+ "--render-stop",
221
+ "--commit-start-1",
222
+ "--react-version-<filtered-version>",
223
+ "--profiler-version-1",
224
+ "--react-internal-module-start-<filtered-file-system-path>",
225
+ "--react-internal-module-stop-<filtered-file-system-path>",
226
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
227
+ "--layout-effects-start-1",
228
+ "--layout-effects-stop",
229
+ "--commit-stop",
230
+ ]
231
+ `);
232
+
233
+ clearPendingMarks();
234
+
235
+ await fakeSuspensePromise;
236
+ expect(clearedMarks).toMatchInlineSnapshot(`
237
+ Array [
238
+ "--suspense-resolved-0-Example",
239
+ ]
240
+ `);
241
+ });
242
+
243
+ it('should mark sync render with suspense that rejects', async () => {
244
+ const fakeSuspensePromise = Promise.reject(new Error('error'));
245
+ function Example() {
246
+ throw fakeSuspensePromise;
247
+ }
248
+
249
+ renderHelper(
250
+ <React.Suspense fallback={null}>
251
+ <Example />
252
+ </React.Suspense>,
253
+ );
254
+
255
+ expect(clearedMarks).toMatchInlineSnapshot(`
256
+ Array [
257
+ "--schedule-render-1",
258
+ "--render-start-1",
259
+ "--component-render-start-Example",
260
+ "--component-render-stop",
261
+ "--suspense-suspend-0-Example-mount-1-",
262
+ "--render-stop",
263
+ "--commit-start-1",
264
+ "--react-version-<filtered-version>",
265
+ "--profiler-version-1",
266
+ "--react-internal-module-start-<filtered-file-system-path>",
267
+ "--react-internal-module-stop-<filtered-file-system-path>",
268
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
269
+ "--layout-effects-start-1",
270
+ "--layout-effects-stop",
271
+ "--commit-stop",
272
+ ]
273
+ `);
274
+
275
+ clearPendingMarks();
276
+
277
+ await expect(fakeSuspensePromise).rejects.toThrow();
278
+ expect(clearedMarks).toContain(`--suspense-rejected-0-Example`);
279
+ });
280
+
281
+ it('should mark concurrent render with suspense that resolves', async () => {
282
+ const fakeSuspensePromise = Promise.resolve(true);
283
+ function Example() {
284
+ throw fakeSuspensePromise;
285
+ }
286
+
287
+ renderRootHelper(
288
+ <React.Suspense fallback={null}>
289
+ <Example />
290
+ </React.Suspense>,
291
+ );
292
+
293
+ expect(clearedMarks).toMatchInlineSnapshot(`
294
+ Array [
295
+ "--schedule-render-16",
296
+ ]
297
+ `);
298
+
299
+ clearPendingMarks();
300
+
301
+ expect(Scheduler).toFlushUntilNextPaint([]);
302
+
303
+ expect(clearedMarks).toMatchInlineSnapshot(`
304
+ Array [
305
+ "--render-start-16",
306
+ "--component-render-start-Example",
307
+ "--component-render-stop",
308
+ "--suspense-suspend-0-Example-mount-16-",
309
+ "--render-stop",
310
+ "--commit-start-16",
311
+ "--react-version-<filtered-version>",
312
+ "--profiler-version-1",
313
+ "--react-internal-module-start-<filtered-file-system-path>",
314
+ "--react-internal-module-stop-<filtered-file-system-path>",
315
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
316
+ "--layout-effects-start-16",
317
+ "--layout-effects-stop",
318
+ "--commit-stop",
319
+ ]
320
+ `);
321
+
322
+ clearPendingMarks();
323
+
324
+ await fakeSuspensePromise;
325
+ expect(clearedMarks).toMatchInlineSnapshot(`
326
+ Array [
327
+ "--suspense-resolved-0-Example",
328
+ ]
329
+ `);
330
+ });
331
+
332
+ it('should mark concurrent render with suspense that rejects', async () => {
333
+ const fakeSuspensePromise = Promise.reject(new Error('error'));
334
+ function Example() {
335
+ throw fakeSuspensePromise;
336
+ }
337
+
338
+ renderRootHelper(
339
+ <React.Suspense fallback={null}>
340
+ <Example />
341
+ </React.Suspense>,
342
+ );
343
+
344
+ expect(clearedMarks).toMatchInlineSnapshot(`
345
+ Array [
346
+ "--schedule-render-16",
347
+ ]
348
+ `);
349
+
350
+ clearPendingMarks();
351
+
352
+ expect(Scheduler).toFlushUntilNextPaint([]);
353
+
354
+ expect(clearedMarks).toMatchInlineSnapshot(`
355
+ Array [
356
+ "--render-start-16",
357
+ "--component-render-start-Example",
358
+ "--component-render-stop",
359
+ "--suspense-suspend-0-Example-mount-16-",
360
+ "--render-stop",
361
+ "--commit-start-16",
362
+ "--react-version-<filtered-version>",
363
+ "--profiler-version-1",
364
+ "--react-internal-module-start-<filtered-file-system-path>",
365
+ "--react-internal-module-stop-<filtered-file-system-path>",
366
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
367
+ "--layout-effects-start-16",
368
+ "--layout-effects-stop",
369
+ "--commit-stop",
370
+ ]
371
+ `);
372
+
373
+ clearPendingMarks();
374
+
375
+ await expect(fakeSuspensePromise).rejects.toThrow();
376
+ expect(clearedMarks).toMatchInlineSnapshot(`
377
+ Array [
378
+ "--suspense-rejected-0-Example",
379
+ ]
380
+ `);
381
+ });
382
+
383
+ it('should mark cascading class component state updates', () => {
384
+ class Example extends React.Component {
385
+ state = {didMount: false};
386
+ componentDidMount() {
387
+ this.setState({didMount: true});
388
+ }
389
+ render() {
390
+ return null;
391
+ }
392
+ }
393
+
394
+ renderRootHelper(<Example />);
395
+
396
+ expect(clearedMarks).toMatchInlineSnapshot(`
397
+ Array [
398
+ "--schedule-render-16",
399
+ ]
400
+ `);
401
+
402
+ clearPendingMarks();
403
+
404
+ expect(Scheduler).toFlushUntilNextPaint([]);
405
+
406
+ expect(clearedMarks).toMatchInlineSnapshot(`
407
+ Array [
408
+ "--render-start-16",
409
+ "--component-render-start-Example",
410
+ "--component-render-stop",
411
+ "--render-stop",
412
+ "--commit-start-16",
413
+ "--react-version-<filtered-version>",
414
+ "--profiler-version-1",
415
+ "--react-internal-module-start-<filtered-file-system-path>",
416
+ "--react-internal-module-stop-<filtered-file-system-path>",
417
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
418
+ "--layout-effects-start-16",
419
+ "--schedule-state-update-1-Example",
420
+ "--layout-effects-stop",
421
+ "--render-start-1",
422
+ "--component-render-start-Example",
423
+ "--component-render-stop",
424
+ "--render-stop",
425
+ "--commit-start-1",
426
+ "--react-version-<filtered-version>",
427
+ "--profiler-version-1",
428
+ "--react-internal-module-start-<filtered-file-system-path>",
429
+ "--react-internal-module-stop-<filtered-file-system-path>",
430
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
431
+ "--commit-stop",
432
+ "--commit-stop",
433
+ ]
434
+ `);
435
+ });
436
+
437
+ it('should mark cascading class component force updates', () => {
438
+ class Example extends React.Component {
439
+ componentDidMount() {
440
+ this.forceUpdate();
441
+ }
442
+ render() {
443
+ return null;
444
+ }
445
+ }
446
+
447
+ renderRootHelper(<Example />);
448
+
449
+ expect(clearedMarks).toMatchInlineSnapshot(`
450
+ Array [
451
+ "--schedule-render-16",
452
+ ]
453
+ `);
454
+
455
+ clearPendingMarks();
456
+
457
+ expect(Scheduler).toFlushUntilNextPaint([]);
458
+
459
+ expect(clearedMarks).toMatchInlineSnapshot(`
460
+ Array [
461
+ "--render-start-16",
462
+ "--component-render-start-Example",
463
+ "--component-render-stop",
464
+ "--render-stop",
465
+ "--commit-start-16",
466
+ "--react-version-<filtered-version>",
467
+ "--profiler-version-1",
468
+ "--react-internal-module-start-<filtered-file-system-path>",
469
+ "--react-internal-module-stop-<filtered-file-system-path>",
470
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
471
+ "--layout-effects-start-16",
472
+ "--schedule-forced-update-1-Example",
473
+ "--layout-effects-stop",
474
+ "--render-start-1",
475
+ "--component-render-start-Example",
476
+ "--component-render-stop",
477
+ "--render-stop",
478
+ "--commit-start-1",
479
+ "--react-version-<filtered-version>",
480
+ "--profiler-version-1",
481
+ "--react-internal-module-start-<filtered-file-system-path>",
482
+ "--react-internal-module-stop-<filtered-file-system-path>",
483
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
484
+ "--commit-stop",
485
+ "--commit-stop",
486
+ ]
487
+ `);
488
+ });
489
+
490
+ it('should mark render phase state updates for class component', () => {
491
+ class Example extends React.Component {
492
+ state = {didRender: false};
493
+ render() {
494
+ if (this.state.didRender === false) {
495
+ this.setState({didRender: true});
496
+ }
497
+ return null;
498
+ }
499
+ }
500
+
501
+ renderRootHelper(<Example />);
502
+
503
+ expect(clearedMarks).toMatchInlineSnapshot(`
504
+ Array [
505
+ "--schedule-render-16",
506
+ ]
507
+ `);
508
+
509
+ clearPendingMarks();
510
+
511
+ let errorMessage;
512
+ spyOn(console, 'error').and.callFake(message => {
513
+ errorMessage = message;
514
+ });
515
+
516
+ expect(Scheduler).toFlushUntilNextPaint([]);
517
+
518
+ expect(console.error).toHaveBeenCalledTimes(1);
519
+ expect(errorMessage).toContain(
520
+ 'Cannot update during an existing state transition',
521
+ );
522
+
523
+ expect(clearedMarks).toMatchInlineSnapshot(`
524
+ Array [
525
+ "--render-start-16",
526
+ "--component-render-start-Example",
527
+ "--schedule-state-update-16-Example",
528
+ "--component-render-stop",
529
+ "--render-stop",
530
+ "--commit-start-16",
531
+ "--react-version-<filtered-version>",
532
+ "--profiler-version-1",
533
+ "--react-internal-module-start-<filtered-file-system-path>",
534
+ "--react-internal-module-stop-<filtered-file-system-path>",
535
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
536
+ "--layout-effects-start-16",
537
+ "--layout-effects-stop",
538
+ "--commit-stop",
539
+ ]
540
+ `);
541
+ });
542
+
543
+ it('should mark render phase force updates for class component', () => {
544
+ class Example extends React.Component {
545
+ state = {didRender: false};
546
+ render() {
547
+ if (this.state.didRender === false) {
548
+ this.forceUpdate(() => this.setState({didRender: true}));
549
+ }
550
+ return null;
551
+ }
552
+ }
553
+
554
+ renderRootHelper(<Example />);
555
+
556
+ expect(clearedMarks).toMatchInlineSnapshot(`
557
+ Array [
558
+ "--schedule-render-16",
559
+ ]
560
+ `);
561
+
562
+ clearPendingMarks();
563
+
564
+ let errorMessage;
565
+ spyOn(console, 'error').and.callFake(message => {
566
+ errorMessage = message;
567
+ });
568
+
569
+ expect(Scheduler).toFlushUntilNextPaint([]);
570
+
571
+ expect(console.error).toHaveBeenCalledTimes(1);
572
+ expect(errorMessage).toContain(
573
+ 'Cannot update during an existing state transition',
574
+ );
575
+
576
+ expect(clearedMarks).toMatchInlineSnapshot(`
577
+ Array [
578
+ "--render-start-16",
579
+ "--component-render-start-Example",
580
+ "--schedule-forced-update-16-Example",
581
+ "--component-render-stop",
582
+ "--render-stop",
583
+ "--commit-start-16",
584
+ "--react-version-<filtered-version>",
585
+ "--profiler-version-1",
586
+ "--react-internal-module-start-<filtered-file-system-path>",
587
+ "--react-internal-module-stop-<filtered-file-system-path>",
588
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
589
+ "--layout-effects-start-16",
590
+ "--layout-effects-stop",
591
+ "--commit-stop",
592
+ ]
593
+ `);
594
+ });
595
+
596
+ it('should mark cascading layout updates', () => {
597
+ function Example() {
598
+ const [didMount, setDidMount] = React.useState(false);
599
+ React.useLayoutEffect(() => {
600
+ setDidMount(true);
601
+ }, []);
602
+ return didMount;
603
+ }
604
+
605
+ renderRootHelper(<Example />);
606
+
607
+ expect(clearedMarks).toMatchInlineSnapshot(`
608
+ Array [
609
+ "--schedule-render-16",
610
+ ]
611
+ `);
612
+
613
+ clearPendingMarks();
614
+
615
+ expect(Scheduler).toFlushUntilNextPaint([]);
616
+
617
+ expect(clearedMarks).toMatchInlineSnapshot(`
618
+ Array [
619
+ "--render-start-16",
620
+ "--component-render-start-Example",
621
+ "--component-render-stop",
622
+ "--render-stop",
623
+ "--commit-start-16",
624
+ "--react-version-<filtered-version>",
625
+ "--profiler-version-1",
626
+ "--react-internal-module-start-<filtered-file-system-path>",
627
+ "--react-internal-module-stop-<filtered-file-system-path>",
628
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
629
+ "--layout-effects-start-16",
630
+ "--component-layout-effect-mount-start-Example",
631
+ "--schedule-state-update-1-Example",
632
+ "--component-layout-effect-mount-stop",
633
+ "--layout-effects-stop",
634
+ "--render-start-1",
635
+ "--component-render-start-Example",
636
+ "--component-render-stop",
637
+ "--render-stop",
638
+ "--commit-start-1",
639
+ "--react-version-<filtered-version>",
640
+ "--profiler-version-1",
641
+ "--react-internal-module-start-<filtered-file-system-path>",
642
+ "--react-internal-module-stop-<filtered-file-system-path>",
643
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
644
+ "--commit-stop",
645
+ "--commit-stop",
646
+ ]
647
+ `);
648
+ });
649
+
650
+ // This test is coupled to lane implementation details, so I'm disabling it in
651
+ // the new fork until it stabilizes so we don't have to repeatedly update it.
652
+ it('should mark cascading passive updates', () => {
653
+ function Example() {
654
+ const [didMount, setDidMount] = React.useState(false);
655
+ React.useEffect(() => {
656
+ setDidMount(true);
657
+ }, []);
658
+ return didMount;
659
+ }
660
+
661
+ renderRootHelper(<Example />);
662
+
663
+ expect(Scheduler).toFlushAndYield([]);
664
+
665
+ expect(clearedMarks).toMatchInlineSnapshot(`
666
+ Array [
667
+ "--schedule-render-16",
668
+ "--render-start-16",
669
+ "--component-render-start-Example",
670
+ "--component-render-stop",
671
+ "--render-stop",
672
+ "--commit-start-16",
673
+ "--react-version-<filtered-version>",
674
+ "--profiler-version-1",
675
+ "--react-internal-module-start-<filtered-file-system-path>",
676
+ "--react-internal-module-stop-<filtered-file-system-path>",
677
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
678
+ "--layout-effects-start-16",
679
+ "--layout-effects-stop",
680
+ "--commit-stop",
681
+ "--passive-effects-start-16",
682
+ "--component-passive-effect-mount-start-Example",
683
+ "--schedule-state-update-16-Example",
684
+ "--component-passive-effect-mount-stop",
685
+ "--passive-effects-stop",
686
+ "--render-start-16",
687
+ "--component-render-start-Example",
688
+ "--component-render-stop",
689
+ "--render-stop",
690
+ "--commit-start-16",
691
+ "--react-version-<filtered-version>",
692
+ "--profiler-version-1",
693
+ "--react-internal-module-start-<filtered-file-system-path>",
694
+ "--react-internal-module-stop-<filtered-file-system-path>",
695
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
696
+ "--commit-stop",
697
+ ]
698
+ `);
699
+ });
700
+
701
+ it('should mark render phase updates', () => {
702
+ function Example() {
703
+ const [didRender, setDidRender] = React.useState(false);
704
+ if (!didRender) {
705
+ setDidRender(true);
706
+ }
707
+ return didRender;
708
+ }
709
+
710
+ renderRootHelper(<Example />);
711
+
712
+ expect(Scheduler).toFlushAndYield([]);
713
+
714
+ expect(clearedMarks).toMatchInlineSnapshot(`
715
+ Array [
716
+ "--schedule-render-16",
717
+ "--render-start-16",
718
+ "--component-render-start-Example",
719
+ "--schedule-state-update-16-Example",
720
+ "--component-render-stop",
721
+ "--render-stop",
722
+ "--commit-start-16",
723
+ "--react-version-<filtered-version>",
724
+ "--profiler-version-1",
725
+ "--react-internal-module-start-<filtered-file-system-path>",
726
+ "--react-internal-module-stop-<filtered-file-system-path>",
727
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
728
+ "--layout-effects-start-16",
729
+ "--layout-effects-stop",
730
+ "--commit-stop",
731
+ ]
732
+ `);
733
+ });
734
+
735
+ it('should mark sync render that throws', async () => {
736
+ spyOn(console, 'error');
737
+
738
+ class ErrorBoundary extends React.Component {
739
+ state = {error: null};
740
+ componentDidCatch(error) {
741
+ this.setState({error});
742
+ }
743
+ render() {
744
+ if (this.state.error) {
745
+ return null;
746
+ }
747
+ return this.props.children;
748
+ }
749
+ }
750
+
751
+ function ExampleThatThrows() {
752
+ throw Error('Expected error');
753
+ }
754
+
755
+ renderHelper(
756
+ <ErrorBoundary>
757
+ <ExampleThatThrows />
758
+ </ErrorBoundary>,
759
+ );
760
+
761
+ expect(clearedMarks).toMatchInlineSnapshot(`
762
+ Array [
763
+ "--schedule-render-1",
764
+ "--render-start-1",
765
+ "--component-render-start-ErrorBoundary",
766
+ "--component-render-stop",
767
+ "--component-render-start-ExampleThatThrows",
768
+ "--component-render-start-ExampleThatThrows",
769
+ "--component-render-stop",
770
+ "--error-ExampleThatThrows-mount-Expected error",
771
+ "--render-stop",
772
+ "--commit-start-1",
773
+ "--react-version-<filtered-version>",
774
+ "--profiler-version-1",
775
+ "--react-internal-module-start-<filtered-file-system-path>",
776
+ "--react-internal-module-stop-<filtered-file-system-path>",
777
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
778
+ "--layout-effects-start-1",
779
+ "--schedule-state-update-1-ErrorBoundary",
780
+ "--layout-effects-stop",
781
+ "--commit-stop",
782
+ "--render-start-1",
783
+ "--component-render-start-ErrorBoundary",
784
+ "--component-render-stop",
785
+ "--render-stop",
786
+ "--commit-start-1",
787
+ "--react-version-<filtered-version>",
788
+ "--profiler-version-1",
789
+ "--react-internal-module-start-<filtered-file-system-path>",
790
+ "--react-internal-module-stop-<filtered-file-system-path>",
791
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
792
+ "--commit-stop",
793
+ ]
794
+ `);
795
+ });
796
+
797
+ it('should mark concurrent render that throws', async () => {
798
+ spyOn(console, 'error');
799
+
800
+ class ErrorBoundary extends React.Component {
801
+ state = {error: null};
802
+ componentDidCatch(error) {
803
+ this.setState({error});
804
+ }
805
+ render() {
806
+ if (this.state.error) {
807
+ return null;
808
+ }
809
+ return this.props.children;
810
+ }
811
+ }
812
+
813
+ function ExampleThatThrows() {
814
+ // eslint-disable-next-line no-throw-literal
815
+ throw 'Expected error';
816
+ }
817
+
818
+ renderRootHelper(
819
+ <ErrorBoundary>
820
+ <ExampleThatThrows />
821
+ </ErrorBoundary>,
822
+ );
823
+
824
+ expect(clearedMarks).toMatchInlineSnapshot(`
825
+ Array [
826
+ "--schedule-render-16",
827
+ ]
828
+ `);
829
+
830
+ clearPendingMarks();
831
+
832
+ expect(Scheduler).toFlushUntilNextPaint([]);
833
+
834
+ expect(clearedMarks).toMatchInlineSnapshot(`
835
+ Array [
836
+ "--render-start-16",
837
+ "--component-render-start-ErrorBoundary",
838
+ "--component-render-stop",
839
+ "--component-render-start-ExampleThatThrows",
840
+ "--component-render-start-ExampleThatThrows",
841
+ "--component-render-stop",
842
+ "--error-ExampleThatThrows-mount-Expected error",
843
+ "--render-stop",
844
+ "--render-start-16",
845
+ "--component-render-start-ErrorBoundary",
846
+ "--component-render-stop",
847
+ "--component-render-start-ExampleThatThrows",
848
+ "--component-render-start-ExampleThatThrows",
849
+ "--component-render-stop",
850
+ "--error-ExampleThatThrows-mount-Expected error",
851
+ "--render-stop",
852
+ "--commit-start-16",
853
+ "--react-version-<filtered-version>",
854
+ "--profiler-version-1",
855
+ "--react-internal-module-start-<filtered-file-system-path>",
856
+ "--react-internal-module-stop-<filtered-file-system-path>",
857
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
858
+ "--layout-effects-start-16",
859
+ "--schedule-state-update-1-ErrorBoundary",
860
+ "--layout-effects-stop",
861
+ "--render-start-1",
862
+ "--component-render-start-ErrorBoundary",
863
+ "--component-render-stop",
864
+ "--render-stop",
865
+ "--commit-start-1",
866
+ "--react-version-<filtered-version>",
867
+ "--profiler-version-1",
868
+ "--react-internal-module-start-<filtered-file-system-path>",
869
+ "--react-internal-module-stop-<filtered-file-system-path>",
870
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
871
+ "--commit-stop",
872
+ "--commit-stop",
873
+ ]
874
+ `);
875
+ });
876
+
877
+ it('should mark passive and layout effects', async () => {
878
+ function ComponentWithEffects() {
879
+ React.useLayoutEffect(() => {
880
+ Scheduler.unstable_yieldValue('layout 1 mount');
881
+ return () => {
882
+ Scheduler.unstable_yieldValue('layout 1 unmount');
883
+ };
884
+ }, []);
885
+
886
+ React.useEffect(() => {
887
+ Scheduler.unstable_yieldValue('passive 1 mount');
888
+ return () => {
889
+ Scheduler.unstable_yieldValue('passive 1 unmount');
890
+ };
891
+ }, []);
892
+
893
+ React.useLayoutEffect(() => {
894
+ Scheduler.unstable_yieldValue('layout 2 mount');
895
+ return () => {
896
+ Scheduler.unstable_yieldValue('layout 2 unmount');
897
+ };
898
+ }, []);
899
+
900
+ React.useEffect(() => {
901
+ Scheduler.unstable_yieldValue('passive 2 mount');
902
+ return () => {
903
+ Scheduler.unstable_yieldValue('passive 2 unmount');
904
+ };
905
+ }, []);
906
+
907
+ React.useEffect(() => {
908
+ Scheduler.unstable_yieldValue('passive 3 mount');
909
+ return () => {
910
+ Scheduler.unstable_yieldValue('passive 3 unmount');
911
+ };
912
+ }, []);
913
+
914
+ return null;
915
+ }
916
+
917
+ const unmount = renderRootHelper(<ComponentWithEffects />);
918
+
919
+ expect(Scheduler).toFlushUntilNextPaint([
920
+ 'layout 1 mount',
921
+ 'layout 2 mount',
922
+ ]);
923
+
924
+ expect(clearedMarks).toMatchInlineSnapshot(`
925
+ Array [
926
+ "--schedule-render-16",
927
+ "--render-start-16",
928
+ "--component-render-start-ComponentWithEffects",
929
+ "--component-render-stop",
930
+ "--render-stop",
931
+ "--commit-start-16",
932
+ "--react-version-<filtered-version>",
933
+ "--profiler-version-1",
934
+ "--react-internal-module-start-<filtered-file-system-path>",
935
+ "--react-internal-module-stop-<filtered-file-system-path>",
936
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
937
+ "--layout-effects-start-16",
938
+ "--component-layout-effect-mount-start-ComponentWithEffects",
939
+ "--component-layout-effect-mount-stop",
940
+ "--component-layout-effect-mount-start-ComponentWithEffects",
941
+ "--component-layout-effect-mount-stop",
942
+ "--layout-effects-stop",
943
+ "--commit-stop",
944
+ ]
945
+ `);
946
+
947
+ clearPendingMarks();
948
+
949
+ expect(Scheduler).toFlushAndYield([
950
+ 'passive 1 mount',
951
+ 'passive 2 mount',
952
+ 'passive 3 mount',
953
+ ]);
954
+
955
+ expect(clearedMarks).toMatchInlineSnapshot(`
956
+ Array [
957
+ "--passive-effects-start-16",
958
+ "--component-passive-effect-mount-start-ComponentWithEffects",
959
+ "--component-passive-effect-mount-stop",
960
+ "--component-passive-effect-mount-start-ComponentWithEffects",
961
+ "--component-passive-effect-mount-stop",
962
+ "--component-passive-effect-mount-start-ComponentWithEffects",
963
+ "--component-passive-effect-mount-stop",
964
+ "--passive-effects-stop",
965
+ ]
966
+ `);
967
+
968
+ clearPendingMarks();
969
+
970
+ expect(Scheduler).toFlushAndYield([]);
971
+
972
+ unmount();
973
+
974
+ expect(Scheduler).toHaveYielded([
975
+ 'layout 1 unmount',
976
+ 'layout 2 unmount',
977
+ 'passive 1 unmount',
978
+ 'passive 2 unmount',
979
+ 'passive 3 unmount',
980
+ ]);
981
+
982
+ expect(clearedMarks).toMatchInlineSnapshot(`
983
+ Array [
984
+ "--schedule-render-1",
985
+ "--render-start-1",
986
+ "--render-stop",
987
+ "--commit-start-1",
988
+ "--react-version-<filtered-version>",
989
+ "--profiler-version-1",
990
+ "--react-internal-module-start-<filtered-file-system-path>",
991
+ "--react-internal-module-stop-<filtered-file-system-path>",
992
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
993
+ "--component-layout-effect-unmount-start-ComponentWithEffects",
994
+ "--component-layout-effect-unmount-stop",
995
+ "--component-layout-effect-unmount-start-ComponentWithEffects",
996
+ "--component-layout-effect-unmount-stop",
997
+ "--layout-effects-start-1",
998
+ "--layout-effects-stop",
999
+ "--passive-effects-start-1",
1000
+ "--component-passive-effect-unmount-start-ComponentWithEffects",
1001
+ "--component-passive-effect-unmount-stop",
1002
+ "--component-passive-effect-unmount-start-ComponentWithEffects",
1003
+ "--component-passive-effect-unmount-stop",
1004
+ "--component-passive-effect-unmount-start-ComponentWithEffects",
1005
+ "--component-passive-effect-unmount-stop",
1006
+ "--passive-effects-stop",
1007
+ "--commit-stop",
1008
+ ]
1009
+ `);
1010
+ });
1011
+
1012
+ describe('lane labels', () => {
1013
+ it('regression test SyncLane', () => {
1014
+ renderHelper(<div />);
1015
+
1016
+ expect(clearedMarks).toMatchInlineSnapshot(`
1017
+ Array [
1018
+ "--schedule-render-1",
1019
+ "--render-start-1",
1020
+ "--render-stop",
1021
+ "--commit-start-1",
1022
+ "--react-version-<filtered-version>",
1023
+ "--profiler-version-1",
1024
+ "--react-internal-module-start-<filtered-file-system-path>",
1025
+ "--react-internal-module-stop-<filtered-file-system-path>",
1026
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
1027
+ "--layout-effects-start-1",
1028
+ "--layout-effects-stop",
1029
+ "--commit-stop",
1030
+ ]
1031
+ `);
1032
+ });
1033
+
1034
+ it('regression test DefaultLane', () => {
1035
+ renderRootHelper(<div />);
1036
+ expect(clearedMarks).toMatchInlineSnapshot(`
1037
+ Array [
1038
+ "--schedule-render-16",
1039
+ ]
1040
+ `);
1041
+ });
1042
+
1043
+ it('regression test InputDiscreteLane', async () => {
1044
+ const targetRef = React.createRef(null);
1045
+
1046
+ function App() {
1047
+ const [count, setCount] = React.useState(0);
1048
+ const handleClick = () => {
1049
+ setCount(count + 1);
1050
+ };
1051
+ return <button ref={targetRef} onClick={handleClick} />;
1052
+ }
1053
+
1054
+ renderRootHelper(<App />);
1055
+ expect(Scheduler).toFlushAndYield([]);
1056
+
1057
+ clearedMarks.splice(0);
1058
+
1059
+ targetRef.current.click();
1060
+
1061
+ // Wait a frame, for React to process the "click" update.
1062
+ await Promise.resolve();
1063
+
1064
+ expect(clearedMarks).toMatchInlineSnapshot(`
1065
+ Array [
1066
+ "--schedule-state-update-1-App",
1067
+ "--render-start-1",
1068
+ "--component-render-start-App",
1069
+ "--component-render-stop",
1070
+ "--render-stop",
1071
+ "--commit-start-1",
1072
+ "--react-version-<filtered-version>",
1073
+ "--profiler-version-1",
1074
+ "--react-internal-module-start-<filtered-file-system-path>",
1075
+ "--react-internal-module-stop-<filtered-file-system-path>",
1076
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
1077
+ "--layout-effects-start-1",
1078
+ "--layout-effects-stop",
1079
+ "--commit-stop",
1080
+ ]
1081
+ `);
1082
+ });
1083
+
1084
+ it('regression test InputContinuousLane', async () => {
1085
+ const targetRef = React.createRef(null);
1086
+
1087
+ function App() {
1088
+ const [count, setCount] = React.useState(0);
1089
+ const handleMouseOver = () => setCount(count + 1);
1090
+ return <div ref={targetRef} onMouseOver={handleMouseOver} />;
1091
+ }
1092
+
1093
+ renderRootHelper(<App />);
1094
+ expect(Scheduler).toFlushAndYield([]);
1095
+
1096
+ clearedMarks.splice(0);
1097
+
1098
+ const event = document.createEvent('MouseEvents');
1099
+ event.initEvent('mouseover', true, true);
1100
+ dispatchAndSetCurrentEvent(targetRef.current, event);
1101
+
1102
+ expect(Scheduler).toFlushAndYield([]);
1103
+
1104
+ expect(clearedMarks).toMatchInlineSnapshot(`
1105
+ Array [
1106
+ "--schedule-state-update-4-App",
1107
+ "--render-start-4",
1108
+ "--component-render-start-App",
1109
+ "--component-render-stop",
1110
+ "--render-stop",
1111
+ "--commit-start-4",
1112
+ "--react-version-<filtered-version>",
1113
+ "--profiler-version-1",
1114
+ "--react-internal-module-start-<filtered-file-system-path>",
1115
+ "--react-internal-module-stop-<filtered-file-system-path>",
1116
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
1117
+ "--layout-effects-start-4",
1118
+ "--layout-effects-stop",
1119
+ "--commit-stop",
1120
+ ]
1121
+ `);
1122
+ });
1123
+ });
1124
+});