main
js 461 lines 12.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type Store from 'react-devtools-shared/src/devtools/store';
11
12 import {getVersionedRenderImplementation} from './utils';
13
14 describe('profiling charts', () => {
15 let React;
16 let Scheduler;
17 let store: Store;
18 let utils;
19
20 beforeEach(() => {
21 utils = require('./utils');
22 utils.beforeEachProfiling();
23
24 store = global.store;
25 store.collapseNodesByDefault = false;
26 store.recordChangeDescriptions = true;
27
28 React = require('react');
29 Scheduler = require('scheduler');
30 });
31
32 const {render} = getVersionedRenderImplementation();
33
34 function getFlamegraphChartData(rootID, commitIndex) {
35 const commitTree = store.profilerStore.profilingCache.getCommitTree({
36 commitIndex,
37 rootID,
38 });
39 const chartData = store.profilerStore.profilingCache.getFlamegraphChartData(
40 {
41 commitIndex,
42 commitTree,
43 rootID,
44 },
45 );
46 return {commitTree, chartData};
47 }
48
49 function getRankedChartData(rootID, commitIndex) {
50 const commitTree = store.profilerStore.profilingCache.getCommitTree({
51 commitIndex,
52 rootID,
53 });
54 const chartData = store.profilerStore.profilingCache.getRankedChartData({
55 commitIndex,
56 commitTree,
57 rootID,
58 });
59 return {commitTree, chartData};
60 }
61
62 describe('flamegraph chart', () => {
63 // @reactVersion >= 16.9
64 it('should contain valid data', () => {
65 const Parent = (_: {}) => {
66 Scheduler.unstable_advanceTime(10);
67 return (
68 <React.Fragment>
69 <Child key="first" duration={3} />
70 <Child key="second" duration={2} />
71 <Child key="third" duration={0} />
72 </React.Fragment>
73 );
74 };
75
76 // Memoize children to verify that chart doesn't include in the update.
77 const Child = React.memo(function Child({duration}) {
78 Scheduler.unstable_advanceTime(duration);
79 return null;
80 });
81
82 utils.act(() => store.profilerStore.startProfiling());
83
84 utils.act(() => render(<Parent />));
85 expect(store).toMatchInlineSnapshot(`
86 [root]
87 ▾ <Parent>
88 <Child key="first"> [Memo]
89 <Child key="second"> [Memo]
90 <Child key="third"> [Memo]
91 `);
92
93 utils.act(() => render(<Parent />));
94 expect(store).toMatchInlineSnapshot(`
95 [root]
96 ▾ <Parent>
97 <Child key="first"> [Memo]
98 <Child key="second"> [Memo]
99 <Child key="third"> [Memo]
100 `);
101
102 utils.act(() => store.profilerStore.stopProfiling());
103
104 const rootID = store.roots[0];
105
106 const firstCommitData = getFlamegraphChartData(rootID, 0);
107 expect(firstCommitData.commitTree.nodes.size).toBe(5);
108 expect(firstCommitData.chartData.rows).toMatchInlineSnapshot(`
109 [
110 [
111 {
112 "actualDuration": 15,
113 "didRender": true,
114 "id": 2,
115 "label": "Parent (10ms of 15ms)",
116 "name": "Parent",
117 "offset": 0,
118 "selfDuration": 10,
119 "treeBaseDuration": 15,
120 },
121 ],
122 [
123 {
124 "actualDuration": 0,
125 "didRender": true,
126 "id": 5,
127 "label": "Child (Memo) key="third" (<0.1ms of <0.1ms)",
128 "name": "Child",
129 "offset": 15,
130 "selfDuration": 0,
131 "treeBaseDuration": 0,
132 },
133 {
134 "actualDuration": 2,
135 "didRender": true,
136 "id": 4,
137 "label": "Child (Memo) key="second" (2ms of 2ms)",
138 "name": "Child",
139 "offset": 13,
140 "selfDuration": 2,
141 "treeBaseDuration": 2,
142 },
143 {
144 "actualDuration": 3,
145 "didRender": true,
146 "id": 3,
147 "label": "Child (Memo) key="first" (3ms of 3ms)",
148 "name": "Child",
149 "offset": 10,
150 "selfDuration": 3,
151 "treeBaseDuration": 3,
152 },
153 ],
154 ]
155 `);
156
157 const secondCommitData = getFlamegraphChartData(rootID, 1);
158 expect(secondCommitData.commitTree.nodes.size).toBe(5);
159 expect(secondCommitData.chartData.rows).toMatchInlineSnapshot(`
160 [
161 [
162 {
163 "actualDuration": 10,
164 "didRender": true,
165 "id": 2,
166 "label": "Parent (10ms of 10ms)",
167 "name": "Parent",
168 "offset": 0,
169 "selfDuration": 10,
170 "treeBaseDuration": 15,
171 },
172 ],
173 [
174 {
175 "actualDuration": 0,
176 "didRender": false,
177 "id": 5,
178 "label": "Child (Memo) key="third"",
179 "name": "Child",
180 "offset": 15,
181 "selfDuration": 0,
182 "treeBaseDuration": 0,
183 },
184 {
185 "actualDuration": 0,
186 "didRender": false,
187 "id": 4,
188 "label": "Child (Memo) key="second"",
189 "name": "Child",
190 "offset": 13,
191 "selfDuration": 0,
192 "treeBaseDuration": 2,
193 },
194 {
195 "actualDuration": 0,
196 "didRender": false,
197 "id": 3,
198 "label": "Child (Memo) key="first"",
199 "name": "Child",
200 "offset": 10,
201 "selfDuration": 0,
202 "treeBaseDuration": 3,
203 },
204 ],
205 ]
206 `);
207 });
208 });
209
210 describe('ranked chart', () => {
211 // @reactVersion >= 16.9
212 it('should contain valid data', () => {
213 const Parent = (_: {}) => {
214 Scheduler.unstable_advanceTime(10);
215 return (
216 <React.Fragment>
217 <Child key="first" duration={3} />
218 <Child key="second" duration={2} />
219 <Child key="third" duration={0} />
220 </React.Fragment>
221 );
222 };
223
224 // Memoize children to verify that chart doesn't include in the update.
225 const Child = React.memo(function Child({duration}) {
226 Scheduler.unstable_advanceTime(duration);
227 return null;
228 });
229
230 utils.act(() => store.profilerStore.startProfiling());
231
232 utils.act(() => render(<Parent />));
233 expect(store).toMatchInlineSnapshot(`
234 [root]
235 ▾ <Parent>
236 <Child key="first"> [Memo]
237 <Child key="second"> [Memo]
238 <Child key="third"> [Memo]
239 `);
240
241 utils.act(() => render(<Parent />));
242 expect(store).toMatchInlineSnapshot(`
243 [root]
244 ▾ <Parent>
245 <Child key="first"> [Memo]
246 <Child key="second"> [Memo]
247 <Child key="third"> [Memo]
248 `);
249
250 utils.act(() => store.profilerStore.stopProfiling());
251
252 const rootID = store.roots[0];
253
254 // Everything should render the first time.
255 const firstCommitData = getRankedChartData(rootID, 0);
256 expect(firstCommitData.commitTree.nodes.size).toBe(5);
257 expect(firstCommitData.chartData.nodes).toMatchInlineSnapshot(`
258 [
259 {
260 "id": 2,
261 "label": "Parent (10ms)",
262 "name": "Parent",
263 "value": 10,
264 },
265 {
266 "id": 3,
267 "label": "Child (Memo) key="first" (3ms)",
268 "name": "Child",
269 "value": 3,
270 },
271 {
272 "id": 4,
273 "label": "Child (Memo) key="second" (2ms)",
274 "name": "Child",
275 "value": 2,
276 },
277 {
278 "id": 5,
279 "label": "Child (Memo) key="third" (<0.1ms)",
280 "name": "Child",
281 "value": 0,
282 },
283 ]
284 `);
285
286 // Only parent should render the second time, since child props have not changed.
287 const secondCommitData = getRankedChartData(rootID, 1);
288 expect(secondCommitData.commitTree.nodes.size).toBe(5);
289 expect(secondCommitData.chartData.nodes).toMatchInlineSnapshot(`
290 [
291 {
292 "id": 2,
293 "label": "Parent (10ms)",
294 "name": "Parent",
295 "value": 10,
296 },
297 ]
298 `);
299 });
300 });
301
302 describe('components behind filtered fibers should not report false re-renders', () => {
303 it('should not report a component as re-rendered when its filtered parent bailed out', () => {
304 let triggerUpdate;
305
306 function Count() {
307 const [count, setCount] = React.useState(0);
308 triggerUpdate = () => setCount(c => c + 1);
309 Scheduler.unstable_advanceTime(5);
310 return count;
311 }
312
313 function Greeting() {
314 Scheduler.unstable_advanceTime(3);
315 return 'Hello';
316 }
317
318 function App() {
319 Scheduler.unstable_advanceTime(1);
320 return (
321 <React.Fragment>
322 <Count />
323 <div>
324 <Greeting />
325 </div>
326 </React.Fragment>
327 );
328 }
329
330 utils.act(() => store.profilerStore.startProfiling());
331 utils.act(() => render(<App />));
332
333 // Verify tree structure: div is filtered, so Greeting appears as child of App
334 expect(store).toMatchInlineSnapshot(`
335 [root]
336 ▾ <App>
337 <Count>
338 <Greeting>
339 `);
340
341 // Trigger a state update in Count. Should not cause Greeting to re-render.
342 utils.act(() => triggerUpdate());
343
344 utils.act(() => store.profilerStore.stopProfiling());
345
346 const rootID = store.roots[0];
347 const {chartData} = getFlamegraphChartData(rootID, 1);
348 const allNodes = chartData.rows.flat();
349
350 expect(allNodes).toEqual([
351 expect.objectContaining({name: 'App', didRender: false}),
352 expect.objectContaining({name: 'Greeting', didRender: false}),
353 expect.objectContaining({name: 'Count', didRender: true}),
354 ]);
355 });
356
357 it('should not report a component as re-rendered when behind a filtered fragment', () => {
358 let triggerUpdate;
359
360 function Count() {
361 const [count, setCount] = React.useState(0);
362 triggerUpdate = () => setCount(c => c + 1);
363 Scheduler.unstable_advanceTime(5);
364 return count;
365 }
366
367 function Greeting() {
368 Scheduler.unstable_advanceTime(3);
369 return 'Hello';
370 }
371
372 function App() {
373 Scheduler.unstable_advanceTime(1);
374 return (
375 <React.Fragment>
376 <Count />
377 <React.Fragment>
378 <Greeting />
379 </React.Fragment>
380 </React.Fragment>
381 );
382 }
383
384 utils.act(() => store.profilerStore.startProfiling());
385 utils.act(() => render(<App />));
386
387 // Fragment with null key is filtered, so Greeting appears as child of App
388 expect(store).toMatchInlineSnapshot(`
389 [root]
390 ▾ <App>
391 <Count>
392 <Greeting>
393 `);
394
395 // Trigger a state update in Count
396 utils.act(() => triggerUpdate());
397
398 utils.act(() => store.profilerStore.stopProfiling());
399
400 const rootID = store.roots[0];
401 const {chartData} = getFlamegraphChartData(rootID, 1);
402 const allNodes = chartData.rows.flat();
403
404 expect(allNodes).toEqual([
405 expect.objectContaining({name: 'App', didRender: false}),
406 expect.objectContaining({name: 'Greeting', didRender: false}),
407 expect.objectContaining({name: 'Count', didRender: true}),
408 ]);
409 });
410
411 it('should correctly report sibling components that did not re-render', () => {
412 let triggerUpdate;
413
414 function Count() {
415 const [count, setCount] = React.useState(0);
416 triggerUpdate = () => setCount(c => c + 1);
417 Scheduler.unstable_advanceTime(5);
418 return count;
419 }
420
421 function Greeting() {
422 Scheduler.unstable_advanceTime(3);
423 return 'Hello';
424 }
425
426 function App() {
427 Scheduler.unstable_advanceTime(1);
428 return (
429 <React.Fragment>
430 <Count />
431 <Greeting />
432 </React.Fragment>
433 );
434 }
435
436 utils.act(() => store.profilerStore.startProfiling());
437 utils.act(() => render(<App />));
438
439 expect(store).toMatchInlineSnapshot(`
440 [root]
441 ▾ <App>
442 <Count>
443 <Greeting>
444 `);
445
446 utils.act(() => triggerUpdate());
447
448 utils.act(() => store.profilerStore.stopProfiling());
449
450 const rootID = store.roots[0];
451 const {chartData} = getFlamegraphChartData(rootID, 1);
452 const allNodes = chartData.rows.flat();
453
454 expect(allNodes).toEqual([
455 expect.objectContaining({name: 'App', didRender: false}),
456 expect.objectContaining({name: 'Greeting', didRender: false}),
457 expect.objectContaining({name: 'Count', didRender: true}),
458 ]);
459 });
460 });
461 });