@samitouri / QOS-React-2 / commits / b25d996fc4

Added profiling chart data tests

Brian Vaughn committed May 6, 2019 at 13:37 UTC b25d996fc4d8d84afe00bbb852f7480ab4725fa6
4 files changed +641 -7
src/__tests__/__snapshots__/profilingCharts-test.js.snap new
+333
@@ -0,0 +1,333 @@
1 +// Jest Snapshot v1, https://goo.gl/fbAQLP
2 +
3 +exports[`profiling charts flamegraph chart should contain valid data: 0: CommitTree 1`] = `
4 +Object {
5 + "nodes": Map {
6 + 1 => Object {
7 + "children": Array [
8 + 2,
9 + ],
10 + "displayName": null,
11 + "id": 1,
12 + "key": null,
13 + "parentID": 0,
14 + "treeBaseDuration": 14,
15 + },
16 + 2 => Object {
17 + "children": Array [
18 + 3,
19 + 4,
20 + ],
21 + "displayName": "Parent",
22 + "id": 2,
23 + "key": null,
24 + "parentID": 1,
25 + "treeBaseDuration": 14,
26 + },
27 + 3 => Object {
28 + "children": Array [],
29 + "displayName": "Memo(Child)",
30 + "id": 3,
31 + "key": "first",
32 + "parentID": 2,
33 + "treeBaseDuration": 2,
34 + },
35 + 4 => Object {
36 + "children": Array [],
37 + "displayName": "Memo(Child)",
38 + "id": 4,
39 + "key": "second",
40 + "parentID": 2,
41 + "treeBaseDuration": 2,
42 + },
43 + },
44 + "rootID": 1,
45 +}
46 +`;
47 +
48 +exports[`profiling charts flamegraph chart should contain valid data: 0: FlamegraphChartData 1`] = `
49 +Object {
50 + "baseDuration": 14,
51 + "depth": 2,
52 + "idToDepthMap": Map {
53 + 2 => 1,
54 + 4 => 2,
55 + 3 => 2,
56 + },
57 + "maxSelfDuration": 10,
58 + "rows": Array [
59 + Array [
60 + Object {
61 + "actualDuration": 14,
62 + "didRender": true,
63 + "id": 2,
64 + "label": "Parent (10.0ms of 14.0ms)",
65 + "name": "Parent",
66 + "offset": 0,
67 + "selfDuration": 10,
68 + "treeBaseDuration": 14,
69 + },
70 + ],
71 + Array [
72 + Object {
73 + "actualDuration": 2,
74 + "didRender": true,
75 + "id": 4,
76 + "label": "Memo(Child) key=\\"second\\" (2.0ms of 2.0ms)",
77 + "name": "Memo(Child)",
78 + "offset": 12,
79 + "selfDuration": 2,
80 + "treeBaseDuration": 2,
81 + },
82 + Object {
83 + "actualDuration": 2,
84 + "didRender": true,
85 + "id": 3,
86 + "label": "Memo(Child) key=\\"first\\" (2.0ms of 2.0ms)",
87 + "name": "Memo(Child)",
88 + "offset": 10,
89 + "selfDuration": 2,
90 + "treeBaseDuration": 2,
91 + },
92 + ],
93 + ],
94 +}
95 +`;
96 +
97 +exports[`profiling charts flamegraph chart should contain valid data: 1: CommitTree 1`] = `
98 +Object {
99 + "nodes": Map {
100 + 1 => Object {
101 + "children": Array [
102 + 2,
103 + ],
104 + "displayName": null,
105 + "id": 1,
106 + "key": null,
107 + "parentID": 0,
108 + "treeBaseDuration": 14,
109 + },
110 + 2 => Object {
111 + "children": Array [
112 + 3,
113 + 4,
114 + ],
115 + "displayName": "Parent",
116 + "id": 2,
117 + "key": null,
118 + "parentID": 1,
119 + "treeBaseDuration": 14,
120 + },
121 + 3 => Object {
122 + "children": Array [],
123 + "displayName": "Memo(Child)",
124 + "id": 3,
125 + "key": "first",
126 + "parentID": 2,
127 + "treeBaseDuration": 2,
128 + },
129 + 4 => Object {
130 + "children": Array [],
131 + "displayName": "Memo(Child)",
132 + "id": 4,
133 + "key": "second",
134 + "parentID": 2,
135 + "treeBaseDuration": 2,
136 + },
137 + },
138 + "rootID": 1,
139 +}
140 +`;
141 +
142 +exports[`profiling charts flamegraph chart should contain valid data: 1: FlamegraphChartData 1`] = `
143 +Object {
144 + "baseDuration": 14,
145 + "depth": 2,
146 + "idToDepthMap": Map {
147 + 2 => 1,
148 + 4 => 2,
149 + 3 => 2,
150 + },
151 + "maxSelfDuration": 10,
152 + "rows": Array [
153 + Array [
154 + Object {
155 + "actualDuration": 10,
156 + "didRender": true,
157 + "id": 2,
158 + "label": "Parent (10.0ms of 10.0ms)",
159 + "name": "Parent",
160 + "offset": 0,
161 + "selfDuration": 10,
162 + "treeBaseDuration": 14,
163 + },
164 + ],
165 + Array [
166 + Object {
167 + "actualDuration": 0,
168 + "didRender": false,
169 + "id": 4,
170 + "label": "Memo(Child) key=\\"second\\"",
171 + "name": "Memo(Child)",
172 + "offset": 12,
173 + "selfDuration": 0,
174 + "treeBaseDuration": 2,
175 + },
176 + Object {
177 + "actualDuration": 0,
178 + "didRender": false,
179 + "id": 3,
180 + "label": "Memo(Child) key=\\"first\\"",
181 + "name": "Memo(Child)",
182 + "offset": 10,
183 + "selfDuration": 0,
184 + "treeBaseDuration": 2,
185 + },
186 + ],
187 + ],
188 +}
189 +`;
190 +
191 +exports[`profiling charts interactions should contain valid data: Interactions 1`] = `
192 +Object {
193 + "lastInteractionTime": 24,
194 + "maxCommitDuration": 14,
195 +}
196 +`;
197 +
198 +exports[`profiling charts interactions should contain valid data: Interactions 2`] = `
199 +Object {
200 + "lastInteractionTime": 24,
201 + "maxCommitDuration": 14,
202 +}
203 +`;
204 +
205 +exports[`profiling charts ranked chart should contain valid data: 0: CommitTree 1`] = `
206 +Object {
207 + "nodes": Map {
208 + 1 => Object {
209 + "children": Array [
210 + 2,
211 + ],
212 + "displayName": null,
213 + "id": 1,
214 + "key": null,
215 + "parentID": 0,
216 + "treeBaseDuration": 14,
217 + },
218 + 2 => Object {
219 + "children": Array [
220 + 3,
221 + 4,
222 + ],
223 + "displayName": "Parent",
224 + "id": 2,
225 + "key": null,
226 + "parentID": 1,
227 + "treeBaseDuration": 14,
228 + },
229 + 3 => Object {
230 + "children": Array [],
231 + "displayName": "Memo(Child)",
232 + "id": 3,
233 + "key": "first",
234 + "parentID": 2,
235 + "treeBaseDuration": 2,
236 + },
237 + 4 => Object {
238 + "children": Array [],
239 + "displayName": "Memo(Child)",
240 + "id": 4,
241 + "key": "second",
242 + "parentID": 2,
243 + "treeBaseDuration": 2,
244 + },
245 + },
246 + "rootID": 1,
247 +}
248 +`;
249 +
250 +exports[`profiling charts ranked chart should contain valid data: 0: RankedChartData 1`] = `
251 +Object {
252 + "maxValue": 10,
253 + "nodes": Array [
254 + Object {
255 + "id": 2,
256 + "label": "Parent (10.0ms)",
257 + "name": "Parent",
258 + "value": 10,
259 + },
260 + Object {
261 + "id": 3,
262 + "label": "Memo(Child) key=\\"first\\" (2.0ms)",
263 + "name": "Memo(Child)",
264 + "value": 2,
265 + },
266 + Object {
267 + "id": 4,
268 + "label": "Memo(Child) key=\\"second\\" (2.0ms)",
269 + "name": "Memo(Child)",
270 + "value": 2,
271 + },
272 + ],
273 +}
274 +`;
275 +
276 +exports[`profiling charts ranked chart should contain valid data: 1: CommitTree 1`] = `
277 +Object {
278 + "nodes": Map {
279 + 1 => Object {
280 + "children": Array [
281 + 2,
282 + ],
283 + "displayName": null,
284 + "id": 1,
285 + "key": null,
286 + "parentID": 0,
287 + "treeBaseDuration": 14,
288 + },
289 + 2 => Object {
290 + "children": Array [
291 + 3,
292 + 4,
293 + ],
294 + "displayName": "Parent",
295 + "id": 2,
296 + "key": null,
297 + "parentID": 1,
298 + "treeBaseDuration": 14,
299 + },
300 + 3 => Object {
301 + "children": Array [],
302 + "displayName": "Memo(Child)",
303 + "id": 3,
304 + "key": "first",
305 + "parentID": 2,
306 + "treeBaseDuration": 2,
307 + },
308 + 4 => Object {
309 + "children": Array [],
310 + "displayName": "Memo(Child)",
311 + "id": 4,
312 + "key": "second",
313 + "parentID": 2,
314 + "treeBaseDuration": 2,
315 + },
316 + },
317 + "rootID": 1,
318 +}
319 +`;
320 +
321 +exports[`profiling charts ranked chart should contain valid data: 1: RankedChartData 1`] = `
322 +Object {
323 + "maxValue": 10,
324 + "nodes": Array [
325 + Object {
326 + "id": 2,
327 + "label": "Parent (10.0ms)",
328 + "name": "Parent",
329 + "value": 10,
330 + },
331 + ],
332 +}
333 +`;
src/__tests__/profiling-test.js
+2 -4
@@ -1,13 +1,11 @@
1 // @flow
2
3 -import type React from 'react';
4 -import type ReactDOM from 'react-dom';
3 import typeof ReactTestRenderer from 'react-test-renderer';
4 import type Store from 'src/devtools/store';
5
6 describe('profiling', () => {
9 - let React: React;
10 - let ReactDOM: ReactDOM;
7 + let React;
8 + let ReactDOM;
9 let Scheduler;
10 let SchedulerTracing;
11 let TestRenderer: ReactTestRenderer;
src/__tests__/profilingCharts-test.js new
+292
@@ -0,0 +1,292 @@
1 +// @flow
2 +
3 +import typeof TestRendererType from 'react-test-renderer';
4 +import type Store from 'src/devtools/store';
5 +
6 +describe('profiling charts', () => {
7 + let React;
8 + let ReactDOM;
9 + let Scheduler;
10 + let SchedulerTracing;
11 + let TestRenderer: TestRendererType;
12 + let store: Store;
13 + let utils;
14 +
15 + beforeEach(() => {
16 + utils = require('./utils');
17 + utils.beforeEachProfiling();
18 +
19 + store = global.store;
20 + store.collapseNodesByDefault = false;
21 +
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + Scheduler = require('scheduler');
25 + SchedulerTracing = require('scheduler/tracing');
26 + TestRenderer = utils.requireTestRenderer();
27 + });
28 +
29 + describe('flamegraph chart', () => {
30 + it('should contain valid data', async done => {
31 + const Parent = ({ count }) => {
32 + Scheduler.advanceTime(10);
33 + return (
34 + <React.Fragment>
35 + <Child key="first" />
36 + <Child key="second" />
37 + </React.Fragment>
38 + );
39 + };
40 +
41 + // Memoize children to verify that chart doesn't include in the update.
42 + const Child = React.memo(function Child() {
43 + Scheduler.advanceTime(2);
44 + return null;
45 + });
46 +
47 + const container = document.createElement('div');
48 +
49 + utils.act(() => store.startProfiling());
50 + utils.act(() =>
51 + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
52 + ReactDOM.render(<Parent />, container)
53 + )
54 + );
55 + utils.act(() =>
56 + SchedulerTracing.unstable_trace(
57 + 'update',
58 + Scheduler.unstable_now(),
59 + () => ReactDOM.render(<Parent />, container)
60 + )
61 + );
62 + utils.act(() => store.stopProfiling());
63 +
64 + let suspenseResolved = false;
65 +
66 + function Suspender({ commitIndex, rendererID, rootID }) {
67 + const profilingSummary = store.profilingCache.ProfilingSummary.read({
68 + rendererID,
69 + rootID,
70 + });
71 + const commitDetails = store.profilingCache.CommitDetails.read({
72 + commitIndex,
73 + rendererID,
74 + rootID,
75 + });
76 + suspenseResolved = true;
77 + const commitTree = store.profilingCache.getCommitTree({
78 + commitIndex,
79 + profilingSummary,
80 + });
81 + const chartData = store.profilingCache.getFlamegraphChartData({
82 + commitDetails,
83 + commitIndex,
84 + commitTree,
85 + });
86 + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
87 + expect(chartData).toMatchSnapshot(
88 + `${commitIndex}: FlamegraphChartData`
89 + );
90 + return null;
91 + }
92 +
93 + const rendererID = utils.getRendererID();
94 + const rootID = store.roots[0];
95 +
96 + for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
97 + suspenseResolved = false;
98 +
99 + await utils.actSuspense(
100 + () =>
101 + TestRenderer.create(
102 + <React.Suspense fallback={null}>
103 + <Suspender
104 + commitIndex={commitIndex}
105 + rendererID={rendererID}
106 + rootID={rootID}
107 + />
108 + </React.Suspense>
109 + ),
110 + 3
111 + );
112 +
113 + expect(suspenseResolved).toBe(true);
114 + }
115 +
116 + expect(suspenseResolved).toBe(true);
117 +
118 + done();
119 + });
120 + });
121 +
122 + describe('ranked chart', () => {
123 + it('should contain valid data', async done => {
124 + const Parent = ({ count }) => {
125 + Scheduler.advanceTime(10);
126 + return (
127 + <React.Fragment>
128 + <Child key="first" />
129 + <Child key="second" />
130 + </React.Fragment>
131 + );
132 + };
133 +
134 + // Memoize children to verify that chart doesn't include in the update.
135 + const Child = React.memo(function Child() {
136 + Scheduler.advanceTime(2);
137 + return null;
138 + });
139 +
140 + const container = document.createElement('div');
141 +
142 + utils.act(() => store.startProfiling());
143 + utils.act(() =>
144 + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
145 + ReactDOM.render(<Parent />, container)
146 + )
147 + );
148 + utils.act(() =>
149 + SchedulerTracing.unstable_trace(
150 + 'update',
151 + Scheduler.unstable_now(),
152 + () => ReactDOM.render(<Parent />, container)
153 + )
154 + );
155 + utils.act(() => store.stopProfiling());
156 +
157 + let suspenseResolved = false;
158 +
159 + function Suspender({ commitIndex, rendererID, rootID }) {
160 + const profilingSummary = store.profilingCache.ProfilingSummary.read({
161 + rendererID,
162 + rootID,
163 + });
164 + const commitDetails = store.profilingCache.CommitDetails.read({
165 + commitIndex,
166 + rendererID,
167 + rootID,
168 + });
169 + suspenseResolved = true;
170 + const commitTree = store.profilingCache.getCommitTree({
171 + commitIndex,
172 + profilingSummary,
173 + });
174 + const chartData = store.profilingCache.getRankedChartData({
175 + commitDetails,
176 + commitIndex,
177 + commitTree,
178 + });
179 + expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
180 + expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`);
181 + return null;
182 + }
183 +
184 + const rendererID = utils.getRendererID();
185 + const rootID = store.roots[0];
186 +
187 + for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
188 + suspenseResolved = false;
189 +
190 + await utils.actSuspense(
191 + () =>
192 + TestRenderer.create(
193 + <React.Suspense fallback={null}>
194 + <Suspender
195 + commitIndex={commitIndex}
196 + rendererID={rendererID}
197 + rootID={rootID}
198 + />
199 + </React.Suspense>
200 + ),
201 + 3
202 + );
203 +
204 + expect(suspenseResolved).toBe(true);
205 + }
206 +
207 + done();
208 + });
209 + });
210 +
211 + describe('interactions', () => {
212 + it('should contain valid data', async done => {
213 + const Parent = ({ count }) => {
214 + Scheduler.advanceTime(10);
215 + return (
216 + <React.Fragment>
217 + <Child key="first" />
218 + <Child key="second" />
219 + </React.Fragment>
220 + );
221 + };
222 +
223 + // Memoize children to verify that chart doesn't include in the update.
224 + const Child = React.memo(function Child() {
225 + Scheduler.advanceTime(2);
226 + return null;
227 + });
228 +
229 + const container = document.createElement('div');
230 +
231 + utils.act(() => store.startProfiling());
232 + utils.act(() =>
233 + SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
234 + ReactDOM.render(<Parent />, container)
235 + )
236 + );
237 + utils.act(() =>
238 + SchedulerTracing.unstable_trace(
239 + 'update',
240 + Scheduler.unstable_now(),
241 + () => ReactDOM.render(<Parent />, container)
242 + )
243 + );
244 + utils.act(() => store.stopProfiling());
245 +
246 + let suspenseResolved = false;
247 +
248 + function Suspender({ commitIndex, rendererID, rootID }) {
249 + const profilingSummary = store.profilingCache.ProfilingSummary.read({
250 + rendererID,
251 + rootID,
252 + });
253 + const interactions = store.profilingCache.Interactions.read({
254 + rendererID,
255 + rootID,
256 + });
257 + suspenseResolved = true;
258 + const chartData = store.profilingCache.getInteractionsChartData({
259 + interactions,
260 + profilingSummary,
261 + });
262 + expect(chartData).toMatchSnapshot('Interactions');
263 + return null;
264 + }
265 +
266 + const rendererID = utils.getRendererID();
267 + const rootID = store.roots[0];
268 +
269 + for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
270 + suspenseResolved = false;
271 +
272 + await utils.actSuspense(
273 + () =>
274 + TestRenderer.create(
275 + <React.Suspense fallback={null}>
276 + <Suspender
277 + commitIndex={commitIndex}
278 + rendererID={rendererID}
279 + rootID={rootID}
280 + />
281 + </React.Suspense>
282 + ),
283 + 3
284 + );
285 +
286 + expect(suspenseResolved).toBe(true);
287 + }
288 +
289 + done();
290 + });
291 + });
292 +});
src/__tests__/utils.js
+14 -3
@@ -12,7 +12,10 @@ export function act(callback: Function): void {
12 jest.runAllTimers();
13 }
14
15 -export async function actSuspense(callback: Function): Promise<void> {
15 +export async function actSuspense(
16 + callback: Function,
17 + numTimesToFlush: number = 1
18 +): Promise<void> {
19 const TestUtils = require('react-dom/test-utils');
20 const Scheduler = require('scheduler');
21
@@ -24,8 +27,16 @@ export async function actSuspense(callback: Function): Promise<void> {
27 jest.runAllTimers();
28 });
29
27 - // Re-render after resolved promises
28 - Scheduler.flushAll();
30 + // Run cascading microtasks and flush scheduled React work.
31 + // Components that suspend multiple times will need to do this once per suspend operation.
32 + // HACK Ideally the mock scheduler would provide an API to ask if there was outstanding work.
33 + while (--numTimesToFlush >= 0) {
34 + // $FlowFixMe Flow doens't know about "await act()" yet
35 + await TestUtils.act(async () => {
36 + jest.runAllTimers();
37 + Scheduler.flushAll();
38 + });
39 + }
40 }
41
42 export function beforeEachProfiling(): void {