main
js 589 lines 14.3 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 let React;
12 let ReactNoop;
13 let Scheduler;
14 let act;
15 let useEffect;
16
17 describe('ReactPerformanceTracks', () => {
18 const performanceMeasureCalls = [];
19
20 beforeEach(() => {
21 performanceMeasureCalls.length = 0;
22 jest
23 .spyOn(performance, 'measure')
24 .mockImplementation((measureName, reusableOptions) => {
25 performanceMeasureCalls.push([
26 measureName,
27 {
28 // React will mutate the options it passes to performance.measure.
29 ...reusableOptions,
30 },
31 ]);
32 });
33 console.timeStamp = () => {};
34 jest.spyOn(console, 'timeStamp').mockImplementation(() => {});
35
36 jest.resetModules();
37
38 React = require('react');
39 ReactNoop = require('react-noop-renderer');
40 Scheduler = require('scheduler');
41 act = require('internal-test-utils').act;
42 useEffect = React.useEffect;
43 });
44
45 function getConsoleTimestampEntries() {
46 try {
47 return console.timeStamp.mock.calls.filter(call => {
48 const [, startTime, endTime] = call;
49
50 const isRegisterTrackCall = startTime !== 0.003 && endTime !== 0.003;
51 return isRegisterTrackCall;
52 });
53 } finally {
54 console.timeStamp.mockClear();
55 }
56 }
57
58 // @gate __DEV__ && enableComponentPerformanceTrack
59 it('shows a hint if an update is triggered by a deeply equal object', async () => {
60 const App = function App({items}) {
61 Scheduler.unstable_advanceTime(10);
62 useEffect(() => {}, [items]);
63 };
64
65 Scheduler.unstable_advanceTime(1);
66 const items = ['one', 'two'];
67 await act(() => {
68 ReactNoop.render(<App items={items} />);
69 });
70
71 expect(performanceMeasureCalls).toEqual([
72 [
73 'Mount',
74 {
75 detail: {
76 devtools: {
77 color: 'warning',
78 properties: null,
79 tooltipText: 'Mount',
80 track: 'Components ⚛',
81 },
82 },
83 end: 11,
84 start: 1,
85 },
86 ],
87 ]);
88 performanceMeasureCalls.length = 0;
89
90 Scheduler.unstable_advanceTime(10);
91 await act(() => {
92 ReactNoop.render(<App items={items.concat('4')} />);
93 });
94
95 expect(performanceMeasureCalls).toEqual([
96 [
97 '​App',
98 {
99 detail: {
100 devtools: {
101 color: 'primary-dark',
102 properties: [
103 ['Changed Props', ''],
104 ['  items', 'Array'],
105 ['+   2', ''],
106 ],
107 tooltipText: 'App',
108 track: 'Components ⚛',
109 },
110 },
111 end: 31,
112 start: 21,
113 },
114 ],
115 ]);
116 });
117
118 // @gate __DEV__ && enableComponentPerformanceTrack
119 it('bails out of diffing wide arrays', async () => {
120 const App = function App({items}) {
121 Scheduler.unstable_advanceTime(10);
122 React.useEffect(() => {}, [items]);
123 };
124
125 Scheduler.unstable_advanceTime(1);
126 const items = Array.from({length: 1000}, (_, i) => i);
127 await act(() => {
128 ReactNoop.render(<App items={items} />);
129 });
130
131 expect(performanceMeasureCalls).toEqual([
132 [
133 'Mount',
134 {
135 detail: {
136 devtools: {
137 color: 'warning',
138 properties: null,
139 tooltipText: 'Mount',
140 track: 'Components ⚛',
141 },
142 },
143 end: 11,
144 start: 1,
145 },
146 ],
147 ]);
148 performanceMeasureCalls.length = 0;
149
150 Scheduler.unstable_advanceTime(10);
151 await act(() => {
152 ReactNoop.render(<App items={items.concat('-1')} />);
153 });
154
155 expect(performanceMeasureCalls).toEqual([
156 [
157 '​App',
158 {
159 detail: {
160 devtools: {
161 color: 'primary-dark',
162 properties: [
163 ['Changed Props', ''],
164 ['  items', 'Array'],
165 [
166 'Previous object has more than 100 properties. React will not attempt to diff objects with too many properties.',
167 '',
168 ],
169 [
170 'Next object has more than 100 properties. React will not attempt to diff objects with too many properties.',
171 '',
172 ],
173 ],
174 tooltipText: 'App',
175 track: 'Components ⚛',
176 },
177 },
178 end: 31,
179 start: 21,
180 },
181 ],
182 ]);
183 });
184
185 // @gate __DEV__ && enableComponentPerformanceTrack
186 it('does not show all properties of wide objects', async () => {
187 const App = function App({items}) {
188 Scheduler.unstable_advanceTime(10);
189 React.useEffect(() => {}, [items]);
190 };
191
192 Scheduler.unstable_advanceTime(1);
193 await act(() => {
194 ReactNoop.render(<App data={{buffer: null}} />);
195 });
196
197 expect(performanceMeasureCalls).toEqual([
198 [
199 'Mount',
200 {
201 detail: {
202 devtools: {
203 color: 'warning',
204 properties: null,
205 tooltipText: 'Mount',
206 track: 'Components ⚛',
207 },
208 },
209 end: 11,
210 start: 1,
211 },
212 ],
213 ]);
214 performanceMeasureCalls.length = 0;
215
216 Scheduler.unstable_advanceTime(10);
217
218 const bigData = new Uint8Array(1000);
219 await act(() => {
220 ReactNoop.render(<App data={{buffer: bigData}} />);
221 });
222
223 expect(performanceMeasureCalls).toEqual([
224 [
225 '​App',
226 {
227 detail: {
228 devtools: {
229 color: 'primary-dark',
230 properties: [
231 ['Changed Props', ''],
232 ['  data', ''],
233 ['-   buffer', 'null'],
234 ['+   buffer', 'Uint8Array(1000)'],
235 ],
236 tooltipText: 'App',
237 track: 'Components ⚛',
238 },
239 },
240 end: 31,
241 start: 21,
242 },
243 ],
244 ]);
245 });
246
247 // @gate __DEV__ && enableComponentPerformanceTrack
248 it('shows the type and length of typed arrays instead of enumerating them', async () => {
249 const App = function App({buffer}) {
250 Scheduler.unstable_advanceTime(10);
251 React.useEffect(() => {}, [buffer]);
252 };
253
254 Scheduler.unstable_advanceTime(1);
255 await act(() => {
256 ReactNoop.render(<App buffer={new Uint8Array(0)} />);
257 });
258
259 expect(performanceMeasureCalls).toEqual([
260 [
261 'Mount',
262 {
263 detail: {
264 devtools: {
265 color: 'warning',
266 properties: null,
267 tooltipText: 'Mount',
268 track: 'Components ⚛',
269 },
270 },
271 end: 11,
272 start: 1,
273 },
274 ],
275 ]);
276 performanceMeasureCalls.length = 0;
277
278 Scheduler.unstable_advanceTime(10);
279
280 // A typed array can hold millions of elements. React must not enumerate
281 // them element-by-element, which would freeze rendering (issue #36200).
282 await act(() => {
283 ReactNoop.render(<App buffer={new Float32Array(1000)} />);
284 });
285
286 expect(performanceMeasureCalls).toEqual([
287 [
288 '​App',
289 {
290 detail: {
291 devtools: {
292 color: 'primary-dark',
293 properties: [
294 ['Changed Props', ''],
295 ['-\xa0buffer', 'Uint8Array(0)'],
296 ['+\xa0buffer', 'Float32Array(1000)'],
297 ],
298 tooltipText: 'App',
299 track: 'Components ⚛',
300 },
301 },
302 end: 31,
303 start: 21,
304 },
305 ],
306 ]);
307 });
308
309 // @gate __DEV__ && enableComponentPerformanceTrack
310 it('includes console.timeStamp spans for Components with no prop changes', async () => {
311 function Left({value}) {
312 Scheduler.unstable_advanceTime(5000);
313 }
314 function Right() {
315 Scheduler.unstable_advanceTime(10000);
316 }
317
318 await act(() => {
319 ReactNoop.render(
320 <>
321 <Left value={1} />
322 <Right />
323 </>,
324 );
325 });
326
327 expect(performanceMeasureCalls).toEqual([
328 [
329 'Mount',
330 {
331 detail: {
332 devtools: {
333 color: 'warning',
334 properties: null,
335 tooltipText: 'Mount',
336 track: 'Components ⚛',
337 },
338 },
339 end: 5000,
340 start: 0,
341 },
342 ],
343 [
344 'Mount',
345 {
346 detail: {
347 devtools: {
348 color: 'warning',
349 properties: null,
350 tooltipText: 'Mount',
351 track: 'Components ⚛',
352 },
353 },
354 end: 15000,
355 start: 5000,
356 },
357 ],
358 ]);
359 performanceMeasureCalls.length = 0;
360 getConsoleTimestampEntries();
361
362 Scheduler.unstable_advanceTime(1000);
363
364 await act(() => {
365 ReactNoop.render(
366 <>
367 <Left value={2} />
368 <Right />
369 </>,
370 );
371 });
372
373 expect(performanceMeasureCalls).toEqual([
374 [
375 '​Left',
376 {
377 detail: {
378 devtools: {
379 color: 'error',
380 properties: [
381 ['Changed Props', ''],
382 ['- value', '1'],
383 ['+ value', '2'],
384 ],
385 tooltipText: 'Left',
386 track: 'Components ⚛',
387 },
388 },
389 end: 21000,
390 start: 16000,
391 },
392 ],
393 ]);
394 expect(getConsoleTimestampEntries()).toEqual([
395 ['Render', 16000, 31000, 'Blocking', 'Scheduler ⚛', 'primary-dark'],
396 ['Right', 21000, 31000, 'Components ⚛', undefined, 'error'],
397 ]);
398 performanceMeasureCalls.length = 0;
399 });
400
401 // @gate __DEV__ && enableComponentPerformanceTrack
402 it('can handle bigint in arrays', async () => {
403 const App = function App({numbers}) {
404 Scheduler.unstable_advanceTime(10);
405 React.useEffect(() => {}, [numbers]);
406 };
407
408 Scheduler.unstable_advanceTime(1);
409 await act(() => {
410 ReactNoop.render(
411 <App
412 data={{
413 deeply: {
414 nested: {
415 numbers: [1n],
416 },
417 },
418 }}
419 />,
420 );
421 });
422
423 expect(performanceMeasureCalls).toEqual([
424 [
425 'Mount',
426 {
427 detail: {
428 devtools: {
429 color: 'warning',
430 properties: null,
431 tooltipText: 'Mount',
432 track: 'Components ⚛',
433 },
434 },
435 end: 11,
436 start: 1,
437 },
438 ],
439 ]);
440 performanceMeasureCalls.length = 0;
441
442 Scheduler.unstable_advanceTime(10);
443
444 await act(() => {
445 ReactNoop.render(
446 <App
447 data={{
448 deeply: {
449 nested: {
450 numbers: [2n],
451 },
452 },
453 }}
454 />,
455 );
456 });
457
458 expect(performanceMeasureCalls).toEqual([
459 [
460 '​App',
461 {
462 detail: {
463 devtools: {
464 color: 'primary-dark',
465 properties: [
466 ['Changed Props', ''],
467 ['  data', ''],
468 ['    deeply', ''],
469 ['      nested', ''],
470 ['-       numbers', 'Array'],
471 ['+       numbers', 'Array'],
472 ],
473 tooltipText: 'App',
474 track: 'Components ⚛',
475 },
476 },
477 end: 31,
478 start: 21,
479 },
480 ],
481 ]);
482 });
483
484 // @gate __DEV__ && enableComponentPerformanceTrack
485 it('diffs HTML-like objects', async () => {
486 const App = function App({container}) {
487 Scheduler.unstable_advanceTime(10);
488 React.useEffect(() => {}, [container]);
489 };
490
491 class Window {}
492 const createOpaqueOriginWindow = () => {
493 return new Proxy(new Window(), {
494 get(target, prop) {
495 if (prop === Symbol.toStringTag) {
496 return target[Symbol.toStringTag];
497 }
498 // Some properties are allowed if JS itself is accessign those e.g.
499 // Symbol.toStringTag.
500 // Just make sure React isn't accessing arbitrary properties.
501 throw new Error(
502 `Failed to read named property '${String(prop)}' from Window`,
503 );
504 },
505 });
506 };
507
508 class OpaqueOriginHTMLIFrameElement {
509 constructor(textContent) {
510 this.textContent = textContent;
511 }
512 contentWindow = createOpaqueOriginWindow();
513 nodeType = 1;
514 [Symbol.toStringTag] = 'HTMLIFrameElement';
515 }
516
517 Scheduler.unstable_advanceTime(1);
518 await act(() => {
519 ReactNoop.render(
520 <App
521 container={new OpaqueOriginHTMLIFrameElement('foo')}
522 contentWindow={createOpaqueOriginWindow()}
523 />,
524 );
525 });
526
527 expect(performanceMeasureCalls).toEqual([
528 [
529 'Mount',
530 {
531 detail: {
532 devtools: {
533 color: 'warning',
534 properties: null,
535 tooltipText: 'Mount',
536 track: 'Components ⚛',
537 },
538 },
539 end: 11,
540 start: 1,
541 },
542 ],
543 ]);
544 performanceMeasureCalls.length = 0;
545
546 Scheduler.unstable_advanceTime(10);
547
548 await act(() => {
549 ReactNoop.render(
550 <App
551 container={new OpaqueOriginHTMLIFrameElement('bar')}
552 contentWindow={createOpaqueOriginWindow()}
553 />,
554 );
555 });
556
557 expect(performanceMeasureCalls).toEqual([
558 [
559 '​App',
560 {
561 detail: {
562 devtools: {
563 color: 'primary-dark',
564 properties: [
565 ['Changed Props', ''],
566 ['- container', 'HTMLIFrameElement'],
567 ['-   contentWindow', 'Window'],
568 ['-   nodeType', '1'],
569 ['-   textContent', '"foo"'],
570 ['+ container', 'HTMLIFrameElement'],
571 ['+   contentWindow', 'Window'],
572 ['+   nodeType', '1'],
573 ['+   textContent', '"bar"'],
574 [
575 '  contentWindow',
576 'Referentially unequal but deeply equal objects. Consider memoization.',
577 ],
578 ],
579 tooltipText: 'App',
580 track: 'Components ⚛',
581 },
582 },
583 end: 31,
584 start: 21,
585 },
586 ],
587 ]);
588 });
589 });