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

Added export+import profiling data test coverage Also fixed an incomplete import feature as well as a regression in export that had gone unnoticed due to previous lack of test coverage.

Added export+import profiling data test coverage Also fixed an incomplete import feature as well as a regression in export that had gone unnoticed due to previous lack of test coverage.

Brian Vaughn committed May 7, 2019 at 13:08 UTC acdc9bae674d27fb107a8ed99680148f07cfd55d
10 files changed +1043 -77
package.json
+2 -1
@@ -14,7 +14,8 @@
14 "<rootDir>"
15 ],
16 "moduleNameMapper": {
17 - "^src/(.*)$": "<rootDir>/src/$1"
17 + "^src/(.*)$": "<rootDir>/src/$1",
18 + "\\.css$": "<rootDir>/src/__tests__/__mocks__/cssMock.js"
19 },
20 "setupFiles": [
21 "<rootDir>/src/__tests__/setupEnv"
src/__tests__/__mocks__/cssMock.js new
+1
@@ -0,0 +1 @@
1 +module.exports = {};
src/__tests__/__snapshots__/profiling-test.js.snap
+765
@@ -54,6 +54,222 @@ Object {
54 }
55 `;
56
57 +exports[`profiling CommitDetails should be collected for each commit: exported data 1`] = `
58 +Object {
59 + "commitDetails": Array [
60 + Object {
61 + "actualDurations": Array [
62 + 1,
63 + 14,
64 + 2,
65 + 14,
66 + 3,
67 + 2,
68 + 4,
69 + 2,
70 + ],
71 + "commitIndex": 0,
72 + "interactions": Array [],
73 + "rootID": 1,
74 + },
75 + Object {
76 + "actualDurations": Array [
77 + 3,
78 + 2,
79 + 4,
80 + 2,
81 + 5,
82 + 2,
83 + 2,
84 + 16,
85 + 1,
86 + 16,
87 + ],
88 + "commitIndex": 1,
89 + "interactions": Array [],
90 + "rootID": 1,
91 + },
92 + Object {
93 + "actualDurations": Array [
94 + 3,
95 + 2,
96 + 2,
97 + 12,
98 + 1,
99 + 12,
100 + ],
101 + "commitIndex": 2,
102 + "interactions": Array [],
103 + "rootID": 1,
104 + },
105 + Object {
106 + "actualDurations": Array [
107 + 2,
108 + 10,
109 + 1,
110 + 10,
111 + ],
112 + "commitIndex": 3,
113 + "interactions": Array [],
114 + "rootID": 1,
115 + },
116 + ],
117 + "interactions": Object {
118 + "interactions": Array [],
119 + "rootID": 1,
120 + },
121 + "profilingOperations": Map {
122 + 1 => Array [
123 + Array [
124 + 1,
125 + 1,
126 + 17,
127 + 6,
128 + 80,
129 + 97,
130 + 114,
131 + 101,
132 + 110,
133 + 116,
134 + 5,
135 + 67,
136 + 104,
137 + 105,
138 + 108,
139 + 100,
140 + 1,
141 + 48,
142 + 1,
143 + 49,
144 + 1,
145 + 1,
146 + 11,
147 + 1,
148 + 1,
149 + 4,
150 + 1,
151 + 14000,
152 + 1,
153 + 2,
154 + 5,
155 + 1,
156 + 0,
157 + 1,
158 + 0,
159 + 4,
160 + 2,
161 + 14000,
162 + 1,
163 + 3,
164 + 5,
165 + 2,
166 + 2,
167 + 2,
168 + 3,
169 + 4,
170 + 3,
171 + 2000,
172 + 1,
173 + 4,
174 + 5,
175 + 2,
176 + 2,
177 + 2,
178 + 4,
179 + 4,
180 + 4,
181 + 2000,
182 + ],
183 + Array [
184 + 1,
185 + 1,
186 + 8,
187 + 5,
188 + 67,
189 + 104,
190 + 105,
191 + 108,
192 + 100,
193 + 1,
194 + 50,
195 + 1,
196 + 5,
197 + 5,
198 + 2,
199 + 2,
200 + 1,
201 + 2,
202 + 4,
203 + 5,
204 + 2000,
205 + 4,
206 + 2,
207 + 16000,
208 + 3,
209 + 2,
210 + 3,
211 + 3,
212 + 4,
213 + 5,
214 + 4,
215 + 1,
216 + 16000,
217 + ],
218 + Array [
219 + 1,
220 + 1,
221 + 0,
222 + 2,
223 + 2,
224 + 5,
225 + 4,
226 + 4,
227 + 2,
228 + 12000,
229 + 4,
230 + 1,
231 + 12000,
232 + ],
233 + Array [
234 + 1,
235 + 1,
236 + 0,
237 + 2,
238 + 1,
239 + 3,
240 + 4,
241 + 2,
242 + 10000,
243 + 4,
244 + 1,
245 + 10000,
246 + ],
247 + ],
248 + },
249 + "profilingSnapshots": Map {
250 + 1 => Map {},
251 + },
252 + "profilingSummary": Object {
253 + "commitDurations": Array [
254 + 14,
255 + 16,
256 + 12,
257 + 10,
258 + ],
259 + "commitTimes": Array [
260 + 14,
261 + 30,
262 + 42,
263 + 52,
264 + ],
265 + "initialTreeBaseDurations": Array [],
266 + "interactionCount": 0,
267 + "rootID": 1,
268 + },
269 + "version": 1,
270 +}
271 +`;
272 +
273 exports[`profiling FiberCommits should be collected for each rendered fiber: FiberCommits: element 2 1`] = `
274 Object {
275 "commitDurations": Array [
@@ -108,6 +324,202 @@ Object {
324 }
325 `;
326
327 +exports[`profiling FiberCommits should be collected for each rendered fiber: exported data 1`] = `
328 +Object {
329 + "commitDetails": Array [
330 + Object {
331 + "actualDurations": Array [
332 + 1,
333 + 12,
334 + 2,
335 + 12,
336 + 3,
337 + 2,
338 + ],
339 + "commitIndex": 0,
340 + "interactions": Array [],
341 + "rootID": 1,
342 + },
343 + Object {
344 + "actualDurations": Array [
345 + 3,
346 + 2,
347 + 4,
348 + 2,
349 + 2,
350 + 14,
351 + 1,
352 + 14,
353 + ],
354 + "commitIndex": 1,
355 + "interactions": Array [],
356 + "rootID": 1,
357 + },
358 + Object {
359 + "actualDurations": Array [
360 + 3,
361 + 2,
362 + 4,
363 + 2,
364 + 5,
365 + 2,
366 + 2,
367 + 16,
368 + 1,
369 + 16,
370 + ],
371 + "commitIndex": 2,
372 + "interactions": Array [],
373 + "rootID": 1,
374 + },
375 + ],
376 + "interactions": Object {
377 + "interactions": Array [],
378 + "rootID": 1,
379 + },
380 + "profilingOperations": Map {
381 + 1 => Array [
382 + Array [
383 + 1,
384 + 1,
385 + 15,
386 + 6,
387 + 80,
388 + 97,
389 + 114,
390 + 101,
391 + 110,
392 + 116,
393 + 5,
394 + 67,
395 + 104,
396 + 105,
397 + 108,
398 + 100,
399 + 1,
400 + 48,
401 + 1,
402 + 1,
403 + 11,
404 + 1,
405 + 1,
406 + 4,
407 + 1,
408 + 12000,
409 + 1,
410 + 2,
411 + 5,
412 + 1,
413 + 0,
414 + 1,
415 + 0,
416 + 4,
417 + 2,
418 + 12000,
419 + 1,
420 + 3,
421 + 5,
422 + 2,
423 + 2,
424 + 2,
425 + 3,
426 + 4,
427 + 3,
428 + 2000,
429 + ],
430 + Array [
431 + 1,
432 + 1,
433 + 8,
434 + 5,
435 + 67,
436 + 104,
437 + 105,
438 + 108,
439 + 100,
440 + 1,
441 + 49,
442 + 1,
443 + 4,
444 + 5,
445 + 2,
446 + 2,
447 + 1,
448 + 2,
449 + 4,
450 + 4,
451 + 2000,
452 + 4,
453 + 2,
454 + 14000,
455 + 3,
456 + 2,
457 + 2,
458 + 3,
459 + 4,
460 + 4,
461 + 1,
462 + 14000,
463 + ],
464 + Array [
465 + 1,
466 + 1,
467 + 8,
468 + 5,
469 + 67,
470 + 104,
471 + 105,
472 + 108,
473 + 100,
474 + 1,
475 + 50,
476 + 1,
477 + 5,
478 + 5,
479 + 2,
480 + 2,
481 + 1,
482 + 2,
483 + 4,
484 + 5,
485 + 2000,
486 + 4,
487 + 2,
488 + 16000,
489 + 3,
490 + 2,
491 + 3,
492 + 3,
493 + 4,
494 + 5,
495 + 4,
496 + 1,
497 + 16000,
498 + ],
499 + ],
500 + },
501 + "profilingSnapshots": Map {
502 + 1 => Map {},
503 + },
504 + "profilingSummary": Object {
505 + "commitDurations": Array [
506 + 12,
507 + 14,
508 + 16,
509 + ],
510 + "commitTimes": Array [
511 + 12,
512 + 26,
513 + 42,
514 + ],
515 + "initialTreeBaseDurations": Array [],
516 + "interactionCount": 0,
517 + "rootID": 1,
518 + },
519 + "version": 1,
520 +}
521 +`;
522 +
523 exports[`profiling Interactions should be collected for every traced interaction: Interactions 1`] = `
524 Array [
525 Object {
@@ -131,6 +543,181 @@ Array [
543 ]
544 `;
545
546 +exports[`profiling Interactions should be collected for every traced interaction: exported data 1`] = `
547 +Object {
548 + "commitDetails": Array [
549 + Object {
550 + "actualDurations": Array [
551 + 1,
552 + 12,
553 + 2,
554 + 12,
555 + 3,
556 + 2,
557 + ],
558 + "commitIndex": 0,
559 + "interactions": Array [
560 + Object {
561 + "__count": 1,
562 + "id": 0,
563 + "name": "mount: one child",
564 + "timestamp": 0,
565 + },
566 + ],
567 + "rootID": 1,
568 + },
569 + Object {
570 + "actualDurations": Array [
571 + 3,
572 + 2,
573 + 4,
574 + 2,
575 + 2,
576 + 14,
577 + 1,
578 + 14,
579 + ],
580 + "commitIndex": 1,
581 + "interactions": Array [
582 + Object {
583 + "__count": 0,
584 + "id": 1,
585 + "name": "update: two children",
586 + "timestamp": 12,
587 + },
588 + ],
589 + "rootID": 1,
590 + },
591 + ],
592 + "interactions": Object {
593 + "interactions": Array [
594 + Object {
595 + "__count": 1,
596 + "commits": Array [
597 + 0,
598 + ],
599 + "id": 0,
600 + "name": "mount: one child",
601 + "timestamp": 0,
602 + },
603 + Object {
604 + "__count": 0,
605 + "commits": Array [
606 + 1,
607 + ],
608 + "id": 1,
609 + "name": "update: two children",
610 + "timestamp": 12,
611 + },
612 + ],
613 + "rootID": 1,
614 + },
615 + "profilingOperations": Map {
616 + 1 => Array [
617 + Array [
618 + 1,
619 + 1,
620 + 15,
621 + 6,
622 + 80,
623 + 97,
624 + 114,
625 + 101,
626 + 110,
627 + 116,
628 + 5,
629 + 67,
630 + 104,
631 + 105,
632 + 108,
633 + 100,
634 + 1,
635 + 48,
636 + 1,
637 + 1,
638 + 11,
639 + 1,
640 + 1,
641 + 4,
642 + 1,
643 + 12000,
644 + 1,
645 + 2,
646 + 5,
647 + 1,
648 + 0,
649 + 1,
650 + 0,
651 + 4,
652 + 2,
653 + 12000,
654 + 1,
655 + 3,
656 + 5,
657 + 2,
658 + 2,
659 + 2,
660 + 3,
661 + 4,
662 + 3,
663 + 2000,
664 + ],
665 + Array [
666 + 1,
667 + 1,
668 + 8,
669 + 5,
670 + 67,
671 + 104,
672 + 105,
673 + 108,
674 + 100,
675 + 1,
676 + 49,
677 + 1,
678 + 4,
679 + 5,
680 + 2,
681 + 2,
682 + 1,
683 + 2,
684 + 4,
685 + 4,
686 + 2000,
687 + 4,
688 + 2,
689 + 14000,
690 + 3,
691 + 2,
692 + 2,
693 + 3,
694 + 4,
695 + 4,
696 + 1,
697 + 14000,
698 + ],
699 + ],
700 + },
701 + "profilingSnapshots": Map {
702 + 1 => Map {},
703 + },
704 + "profilingSummary": Object {
705 + "commitDurations": Array [
706 + 12,
707 + 14,
708 + ],
709 + "commitTimes": Array [
710 + 12,
711 + 26,
712 + ],
713 + "initialTreeBaseDurations": Array [],
714 + "interactionCount": 2,
715 + "rootID": 1,
716 + },
717 + "version": 1,
718 +}
719 +`;
720 +
721 exports[`profiling ProfilingSummary should be collected for each commit: ProfilingSummary 1`] = `
722 Object {
723 "commitDurations": Array [
@@ -153,3 +740,181 @@ Object {
740 "rootID": 1,
741 }
742 `;
743 +
744 +exports[`profiling ProfilingSummary should be collected for each commit: exported data 1`] = `
745 +Object {
746 + "commitDetails": Array [
747 + Object {
748 + "actualDurations": Array [
749 + 3,
750 + 2,
751 + 4,
752 + 2,
753 + 5,
754 + 2,
755 + 2,
756 + 16,
757 + 1,
758 + 16,
759 + ],
760 + "commitIndex": 0,
761 + "interactions": Array [],
762 + "rootID": 1,
763 + },
764 + Object {
765 + "actualDurations": Array [
766 + 3,
767 + 2,
768 + 2,
769 + 12,
770 + 1,
771 + 12,
772 + ],
773 + "commitIndex": 1,
774 + "interactions": Array [],
775 + "rootID": 1,
776 + },
777 + Object {
778 + "actualDurations": Array [
779 + 2,
780 + 10,
781 + 1,
782 + 10,
783 + ],
784 + "commitIndex": 2,
785 + "interactions": Array [],
786 + "rootID": 1,
787 + },
788 + ],
789 + "interactions": Object {
790 + "interactions": Array [],
791 + "rootID": 1,
792 + },
793 + "profilingOperations": Map {
794 + 1 => Array [
795 + Array [
796 + 1,
797 + 1,
798 + 8,
799 + 5,
800 + 67,
801 + 104,
802 + 105,
803 + 108,
804 + 100,
805 + 1,
806 + 50,
807 + 1,
808 + 5,
809 + 5,
810 + 2,
811 + 2,
812 + 1,
813 + 2,
814 + 4,
815 + 5,
816 + 2000,
817 + 4,
818 + 2,
819 + 16000,
820 + 3,
821 + 2,
822 + 3,
823 + 3,
824 + 4,
825 + 5,
826 + 4,
827 + 1,
828 + 16000,
829 + ],
830 + Array [
831 + 1,
832 + 1,
833 + 0,
834 + 2,
835 + 2,
836 + 5,
837 + 4,
838 + 4,
839 + 2,
840 + 12000,
841 + 4,
842 + 1,
843 + 12000,
844 + ],
845 + Array [
846 + 1,
847 + 1,
848 + 0,
849 + 2,
850 + 1,
851 + 3,
852 + 4,
853 + 2,
854 + 10000,
855 + 4,
856 + 1,
857 + 10000,
858 + ],
859 + ],
860 + },
861 + "profilingSnapshots": Map {
862 + 1 => Map {
863 + 1 => Object {
864 + "children": Array [
865 + 2,
866 + ],
867 + "displayName": null,
868 + "id": 1,
869 + "key": null,
870 + },
871 + 2 => Object {
872 + "children": Array [
873 + 3,
874 + 4,
875 + ],
876 + "displayName": "Parent",
877 + "id": 2,
878 + "key": null,
879 + },
880 + 3 => Object {
881 + "children": Array [],
882 + "displayName": "Child",
883 + "id": 3,
884 + "key": "0",
885 + },
886 + 4 => Object {
887 + "children": Array [],
888 + "displayName": "Child",
889 + "id": 4,
890 + "key": "1",
891 + },
892 + },
893 + },
894 + "profilingSummary": Object {
895 + "commitDurations": Array [
896 + 16,
897 + 12,
898 + 10,
899 + ],
900 + "commitTimes": Array [
901 + 16,
902 + 28,
903 + 38,
904 + ],
905 + "initialTreeBaseDurations": Array [
906 + 1,
907 + 14,
908 + 2,
909 + 14,
910 + 3,
911 + 2,
912 + 4,
913 + 2,
914 + ],
915 + "interactionCount": 0,
916 + "rootID": 1,
917 + },
918 + "version": 1,
919 +}
920 +`;
src/__tests__/profiling-test.js
+174 -31
@@ -1,6 +1,7 @@
1 // @flow
2
3 import typeof ReactTestRenderer from 'react-test-renderer';
4 +import type Bridge from 'src/bridge';
5 import type Store from 'src/devtools/store';
6
7 describe('profiling', () => {
@@ -9,13 +10,55 @@ describe('profiling', () => {
10 let Scheduler;
11 let SchedulerTracing;
12 let TestRenderer: ReactTestRenderer;
13 + let bridge: Bridge;
14 let store: Store;
15 let utils;
16
17 + const exportImportHelper = (rendererID: number, rootID: number) => {
18 + const {
19 + prepareProfilingExport,
20 + prepareProfilingImport,
21 + } = require('src/devtools/views/Profiler/utils');
22 +
23 + let exportedProfilingSummary;
24 + bridge.addListener('exportFile', ({ contents }) => {
25 + exportedProfilingSummary = contents;
26 + });
27 +
28 + utils.act(() => {
29 + const exportProfilingSummary = prepareProfilingExport(
30 + store.profilingOperations,
31 + store.profilingSnapshots,
32 + rootID,
33 + rendererID
34 + );
35 + bridge.send('exportProfilingSummary', exportProfilingSummary);
36 + });
37 +
38 + expect(exportedProfilingSummary).toBeDefined();
39 +
40 + const importedProfilingSummary = prepareProfilingImport(
41 + ((exportedProfilingSummary: any): string)
42 + );
43 +
44 + // Sanity check that profiling snapshots are serialized correctly.
45 + expect(store.profilingSnapshots.get(rootID)).toEqual(
46 + importedProfilingSummary.profilingSnapshots.get(rootID)
47 + );
48 +
49 + // Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
50 + expect(importedProfilingSummary).toMatchSnapshot('exported data');
51 +
52 + utils.act(() => {
53 + store.importedProfilingData = importedProfilingSummary;
54 + });
55 + };
56 +
57 beforeEach(() => {
58 utils = require('./utils');
59 utils.beforeEachProfiling();
60
61 + bridge = global.bridge;
62 store = global.store;
63 store.collapseNodesByDefault = false;
64
@@ -48,15 +91,18 @@ describe('profiling', () => {
91 utils.act(() => ReactDOM.render(<Parent count={0} />, container));
92 utils.act(() => store.stopProfiling());
93
51 - let suspenseResolved = false;
94 + let profilingSummary = null;
95
53 - function Suspender({ rendererID, rootID }) {
54 - const profilingSummary = store.profilingCache.ProfilingSummary.read({
96 + function Suspender({ previousPofilingSummary, rendererID, rootID }) {
97 + profilingSummary = store.profilingCache.ProfilingSummary.read({
98 rendererID,
99 rootID,
100 });
58 - suspenseResolved = true;
59 - expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
101 + if (previousPofilingSummary != null) {
102 + expect(profilingSummary).toEqual(previousPofilingSummary);
103 + } else {
104 + expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
105 + }
106 return null;
107 }
108
@@ -66,12 +112,30 @@ describe('profiling', () => {
112 await utils.actSuspense(() =>
113 TestRenderer.create(
114 <React.Suspense fallback={null}>
69 - <Suspender rendererID={rendererID} rootID={rootID} />
115 + <Suspender
116 + previousPofilingSummary={null}
117 + rendererID={rendererID}
118 + rootID={rootID}
119 + />
120 </React.Suspense>
121 )
122 );
123
74 - expect(suspenseResolved).toBe(true);
124 + expect(profilingSummary).not.toBeNull();
125 +
126 + exportImportHelper(rendererID, rootID);
127 +
128 + await utils.actSuspense(() =>
129 + TestRenderer.create(
130 + <React.Suspense fallback={null}>
131 + <Suspender
132 + previousPofilingSummary={profilingSummary}
133 + rendererID={rendererID}
134 + rootID={rootID}
135 + />
136 + </React.Suspense>
137 + )
138 + );
139
140 done();
141 });
@@ -99,38 +163,65 @@ describe('profiling', () => {
163 utils.act(() => ReactDOM.render(<Parent count={0} />, container));
164 utils.act(() => store.stopProfiling());
165
102 - let suspenseResolved = false;
166 + const allCommitDetails = [];
167
104 - function Suspender({ commitIndex, rendererID, rootID }) {
168 + function Suspender({
169 + commitIndex,
170 + previousCommitDetails,
171 + rendererID,
172 + rootID,
173 + }) {
174 const commitDetails = store.profilingCache.CommitDetails.read({
175 commitIndex,
176 rendererID,
177 rootID,
178 });
110 - suspenseResolved = true;
111 - expect(commitDetails).toMatchSnapshot(
112 - `CommitDetails commitIndex: ${commitIndex}`
113 - );
179 + if (previousCommitDetails != null) {
180 + expect(commitDetails).toEqual(previousCommitDetails);
181 + } else {
182 + allCommitDetails.push(commitDetails);
183 + expect(commitDetails).toMatchSnapshot(
184 + `CommitDetails commitIndex: ${commitIndex}`
185 + );
186 + }
187 return null;
188 }
189
190 const rendererID = utils.getRendererID();
191 const rootID = store.roots[0];
192
120 - for (let commitIndex = 0; commitIndex <= 3; commitIndex++) {
121 - suspenseResolved = false;
193 + for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
194 await utils.actSuspense(() => {
195 TestRenderer.create(
196 <React.Suspense fallback={null}>
197 <Suspender
198 commitIndex={commitIndex}
199 + previousCommitDetails={null}
200 + rendererID={rendererID}
201 + rootID={rootID}
202 + />
203 + </React.Suspense>
204 + );
205 + });
206 + }
207 +
208 + expect(allCommitDetails).toHaveLength(4);
209 +
210 + exportImportHelper(rendererID, rootID);
211 +
212 + for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
213 + await utils.actSuspense(() => {
214 + TestRenderer.create(
215 + <React.Suspense fallback={null}>
216 + <Suspender
217 + commitIndex={commitIndex}
218 + previousCommitDetails={allCommitDetails[commitIndex]}
219 rendererID={rendererID}
220 rootID={rootID}
221 />
222 </React.Suspense>
223 );
224 });
133 - expect(suspenseResolved).toBe(true);
225 }
226
227 done();
@@ -158,18 +249,27 @@ describe('profiling', () => {
249 utils.act(() => ReactDOM.render(<Parent count={3} />, container));
250 utils.act(() => store.stopProfiling());
251
161 - let suspenseResolved = false;
252 + const allFiberCommits = [];
253
163 - function Suspender({ fiberID, rendererID, rootID }) {
254 + function Suspender({
255 + fiberID,
256 + previousFiberCommits,
257 + rendererID,
258 + rootID,
259 + }) {
260 const fiberCommits = store.profilingCache.FiberCommits.read({
261 fiberID,
262 rendererID,
263 rootID,
264 });
169 - suspenseResolved = true;
170 - expect(fiberCommits).toMatchSnapshot(
171 - `FiberCommits: element ${fiberID}`
172 - );
265 + if (previousFiberCommits != null) {
266 + expect(fiberCommits).toEqual(previousFiberCommits);
267 + } else {
268 + allFiberCommits.push(fiberCommits);
269 + expect(fiberCommits).toMatchSnapshot(
270 + `FiberCommits: element ${fiberID}`
271 + );
272 + }
273 return null;
274 }
275
@@ -177,7 +277,6 @@ describe('profiling', () => {
277 const rootID = store.roots[0];
278
279 for (let index = 0; index < store.numElements; index++) {
180 - suspenseResolved = false;
280 await utils.actSuspense(() => {
281 const fiberID = store.getElementIDAtIndex(index);
282 if (fiberID == null) {
@@ -187,13 +286,36 @@ describe('profiling', () => {
286 <React.Suspense fallback={null}>
287 <Suspender
288 fiberID={fiberID}
289 + previousFiberCommits={null}
290 + rendererID={rendererID}
291 + rootID={rootID}
292 + />
293 + </React.Suspense>
294 + );
295 + });
296 + }
297 +
298 + expect(allFiberCommits).toHaveLength(store.numElements);
299 +
300 + exportImportHelper(rendererID, rootID);
301 +
302 + for (let index = 0; index < store.numElements; index++) {
303 + await utils.actSuspense(() => {
304 + const fiberID = store.getElementIDAtIndex(index);
305 + if (fiberID == null) {
306 + throw Error(`Unexpected null ID for element at index ${index}`);
307 + }
308 + TestRenderer.create(
309 + <React.Suspense fallback={null}>
310 + <Suspender
311 + fiberID={fiberID}
312 + previousFiberCommits={allFiberCommits[index]}
313 rendererID={rendererID}
314 rootID={rootID}
315 />
316 </React.Suspense>
317 );
318 });
196 - expect(suspenseResolved).toBe(true);
319 }
320
321 done();
@@ -232,15 +354,18 @@ describe('profiling', () => {
354 );
355 utils.act(() => store.stopProfiling());
356
235 - let suspenseResolved = false;
357 + let interactions = null;
358
237 - function Suspender({ rendererID, rootID }) {
238 - const interactions = store.profilingCache.Interactions.read({
359 + function Suspender({ previousInteractions, rendererID, rootID }) {
360 + interactions = store.profilingCache.Interactions.read({
361 rendererID,
362 rootID,
363 });
242 - suspenseResolved = true;
243 - expect(interactions).toMatchSnapshot('Interactions');
364 + if (previousInteractions != null) {
365 + expect(interactions).toEqual(previousInteractions);
366 + } else {
367 + expect(interactions).toMatchSnapshot('Interactions');
368 + }
369 return null;
370 }
371
@@ -250,12 +375,30 @@ describe('profiling', () => {
375 await utils.actSuspense(() =>
376 TestRenderer.create(
377 <React.Suspense fallback={null}>
253 - <Suspender rendererID={rendererID} rootID={rootID} />
378 + <Suspender
379 + previousInteractions={null}
380 + rendererID={rendererID}
381 + rootID={rootID}
382 + />
383 </React.Suspense>
384 )
385 );
386
258 - expect(suspenseResolved).toBe(true);
387 + expect(interactions).not.toBeNull();
388 +
389 + exportImportHelper(rendererID, rootID);
390 +
391 + await utils.actSuspense(() =>
392 + TestRenderer.create(
393 + <React.Suspense fallback={null}>
394 + <Suspender
395 + previousInteractions={interactions}
396 + rendererID={rendererID}
397 + rootID={rootID}
398 + />
399 + </React.Suspense>
400 + )
401 + );
402
403 done();
404 });
src/backend/agent.js
+3 -3
@@ -151,12 +151,12 @@ export default class Agent extends EventEmitter {
151
152 exportProfilingSummary = ({
153 profilingOperations,
154 - profilingSnapshot,
154 + profilingSnapshots,
155 rendererID,
156 rootID,
157 }: {
158 profilingOperations: Array<any>,
159 - profilingSnapshot: Array<any>,
159 + profilingSnapshots: Array<any>,
160 rendererID: number,
161 rootID: number,
162 }) => {
@@ -170,7 +170,7 @@ export default class Agent extends EventEmitter {
170 {
171 ...rendererData,
172 profilingOperations,
173 - profilingSnapshot,
173 + profilingSnapshots,
174 },
175 null,
176 2
src/devtools/ProfilingCache.js
+19 -2
@@ -144,8 +144,25 @@ export default class ProfilingCache {
144 return new Promise(resolve => {
145 const importedProfilingData = this._store.importedProfilingData;
146 if (importedProfilingData !== null) {
147 - // TODO (profiling) commit details
148 - // Copy from renderer getFiberCommits()
147 + const { commitDetails } = (importedProfilingData: any);
148 + if (commitDetails != null) {
149 + const commitDurations = [];
150 + commitDetails.forEach(({ actualDurations }, commitIndex) => {
151 + for (let i = 0; i < actualDurations.length; i += 2) {
152 + if (actualDurations[i] === fiberID) {
153 + commitDurations.push(commitIndex, actualDurations[i + 1]);
154 + break;
155 + }
156 + }
157 + });
158 + this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
159 + this.onFiberCommits({
160 + commitDurations,
161 + fiberID,
162 + rootID,
163 + });
164 + return;
165 + }
166 } else if (this._store.profilingOperations.has(rootID)) {
167 this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
168 this._bridge.send('getFiberCommits', {
src/devtools/store.js
+6 -6
@@ -707,11 +707,11 @@ export default class Store extends EventEmitter {
707
708 _takeProfilingSnapshotRecursive = (
709 elementID: number,
710 - profilingSnapshot: Map<number, ProfilingSnapshotNode>
710 + profilingSnapshots: Map<number, ProfilingSnapshotNode>
711 ) => {
712 const element = this.getElementByID(elementID);
713 if (element !== null) {
714 - profilingSnapshot.set(elementID, {
714 + profilingSnapshots.set(elementID, {
715 id: elementID,
716 children: element.children.slice(0),
717 displayName: element.displayName,
@@ -719,7 +719,7 @@ export default class Store extends EventEmitter {
719 });
720
721 element.children.forEach(childID =>
722 - this._takeProfilingSnapshotRecursive(childID, profilingSnapshot)
722 + this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
723 );
724 }
725 };
@@ -1067,9 +1067,9 @@ export default class Store extends EventEmitter {
1067 this._profilingScreenshotsByRootID = new Map();
1068 this._profilingSnapshotsByRootID = new Map();
1069 this.roots.forEach(rootID => {
1070 - const profilingSnapshot = new Map();
1071 - this._profilingSnapshotsByRootID.set(rootID, profilingSnapshot);
1072 - this._takeProfilingSnapshotRecursive(rootID, profilingSnapshot);
1070 + const profilingSnapshots = new Map();
1071 + this._profilingSnapshotsByRootID.set(rootID, profilingSnapshots);
1072 + this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
1073 });
1074 }
1075
src/devtools/views/Profiler/CommitTreeBuilder.js
+6 -6
@@ -68,12 +68,12 @@ export function getCommitTree({
68 const nodes = new Map();
69
70 const { importedProfilingData } = store;
71 - const profilingSnapshot =
71 + const profilingSnapshots =
72 importedProfilingData != null
73 ? importedProfilingData.profilingSnapshots.get(rootID)
74 : store.profilingSnapshots.get(rootID);
75
76 - if (profilingSnapshot == null) {
76 + if (profilingSnapshots == null) {
77 throw Error(`Could not find profiling snapshot for root "${rootID}"`);
78 }
79
@@ -83,7 +83,7 @@ export function getCommitTree({
83 0,
84 nodes,
85 profilingSummary.initialTreeBaseDurations,
86 - profilingSnapshot
86 + profilingSnapshots
87 );
88
89 // Mutate the tree
@@ -133,9 +133,9 @@ function recursivelyInitializeTree(
133 parentID: number,
134 nodes: Map<number, CommitTreeNodeFrontend>,
135 initialTreeBaseDurations: Map<number, number>,
136 - profilingSnapshot: Map<number, ProfilingSnapshotNode>
136 + profilingSnapshots: Map<number, ProfilingSnapshotNode>
137 ): void {
138 - const node = profilingSnapshot.get(id);
138 + const node = profilingSnapshots.get(id);
139 if (node != null) {
140 nodes.set(id, {
141 id,
@@ -152,7 +152,7 @@ function recursivelyInitializeTree(
152 id,
153 nodes,
154 initialTreeBaseDurations,
155 - profilingSnapshot
155 + profilingSnapshots
156 )
157 );
158 }
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+14 -27
@@ -5,6 +5,7 @@ import { ProfilerContext } from './ProfilerContext';
5 import Button from '../Button';
6 import ButtonIcon from '../ButtonIcon';
7 import { BridgeContext, StoreContext } from '../context';
8 +import { prepareProfilingExport, prepareProfilingImport } from './utils';
9
10 import styles from './ProfilingImportExportButtons.css';
11
@@ -20,32 +21,19 @@ export default function ProfilingImportExportButtons() {
21 const inputRef = useRef<HTMLInputElement | null>(null);
22
23 const downloadData = useCallback(() => {
23 - if (rootID === null) {
24 + if (rendererID === null || rootID === null) {
25 return;
26 }
27
27 - const profilingOperationsForRoot = [];
28 - const operations = store.profilingOperations.get(rootID);
29 - if (operations != null) {
30 - operations.forEach(operations => {
31 - // Convert typed Array before JSON serialization, or it will be converted to an Object.
32 - profilingOperationsForRoot.push(Array.from(operations));
33 - });
34 - }
35 -
36 - const profilingSnapshotForRoot = [];
37 - const queue = [rootID];
38 - while (queue.length) {
39 - const id = queue.pop();
40 - profilingSnapshotForRoot.push([id, store.profilingSnapshots.get(id)]);
41 - }
42 -
43 - bridge.send('exportProfilingSummary', {
44 - profilingOperations: [[rootID, profilingOperationsForRoot]],
45 - profilingSnapshot: profilingSnapshotForRoot,
46 - rendererID,
47 - rootID,
48 - });
28 + bridge.send(
29 + 'exportProfilingSummary',
30 + prepareProfilingExport(
31 + store.profilingOperations,
32 + store.profilingSnapshots,
33 + rootID,
34 + rendererID
35 + )
36 + );
37 }, [
38 bridge,
39 rendererID,
@@ -65,11 +53,10 @@ export default function ProfilingImportExportButtons() {
53 if (input !== null && input.files.length > 0) {
54 const fileReader = new FileReader();
55 fileReader.addEventListener('load', () => {
68 - const data = JSON.parse((fileReader.result: any));
69 - data.profilingOperations = new Map(data.profilingOperations);
70 - data.profilingSnapshot = new Map(data.profilingSnapshot);
56 + const raw = ((fileReader.result: any): string);
57 + const data = prepareProfilingImport(raw);
58
72 - // TODO (profiling) Version check; warn if older version.
59 + // TODO (profiling) Catch possible version check error and show dialog.
60
61 store.importedProfilingData = ((data: any): ImportedProfilingData);
62 });
src/devtools/views/Profiler/utils.js
+53 -1
@@ -1,6 +1,10 @@
1 // @flow
2
3 -import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
3 +import type {
4 + CommitDetailsFrontend,
5 + CommitTreeFrontend,
6 + ProfilingSnapshotNode,
7 +} from './types';
8
9 const commitGradient = [
10 'var(--color-commit-gradient-0)',
@@ -43,6 +47,54 @@ export const calculateSelfDuration = (
47 return selfDuration;
48 };
49
50 +export const prepareProfilingExport = (
51 + profilingOperations: Map<number, Array<Uint32Array>>,
52 + profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
53 + rendererID: number,
54 + rootID: number
55 +) => {
56 + const profilingOperationsForRoot = [];
57 + const operations = profilingOperations.get(rootID);
58 + if (operations != null) {
59 + operations.forEach(operations => {
60 + // Convert typed Array before JSON serialization, or it will be converted to an Object.
61 + profilingOperationsForRoot.push(Array.from<number>(operations));
62 + });
63 + }
64 +
65 + // Convert Map to Object or JSON.stringify will clobber the contents.
66 + const profilingSnapshotsForRoot = {};
67 + const profilingSnapshotsMap = profilingSnapshots.get(rootID);
68 + if (profilingSnapshotsMap != null) {
69 + for (let [id, snapshot] of profilingSnapshotsMap.entries()) {
70 + profilingSnapshotsForRoot[id] = snapshot;
71 + }
72 + }
73 +
74 + return {
75 + profilingOperations: profilingOperationsForRoot,
76 + profilingSnapshots: profilingSnapshotsForRoot,
77 + rendererID,
78 + rootID,
79 + };
80 +};
81 +
82 +export const prepareProfilingImport = (raw: string) => {
83 + const parsed = JSON.parse(raw);
84 +
85 + // TODO (profiling) Version check; throw if older version.
86 +
87 + const entries = [];
88 + Object.values(parsed.profilingSnapshots).forEach(snapshot => {
89 + entries.push([(snapshot: any).id, snapshot]);
90 + });
91 +
92 + const rootID = parsed.profilingSummary.rootID;
93 + parsed.profilingOperations = new Map([[rootID, parsed.profilingOperations]]);
94 + parsed.profilingSnapshots = new Map([[rootID, new Map(entries)]]);
95 + return parsed;
96 +};
97 +
98 export const getGradientColor = (value: number) => {
99 const maxIndex = commitGradient.length - 1;
100 let index;