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
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+'use strict';
12
+
13
+function normalizeCodeLocInfo(str) {
14
+ return (
15
+ str &&
16
+ str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
17
+ return '\n in ' + name + ' (at **)';
18
+ })
19
+ );
20
+}
21
+
22
+describe('SchedulingProfiler', () => {
23
+ let React;
24
+ let ReactTestRenderer;
25
+ let ReactNoop;
26
+ let Scheduler;
27
+
28
+ let marks;
29
+
30
+ function createUserTimingPolyfill() {
31
+ // This is not a true polyfill, but it gives us enough to capture marks.
32
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
33
+ return {
34
+ mark(markName) {
35
+ marks.push(markName);
36
+ },
37
+ };
38
+ }
39
+
40
+ beforeEach(() => {
41
+ jest.resetModules();
42
+ global.performance = createUserTimingPolyfill();
43
+
44
+ React = require('react');
45
+
46
+ // ReactNoop must be imported after ReactTestRenderer!
47
+ ReactTestRenderer = require('react-test-renderer');
48
+ ReactNoop = require('react-noop-renderer');
49
+
50
+ Scheduler = require('scheduler');
51
+
52
+ marks = [];
53
+ });
54
+
55
+ afterEach(() => {
56
+ delete global.performance;
57
+ });
58
+
59
+ // @gate !enableSchedulingProfiler
60
+ it('should not mark if enableSchedulingProfiler is false', () => {
61
+ ReactTestRenderer.create(<div />);
62
+ expect(marks).toEqual([]);
63
+ });
64
+
65
+ // @gate enableSchedulingProfiler
66
+ it('should mark sync render without suspends or state updates', () => {
67
+ ReactTestRenderer.create(<div />);
68
+
69
+ expect(marks).toEqual([
70
+ '--schedule-render-1',
71
+ '--render-start-1',
72
+ '--render-stop',
73
+ '--commit-start-1',
74
+ '--layout-effects-start-1',
75
+ '--layout-effects-stop',
76
+ '--commit-stop',
77
+ ]);
78
+ });
79
+
80
+ // @gate enableSchedulingProfiler
81
+ it('should mark concurrent render without suspends or state updates', () => {
82
+ ReactTestRenderer.create(<div />, {unstable_isConcurrent: true});
83
+
84
+ expect(marks).toEqual(['--schedule-render-512']);
85
+
86
+ marks.splice(0);
87
+
88
+ expect(Scheduler).toFlushUntilNextPaint([]);
89
+
90
+ expect(marks).toEqual([
91
+ '--render-start-512',
92
+ '--render-stop',
93
+ '--commit-start-512',
94
+ '--layout-effects-start-512',
95
+ '--layout-effects-stop',
96
+ '--commit-stop',
97
+ ]);
98
+ });
99
+
100
+ // @gate enableSchedulingProfiler
101
+ it('should mark render yields', async () => {
102
+ function Bar() {
103
+ Scheduler.unstable_yieldValue('Bar');
104
+ return null;
105
+ }
106
+
107
+ function Foo() {
108
+ Scheduler.unstable_yieldValue('Foo');
109
+ return <Bar />;
110
+ }
111
+
112
+ ReactNoop.render(<Foo />);
113
+ // Do one step of work.
114
+ expect(ReactNoop.flushNextYield()).toEqual(['Foo']);
115
+
116
+ expect(marks).toEqual([
117
+ '--schedule-render-512',
118
+ '--render-start-512',
119
+ '--render-yield',
120
+ ]);
121
+ });
122
+
123
+ // @gate enableSchedulingProfiler
124
+ it('should mark sync render with suspense that resolves', async () => {
125
+ const fakeSuspensePromise = Promise.resolve(true);
126
+ function Example() {
127
+ throw fakeSuspensePromise;
128
+ }
129
+
130
+ ReactTestRenderer.create(
131
+ <React.Suspense fallback={null}>
132
+ <Example />
133
+ </React.Suspense>,
134
+ );
135
+
136
+ expect(marks).toEqual([
137
+ '--schedule-render-1',
138
+ '--render-start-1',
139
+ '--suspense-suspend-0-Example-\n at Example\n at Suspense',
140
+ '--render-stop',
141
+ '--commit-start-1',
142
+ '--layout-effects-start-1',
143
+ '--layout-effects-stop',
144
+ '--commit-stop',
145
+ ]);
146
+
147
+ marks.splice(0);
148
+
149
+ await fakeSuspensePromise;
150
+ expect(marks).toEqual([
151
+ '--suspense-resolved-0-Example-\n at Example\n at Suspense',
152
+ ]);
153
+ });
154
+
155
+ // @gate enableSchedulingProfiler
156
+ it('should mark sync render with suspense that rejects', async () => {
157
+ const fakeSuspensePromise = Promise.reject(new Error('error'));
158
+ function Example() {
159
+ throw fakeSuspensePromise;
160
+ }
161
+
162
+ ReactTestRenderer.create(
163
+ <React.Suspense fallback={null}>
164
+ <Example />
165
+ </React.Suspense>,
166
+ );
167
+
168
+ expect(marks).toEqual([
169
+ '--schedule-render-1',
170
+ '--render-start-1',
171
+ '--suspense-suspend-0-Example-\n at Example\n at Suspense',
172
+ '--render-stop',
173
+ '--commit-start-1',
174
+ '--layout-effects-start-1',
175
+ '--layout-effects-stop',
176
+ '--commit-stop',
177
+ ]);
178
+
179
+ marks.splice(0);
180
+
181
+ await expect(fakeSuspensePromise).rejects.toThrow();
182
+ expect(marks).toEqual([
183
+ '--suspense-rejected-0-Example-\n at Example\n at Suspense',
184
+ ]);
185
+ });
186
+
187
+ // @gate enableSchedulingProfiler
188
+ it('should mark concurrent render with suspense that resolves', async () => {
189
+ const fakeSuspensePromise = Promise.resolve(true);
190
+ function Example() {
191
+ throw fakeSuspensePromise;
192
+ }
193
+
194
+ ReactTestRenderer.create(
195
+ <React.Suspense fallback={null}>
196
+ <Example />
197
+ </React.Suspense>,
198
+ {unstable_isConcurrent: true},
199
+ );
200
+
201
+ expect(marks).toEqual(['--schedule-render-512']);
202
+
203
+ marks.splice(0);
204
+
205
+ expect(Scheduler).toFlushUntilNextPaint([]);
206
+
207
+ expect(marks).toEqual([
208
+ '--render-start-512',
209
+ '--suspense-suspend-0-Example-\n at Example\n at Suspense',
210
+ '--render-stop',
211
+ '--commit-start-512',
212
+ '--layout-effects-start-512',
213
+ '--layout-effects-stop',
214
+ '--commit-stop',
215
+ ]);
216
+
217
+ marks.splice(0);
218
+
219
+ await fakeSuspensePromise;
220
+ expect(marks).toEqual([
221
+ '--suspense-resolved-0-Example-\n at Example\n at Suspense',
222
+ ]);
223
+ });
224
+
225
+ // @gate enableSchedulingProfiler
226
+ it('should mark concurrent render with suspense that rejects', async () => {
227
+ const fakeSuspensePromise = Promise.reject(new Error('error'));
228
+ function Example() {
229
+ throw fakeSuspensePromise;
230
+ }
231
+
232
+ ReactTestRenderer.create(
233
+ <React.Suspense fallback={null}>
234
+ <Example />
235
+ </React.Suspense>,
236
+ {unstable_isConcurrent: true},
237
+ );
238
+
239
+ expect(marks).toEqual(['--schedule-render-512']);
240
+
241
+ marks.splice(0);
242
+
243
+ expect(Scheduler).toFlushUntilNextPaint([]);
244
+
245
+ expect(marks).toEqual([
246
+ '--render-start-512',
247
+ '--suspense-suspend-0-Example-\n at Example\n at Suspense',
248
+ '--render-stop',
249
+ '--commit-start-512',
250
+ '--layout-effects-start-512',
251
+ '--layout-effects-stop',
252
+ '--commit-stop',
253
+ ]);
254
+
255
+ marks.splice(0);
256
+
257
+ await expect(fakeSuspensePromise).rejects.toThrow();
258
+ expect(marks).toEqual([
259
+ '--suspense-rejected-0-Example-\n at Example\n at Suspense',
260
+ ]);
261
+ });
262
+
263
+ // @gate enableSchedulingProfiler
264
+ it('should mark cascading class component state updates', () => {
265
+ class Example extends React.Component {
266
+ state = {didMount: false};
267
+ componentDidMount() {
268
+ this.setState({didMount: true});
269
+ }
270
+ render() {
271
+ return null;
272
+ }
273
+ }
274
+
275
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
276
+
277
+ expect(marks).toEqual(['--schedule-render-512']);
278
+
279
+ marks.splice(0);
280
+
281
+ expect(Scheduler).toFlushUntilNextPaint([]);
282
+
283
+ expect(marks.map(normalizeCodeLocInfo)).toEqual([
284
+ '--render-start-512',
285
+ '--render-stop',
286
+ '--commit-start-512',
287
+ '--layout-effects-start-512',
288
+ '--schedule-state-update-1-Example-\n in Example (at **)',
289
+ '--layout-effects-stop',
290
+ '--render-start-1',
291
+ '--render-stop',
292
+ '--commit-start-1',
293
+ '--commit-stop',
294
+ '--commit-stop',
295
+ ]);
296
+ });
297
+
298
+ // @gate enableSchedulingProfiler
299
+ it('should mark cascading class component force updates', () => {
300
+ class Example extends React.Component {
301
+ componentDidMount() {
302
+ this.forceUpdate();
303
+ }
304
+ render() {
305
+ return null;
306
+ }
307
+ }
308
+
309
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
310
+
311
+ expect(marks).toEqual(['--schedule-render-512']);
312
+
313
+ marks.splice(0);
314
+
315
+ expect(Scheduler).toFlushUntilNextPaint([]);
316
+
317
+ expect(marks.map(normalizeCodeLocInfo)).toEqual([
318
+ '--render-start-512',
319
+ '--render-stop',
320
+ '--commit-start-512',
321
+ '--layout-effects-start-512',
322
+ '--schedule-forced-update-1-Example-\n in Example (at **)',
323
+ '--layout-effects-stop',
324
+ '--render-start-1',
325
+ '--render-stop',
326
+ '--commit-start-1',
327
+ '--commit-stop',
328
+ '--commit-stop',
329
+ ]);
330
+ });
331
+
332
+ // @gate enableSchedulingProfiler
333
+ it('should mark render phase state updates for class component', () => {
334
+ class Example extends React.Component {
335
+ state = {didRender: false};
336
+ render() {
337
+ if (this.state.didRender === false) {
338
+ this.setState({didRender: true});
339
+ }
340
+ return null;
341
+ }
342
+ }
343
+
344
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
345
+
346
+ expect(marks).toEqual(['--schedule-render-512']);
347
+
348
+ marks.splice(0);
349
+
350
+ expect(() => {
351
+ expect(Scheduler).toFlushUntilNextPaint([]);
352
+ }).toErrorDev('Cannot update during an existing state transition');
353
+
354
+ expect(marks.map(normalizeCodeLocInfo)).toContain(
355
+ '--schedule-state-update-1024-Example-\n in Example (at **)',
356
+ );
357
+ });
358
+
359
+ // @gate enableSchedulingProfiler
360
+ it('should mark render phase force updates for class component', () => {
361
+ class Example extends React.Component {
362
+ state = {didRender: false};
363
+ render() {
364
+ if (this.state.didRender === false) {
365
+ this.forceUpdate(() => this.setState({didRender: true}));
366
+ }
367
+ return null;
368
+ }
369
+ }
370
+
371
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
372
+
373
+ expect(marks).toEqual(['--schedule-render-512']);
374
+
375
+ marks.splice(0);
376
+
377
+ expect(() => {
378
+ expect(Scheduler).toFlushUntilNextPaint([]);
379
+ }).toErrorDev('Cannot update during an existing state transition');
380
+
381
+ expect(marks.map(normalizeCodeLocInfo)).toContain(
382
+ '--schedule-forced-update-1024-Example-\n in Example (at **)',
383
+ );
384
+ });
385
+
386
+ // @gate enableSchedulingProfiler
387
+ it('should mark cascading layout updates', () => {
388
+ function Example() {
389
+ const [didMount, setDidMount] = React.useState(false);
390
+ React.useLayoutEffect(() => {
391
+ setDidMount(true);
392
+ }, []);
393
+ return didMount;
394
+ }
395
+
396
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
397
+
398
+ expect(marks).toEqual(['--schedule-render-512']);
399
+
400
+ marks.splice(0);
401
+
402
+ expect(Scheduler).toFlushUntilNextPaint([]);
403
+
404
+ expect(marks.map(normalizeCodeLocInfo)).toEqual([
405
+ '--render-start-512',
406
+ '--render-stop',
407
+ '--commit-start-512',
408
+ '--layout-effects-start-512',
409
+ '--schedule-state-update-1-Example-\n in Example (at **)',
410
+ '--layout-effects-stop',
411
+ '--render-start-1',
412
+ '--render-stop',
413
+ '--commit-start-1',
414
+ '--commit-stop',
415
+ '--commit-stop',
416
+ ]);
417
+ });
418
+
419
+ // @gate enableSchedulingProfiler
420
+ it('should mark cascading passive updates', () => {
421
+ function Example() {
422
+ const [didMount, setDidMount] = React.useState(false);
423
+ React.useEffect(() => {
424
+ setDidMount(true);
425
+ }, []);
426
+ return didMount;
427
+ }
428
+
429
+ ReactTestRenderer.act(() => {
430
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
431
+ });
432
+ expect(marks.map(normalizeCodeLocInfo)).toEqual([
433
+ '--schedule-render-512',
434
+ '--render-start-512',
435
+ '--render-stop',
436
+ '--commit-start-512',
437
+ '--layout-effects-start-512',
438
+ '--layout-effects-stop',
439
+ '--commit-stop',
440
+ '--passive-effects-start-512',
441
+ '--schedule-state-update-1024-Example-\n in Example (at **)',
442
+ '--passive-effects-stop',
443
+ '--render-start-1024',
444
+ '--render-stop',
445
+ '--commit-start-1024',
446
+ '--commit-stop',
447
+ ]);
448
+ });
449
+
450
+ // @gate enableSchedulingProfiler
451
+ it('should mark render phase updates', () => {
452
+ function Example() {
453
+ const [didRender, setDidRender] = React.useState(false);
454
+ if (!didRender) {
455
+ setDidRender(true);
456
+ }
457
+ return didRender;
458
+ }
459
+
460
+ ReactTestRenderer.act(() => {
461
+ ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
462
+ });
463
+
464
+ expect(marks.map(normalizeCodeLocInfo)).toContain(
465
+ '--schedule-state-update-1024-Example-\n in Example (at **)',
466
+ );
467
+ });
468
+});