@samitouri / QOS-React-2 / commits / 76e569992b

Cleanup profile export/import data types, add export/import test

Strengthened the I/O-boundary type conversion logic. Fixed type inconsistencies uncovered by removing `any` and by making explicit type annotations and transformations. In particular, these were likely malformed when restored from a file: - `commitDetails` - `interactions` - `initialTreeBaseDurations` The mismatches were Map vs interleaved Array.

Ivan Babak committed Apr 30, 2019 at 03:16 UTC 76e569992b259b9e636ee68dcc7719539f4b9bb8
17 files changed +657 -467
.eslintignore
+2 -1
@@ -6,6 +6,7 @@ shells/browser/firefox/build
6 shells/browser/shared/build
7 shells/dev/dist
8 vendor
9 +*.js.snap
10
11 package-lock.json
11 -yarn.lock
\ No newline at end of file
12 +yarn.lock
src/__tests__/__snapshots__/profiling-test.js.snap
+189 -213
@@ -78,81 +78,74 @@ Object {
78 }
79 `;
80
81 -exports[`profiling CommitDetails should be collected for each commit: exported data 1`] = `
81 +exports[`profiling CommitDetails should be collected for each commit: imported data 1`] = `
82 Object {
83 "commitDetails": Array [
84 Object {
85 + "actualDurations": Map {
86 + 1 => 12,
87 + 2 => 12,
88 + 3 => 0,
89 + 4 => 1,
90 + 5 => 1,
91 + },
92 "commitIndex": 0,
86 - "durations": Array [
87 - 1,
88 - 12,
89 - 0,
90 - 2,
91 - 12,
92 - 10,
93 - 3,
94 - 0,
95 - 0,
96 - 4,
97 - 1,
98 - 1,
99 - 5,
100 - 1,
101 - 1,
102 - ],
93 "interactions": Array [],
94 "rootID": 1,
95 + "selfDurations": Map {
96 + 1 => 0,
97 + 2 => 10,
98 + 3 => 0,
99 + 4 => 1,
100 + 5 => 1,
101 + },
102 },
103 Object {
104 + "actualDurations": Map {
105 + 3 => 0,
106 + 4 => 1,
107 + 6 => 2,
108 + 2 => 13,
109 + 1 => 13,
110 + },
111 "commitIndex": 1,
108 - "durations": Array [
109 - 3,
110 - 0,
111 - 0,
112 - 4,
113 - 1,
114 - 1,
115 - 6,
116 - 2,
117 - 2,
118 - 2,
119 - 13,
120 - 10,
121 - 1,
122 - 13,
123 - 0,
124 - ],
112 "interactions": Array [],
113 "rootID": 1,
114 + "selfDurations": Map {
115 + 3 => 0,
116 + 4 => 1,
117 + 6 => 2,
118 + 2 => 10,
119 + 1 => 0,
120 + },
121 },
122 Object {
123 + "actualDurations": Map {
124 + 3 => 0,
125 + 2 => 10,
126 + 1 => 10,
127 + },
128 "commitIndex": 2,
130 - "durations": Array [
131 - 3,
132 - 0,
133 - 0,
134 - 2,
135 - 10,
136 - 10,
137 - 1,
138 - 10,
139 - 0,
140 - ],
129 "interactions": Array [],
130 "rootID": 1,
131 + "selfDurations": Map {
132 + 3 => 0,
133 + 2 => 10,
134 + 1 => 0,
135 + },
136 },
137 Object {
138 + "actualDurations": Map {
139 + 2 => 10,
140 + 1 => 10,
141 + },
142 "commitIndex": 3,
146 - "durations": Array [
147 - 2,
148 - 10,
149 - 10,
150 - 1,
151 - 10,
152 - 0,
153 - ],
143 "interactions": Array [],
144 "rootID": 1,
145 + "selfDurations": Map {
146 + 2 => 10,
147 + 1 => 0,
148 + },
149 },
150 ],
151 "interactions": Object {
@@ -161,7 +154,7 @@ Object {
154 },
155 "profilingOperations": Map {
156 1 => Array [
164 - Array [
157 + Uint32Array [
158 1,
159 1,
160 17,
@@ -231,7 +224,7 @@ Object {
224 5,
225 1000,
226 ],
234 - Array [
227 + Uint32Array [
228 1,
229 1,
230 8,
@@ -267,7 +260,7 @@ Object {
260 1,
261 14000,
262 ],
270 - Array [
263 + Uint32Array [
264 1,
265 1,
266 0,
@@ -287,7 +280,7 @@ Object {
280 1,
281 11000,
282 ],
290 - Array [
283 + Uint32Array [
284 1,
285 1,
286 0,
@@ -313,7 +306,7 @@ Object {
306 35,
307 45,
308 ],
316 - "initialTreeBaseDurations": Array [],
309 + "initialTreeBaseDurations": Map {},
310 "interactionCount": 0,
311 "rootID": 1,
312 },
@@ -442,68 +435,61 @@ Object {
435 }
436 `;
437
445 -exports[`profiling FiberCommits should be collected for each rendered fiber: exported data 1`] = `
438 +exports[`profiling FiberCommits should be collected for each rendered fiber: imported data 1`] = `
439 Object {
440 "commitDetails": Array [
441 Object {
442 + "actualDurations": Map {
443 + 1 => 11,
444 + 2 => 11,
445 + 3 => 0,
446 + 4 => 1,
447 + },
448 "commitIndex": 0,
450 - "durations": Array [
451 - 1,
452 - 11,
453 - 0,
454 - 2,
455 - 11,
456 - 10,
457 - 3,
458 - 0,
459 - 0,
460 - 4,
461 - 1,
462 - 1,
463 - ],
449 "interactions": Array [],
450 "rootID": 1,
451 + "selfDurations": Map {
452 + 1 => 0,
453 + 2 => 10,
454 + 3 => 0,
455 + 4 => 1,
456 + },
457 },
458 Object {
459 + "actualDurations": Map {
460 + 3 => 0,
461 + 5 => 1,
462 + 2 => 11,
463 + 1 => 11,
464 + },
465 "commitIndex": 1,
469 - "durations": Array [
470 - 3,
471 - 0,
472 - 0,
473 - 5,
474 - 1,
475 - 1,
476 - 2,
477 - 11,
478 - 10,
479 - 1,
480 - 11,
481 - 0,
482 - ],
466 "interactions": Array [],
467 "rootID": 1,
468 + "selfDurations": Map {
469 + 3 => 0,
470 + 5 => 1,
471 + 2 => 10,
472 + 1 => 0,
473 + },
474 },
475 Object {
476 + "actualDurations": Map {
477 + 3 => 0,
478 + 5 => 1,
479 + 6 => 2,
480 + 2 => 13,
481 + 1 => 13,
482 + },
483 "commitIndex": 2,
488 - "durations": Array [
489 - 3,
490 - 0,
491 - 0,
492 - 5,
493 - 1,
494 - 1,
495 - 6,
496 - 2,
497 - 2,
498 - 2,
499 - 13,
500 - 10,
501 - 1,
502 - 13,
503 - 0,
504 - ],
484 "interactions": Array [],
485 "rootID": 1,
486 + "selfDurations": Map {
487 + 3 => 0,
488 + 5 => 1,
489 + 6 => 2,
490 + 2 => 10,
491 + 1 => 0,
492 + },
493 },
494 ],
495 "interactions": Object {
@@ -512,7 +498,7 @@ Object {
498 },
499 "profilingOperations": Map {
500 1 => Array [
515 - Array [
501 + Uint32Array [
502 1,
503 1,
504 15,
@@ -570,7 +556,7 @@ Object {
556 4,
557 1000,
558 ],
573 - Array [
559 + Uint32Array [
560 1,
561 1,
562 8,
@@ -605,7 +591,7 @@ Object {
591 1,
592 12000,
593 ],
608 - Array [
594 + Uint32Array [
595 1,
596 1,
597 8,
@@ -657,7 +643,7 @@ Object {
643 22,
644 35,
645 ],
660 - "initialTreeBaseDurations": Array [],
646 + "initialTreeBaseDurations": Map {},
647 "interactionCount": 0,
648 "rootID": 1,
649 },
@@ -666,47 +652,42 @@ Object {
652 `;
653
654 exports[`profiling Interactions should be collected for every traced interaction: Interactions 1`] = `
669 -Array [
670 - Object {
671 - "__count": 1,
672 - "commits": Array [
673 - 0,
674 - ],
675 - "id": 0,
676 - "name": "mount: one child",
677 - "timestamp": 0,
678 - },
679 - Object {
680 - "__count": 0,
681 - "commits": Array [
682 - 1,
683 - ],
684 - "id": 1,
685 - "name": "update: two children",
686 - "timestamp": 11,
687 - },
688 -]
655 +Object {
656 + "interactions": Array [
657 + Object {
658 + "__count": 1,
659 + "commits": Array [
660 + 0,
661 + ],
662 + "id": 0,
663 + "name": "mount: one child",
664 + "timestamp": 0,
665 + },
666 + Object {
667 + "__count": 0,
668 + "commits": Array [
669 + 1,
670 + ],
671 + "id": 1,
672 + "name": "update: two children",
673 + "timestamp": 11,
674 + },
675 + ],
676 + "rootID": 1,
677 +}
678 `;
679
691 -exports[`profiling Interactions should be collected for every traced interaction: exported data 1`] = `
680 +exports[`profiling Interactions should be collected for every traced interaction: imported data 1`] = `
681 Object {
682 "commitDetails": Array [
683 Object {
684 + "actualDurations": Map {
685 + 1 => 11,
686 + 2 => 11,
687 + 3 => 0,
688 + 4 => 1,
689 + },
690 "commitIndex": 0,
696 - "durations": Array [
697 - 1,
698 - 11,
699 - 0,
700 - 2,
701 - 11,
702 - 10,
703 - 3,
704 - 0,
705 - 0,
706 - 4,
707 - 1,
708 - 1,
709 - ],
691 "interactions": Array [
692 Object {
693 "__count": 1,
@@ -716,23 +697,21 @@ Object {
697 },
698 ],
699 "rootID": 1,
700 + "selfDurations": Map {
701 + 1 => 0,
702 + 2 => 10,
703 + 3 => 0,
704 + 4 => 1,
705 + },
706 },
707 Object {
708 + "actualDurations": Map {
709 + 3 => 0,
710 + 5 => 1,
711 + 2 => 11,
712 + 1 => 11,
713 + },
714 "commitIndex": 1,
722 - "durations": Array [
723 - 3,
724 - 0,
725 - 0,
726 - 5,
727 - 1,
728 - 1,
729 - 2,
730 - 11,
731 - 10,
732 - 1,
733 - 11,
734 - 0,
735 - ],
715 "interactions": Array [
716 Object {
717 "__count": 0,
@@ -742,6 +721,12 @@ Object {
721 },
722 ],
723 "rootID": 1,
724 + "selfDurations": Map {
725 + 3 => 0,
726 + 5 => 1,
727 + 2 => 10,
728 + 1 => 0,
729 + },
730 },
731 ],
732 "interactions": Object {
@@ -769,7 +754,7 @@ Object {
754 },
755 "profilingOperations": Map {
756 1 => Array [
772 - Array [
757 + Uint32Array [
758 1,
759 1,
760 15,
@@ -827,7 +812,7 @@ Object {
812 4,
813 1000,
814 ],
830 - Array [
815 + Uint32Array [
816 1,
817 1,
818 8,
@@ -876,7 +861,7 @@ Object {
861 11,
862 22,
863 ],
879 - "initialTreeBaseDurations": Array [],
864 + "initialTreeBaseDurations": Map {},
865 "interactionCount": 2,
866 "rootID": 1,
867 },
@@ -908,59 +893,55 @@ Object {
893 }
894 `;
895
911 -exports[`profiling ProfilingSummary should be collected for each commit: exported data 1`] = `
896 +exports[`profiling ProfilingSummary should be collected for each commit: imported data 1`] = `
897 Object {
898 "commitDetails": Array [
899 Object {
900 + "actualDurations": Map {
901 + 3 => 0,
902 + 4 => 1,
903 + 6 => 2,
904 + 2 => 13,
905 + 1 => 13,
906 + },
907 "commitIndex": 0,
916 - "durations": Array [
917 - 3,
918 - 0,
919 - 0,
920 - 4,
921 - 1,
922 - 1,
923 - 6,
924 - 2,
925 - 2,
926 - 2,
927 - 13,
928 - 10,
929 - 1,
930 - 13,
931 - 0,
932 - ],
908 "interactions": Array [],
909 "rootID": 1,
910 + "selfDurations": Map {
911 + 3 => 0,
912 + 4 => 1,
913 + 6 => 2,
914 + 2 => 10,
915 + 1 => 0,
916 + },
917 },
918 Object {
919 + "actualDurations": Map {
920 + 3 => 0,
921 + 2 => 10,
922 + 1 => 10,
923 + },
924 "commitIndex": 1,
938 - "durations": Array [
939 - 3,
940 - 0,
941 - 0,
942 - 2,
943 - 10,
944 - 10,
945 - 1,
946 - 10,
947 - 0,
948 - ],
925 "interactions": Array [],
926 "rootID": 1,
927 + "selfDurations": Map {
928 + 3 => 0,
929 + 2 => 10,
930 + 1 => 0,
931 + },
932 },
933 Object {
934 + "actualDurations": Map {
935 + 2 => 10,
936 + 1 => 10,
937 + },
938 "commitIndex": 2,
954 - "durations": Array [
955 - 2,
956 - 10,
957 - 10,
958 - 1,
959 - 10,
960 - 0,
961 - ],
939 "interactions": Array [],
940 "rootID": 1,
941 + "selfDurations": Map {
942 + 2 => 10,
943 + 1 => 0,
944 + },
945 },
946 ],
947 "interactions": Object {
@@ -969,7 +950,7 @@ Object {
950 },
951 "profilingOperations": Map {
952 1 => Array [
972 - Array [
953 + Uint32Array [
954 1,
955 1,
956 8,
@@ -1005,7 +986,7 @@ Object {
986 1,
987 14000,
988 ],
1008 - Array [
989 + Uint32Array [
990 1,
991 1,
992 0,
@@ -1025,7 +1006,7 @@ Object {
1006 1,
1007 11000,
1008 ],
1028 - Array [
1009 + Uint32Array [
1010 1,
1011 1,
1012 0,
@@ -1091,18 +1072,13 @@ Object {
1072 23,
1073 33,
1074 ],
1094 - "initialTreeBaseDurations": Array [
1095 - 1,
1096 - 12,
1097 - 2,
1098 - 12,
1099 - 3,
1100 - 0,
1101 - 4,
1102 - 1,
1103 - 5,
1104 - 1,
1105 - ],
1075 + "initialTreeBaseDurations": Map {
1076 + 1 => 12,
1077 + 2 => 12,
1078 + 3 => 0,
1079 + 4 => 1,
1080 + 5 => 1,
1081 + },
1082 "interactionCount": 0,
1083 "rootID": 1,
1084 },
src/__tests__/profiling-test.js
+6 -46
@@ -14,46 +14,6 @@ describe('profiling', () => {
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 -
17 beforeEach(() => {
18 utils = require('./utils');
19 utils.beforeEachProfiling();
@@ -71,10 +31,10 @@ describe('profiling', () => {
31
32 it('should throw if importing older/unsupported data', () => {
33 const {
74 - prepareProfilingImport,
34 + prepareImportedProfilingData,
35 } = require('src/devtools/views/Profiler/utils');
36 expect(() =>
77 - prepareProfilingImport(
37 + prepareImportedProfilingData(
38 JSON.stringify({
39 version: 0,
40 })
@@ -143,7 +103,7 @@ describe('profiling', () => {
103
104 expect(profilingSummary).not.toBeNull();
105
146 - exportImportHelper(rendererID, rootID);
106 + utils.exportImportHelper(bridge, store, rendererID, rootID);
107
108 await utils.actAsync(() =>
109 TestRenderer.create(
@@ -234,7 +194,7 @@ describe('profiling', () => {
194
195 expect(allCommitDetails).toHaveLength(4);
196
237 - exportImportHelper(rendererID, rootID);
197 + utils.exportImportHelper(bridge, store, rendererID, rootID);
198
199 for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
200 await utils.actAsync(() => {
@@ -466,7 +426,7 @@ describe('profiling', () => {
426
427 expect(allFiberCommits).toHaveLength(store.numElements);
428
469 - exportImportHelper(rendererID, rootID);
429 + utils.exportImportHelper(bridge, store, rendererID, rootID);
430
431 for (let index = 0; index < store.numElements; index++) {
432 await utils.actAsync(() => {
@@ -562,7 +522,7 @@ describe('profiling', () => {
522
523 expect(interactions).not.toBeNull();
524
565 - exportImportHelper(rendererID, rootID);
525 + utils.exportImportHelper(bridge, store, rendererID, rootID);
526
527 await utils.actAsync(() =>
528 TestRenderer.create(
src/__tests__/profilingCharts-test.js
+1 -1
@@ -253,7 +253,7 @@ describe('profiling charts', () => {
253 rendererID,
254 rootID,
255 });
256 - const interactions = store.profilingCache.Interactions.read({
256 + const { interactions } = store.profilingCache.Interactions.read({
257 rendererID,
258 rootID,
259 });
src/__tests__/setupTests.js
+33 -21
@@ -31,31 +31,43 @@ env.beforeEach(() => {
31
32 installHook(global);
33
34 - const bridgeListeners = [];
35 - const bridge = new Bridge({
36 - listen(callback) {
37 - bridgeListeners.push(callback);
38 - return () => {
39 - const index = bridgeListeners.indexOf(callback);
40 - if (index >= 0) {
41 - bridgeListeners.splice(index, 1);
42 - }
43 - };
44 - },
45 - send(event: string, payload: any, transferable?: Array<any>) {
46 - bridgeListeners.forEach(callback => callback({ event, payload }));
47 - },
48 - });
34 + function init() {
35 + const bridgeListeners = [];
36 + const bridge = new Bridge({
37 + listen(callback) {
38 + bridgeListeners.push(callback);
39 + return () => {
40 + const index = bridgeListeners.indexOf(callback);
41 + if (index >= 0) {
42 + bridgeListeners.splice(index, 1);
43 + }
44 + };
45 + },
46 + send(event: string, payload: any, transferable?: Array<any>) {
47 + bridgeListeners.forEach(callback => callback({ event, payload }));
48 + },
49 + });
50
50 - const agent = new Agent(bridge);
51 + const agent = new Agent(bridge);
52
52 - const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
53 + const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
54
54 - initBackend(hook, agent, global);
55 + const destroyBackend = initBackend(hook, agent, global);
56
56 - global.agent = agent;
57 - global.bridge = bridge;
58 - global.store = new Store(bridge);
57 + const store = new Store(bridge);
58 +
59 + global.agent = agent;
60 + global.bridge = bridge;
61 + global.store = store;
62 +
63 + // Reinit may be used to reset the store and the bridge during a test.
64 + global.reinit = () => {
65 + destroyBackend();
66 + init();
67 + };
68 + }
69 +
70 + init();
71 });
72 env.afterEach(() => {
73 delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
src/__tests__/utils.js
+58
@@ -4,6 +4,9 @@ import typeof ReactTestRenderer from 'react-test-renderer';
4
5 import type { ElementType } from 'src/types';
6
7 +import type Bridge from 'src/bridge';
8 +import type Store from 'src/devtools/store';
9 +
10 export function act(callback: Function): void {
11 const TestUtils = require('react-dom/test-utils');
12 TestUtils.act(() => {
@@ -129,3 +132,58 @@ export function requireTestRenderer(): ReactTestRenderer {
132 global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
133 }
134 }
135 +
136 +export function exportImportHelper(
137 + bridge: Bridge,
138 + store: Store,
139 + rendererID: number,
140 + rootID: number
141 +): void {
142 + const utils = require('./utils');
143 + const {
144 + prepareExportedProfilingSummary,
145 + prepareImportedProfilingData,
146 + } = require('src/devtools/views/Profiler/utils');
147 +
148 + let exportedProfilingDataJsonString = '';
149 + const onExportFile = ({ contents }) => {
150 + if (typeof contents === 'string') {
151 + exportedProfilingDataJsonString = (contents: string);
152 + }
153 + };
154 + bridge.addListener('exportFile', onExportFile);
155 +
156 + utils.act(() => {
157 + const exportProfilingSummary = prepareExportedProfilingSummary(
158 + store.profilingOperations,
159 + store.profilingSnapshots,
160 + rendererID,
161 + rootID
162 + );
163 + bridge.send('exportProfilingSummary', exportProfilingSummary);
164 + });
165 +
166 + // Cleanup to be able to call this again on the same bridge without memory leaks.
167 + bridge.removeListener('exportFile', onExportFile);
168 +
169 + expect(typeof exportedProfilingDataJsonString).toBe('string');
170 + expect(exportedProfilingDataJsonString).not.toBe('');
171 +
172 + const importedProfilingData = prepareImportedProfilingData(
173 + exportedProfilingDataJsonString
174 + );
175 + // Sanity check that profiling snapshots are serialized correctly.
176 + expect(store.profilingSnapshots.get(rootID)).toEqual(
177 + importedProfilingData.profilingSnapshots.get(rootID)
178 + );
179 + expect(store.profilingOperations.get(rootID)).toEqual(
180 + importedProfilingData.profilingOperations.get(rootID)
181 + );
182 +
183 + // Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
184 + expect(importedProfilingData).toMatchSnapshot('imported data');
185 +
186 + utils.act(() => {
187 + store.importedProfilingData = importedProfilingData;
188 + });
189 +}
src/backend/agent.js
+20 -22
@@ -10,6 +10,7 @@ import {
10 } from '../constants';
11 import { hideOverlay, showOverlay } from './views/Highlighter';
12
13 +import type { ExportedProfilingSummaryFromFrontend } from 'src/devtools/views/Profiler/types';
14 import type {
15 PathFrame,
16 PathMatch,
@@ -19,6 +20,8 @@ import type {
20 import type { OwnersList } from 'src/devtools/views/Components/types';
21 import type { Bridge, ComponentFilter } from '../types';
22
23 +import { prepareExportedProfilingData } from 'src/devtools/views/Profiler/utils';
24 +
25 const debug = (methodName, ...args) => {
26 if (__DEBUG__) {
27 console.log(
@@ -151,34 +154,29 @@ export default class Agent extends EventEmitter {
154 return null;
155 }
156
154 - exportProfilingSummary = ({
155 - profilingOperations,
156 - profilingSnapshots,
157 - rendererID,
158 - rootID,
159 - }: {
160 - profilingOperations: Array<any>,
161 - profilingSnapshots: Array<any>,
162 - rendererID: number,
163 - rootID: number,
164 - }) => {
157 + exportProfilingSummary = (
158 + exportedProfilingSummary: ExportedProfilingSummaryFromFrontend
159 + ): void => {
160 + const { rendererID, rootID } = exportedProfilingSummary;
161 const renderer = this._rendererInterfaces[rendererID];
162 if (renderer == null) {
163 console.warn(`Invalid renderer id "${rendererID}"`);
168 - } else {
169 - const rendererData = renderer.getProfilingDataForDownload(rootID);
164 + return;
165 + }
166 + try {
167 + const exportedProfilingDataFromRenderer = renderer.getExportedProfilingData(
168 + rootID
169 + );
170 + const exportedProfilingData = prepareExportedProfilingData(
171 + exportedProfilingDataFromRenderer,
172 + exportedProfilingSummary
173 + );
174 this._bridge.send('exportFile', {
171 - contents: JSON.stringify(
172 - {
173 - ...rendererData,
174 - profilingOperations,
175 - profilingSnapshots,
176 - },
177 - null,
178 - 2
179 - ),
175 + contents: JSON.stringify(exportedProfilingData, null, 2),
176 filename: 'profile-data.json',
177 });
178 + } catch (ex) {
179 + console.warn(`Unable to export file: ${ex.stack}`);
180 }
181 };
182
src/backend/index.js
+17 -7
@@ -9,7 +9,7 @@ export function initBackend(
9 hook: DevToolsHook,
10 agent: Agent,
11 global: Object
12 -): void {
12 +): () => void {
13 const subs = [
14 hook.sub(
15 'renderer-attached',
@@ -60,20 +60,30 @@ export function initBackend(
60 });
61
62 // Connect any new renderers that injected themselves.
63 - hook.on(
64 - 'renderer',
65 - ({ id, renderer }: { id: number, renderer: ReactRenderer }) => {
66 - attachRenderer(id, renderer);
67 - }
63 + subs.push(
64 + hook.sub(
65 + 'renderer',
66 + ({ id, renderer }: { id: number, renderer: ReactRenderer }) => {
67 + attachRenderer(id, renderer);
68 + }
69 + )
70 );
71
72 hook.emit('react-devtools', agent);
73 hook.reactDevtoolsAgent = agent;
72 - agent.addListener('shutdown', () => {
74 + const onAgentShutdown = () => {
75 subs.forEach(fn => fn());
76 hook.rendererInterfaces.forEach(rendererInterface => {
77 rendererInterface.cleanup();
78 });
79 hook.reactDevtoolsAgent = null;
80 + };
81 + agent.addListener('shutdown', onAgentShutdown);
82 + subs.push(() => {
83 + agent.removeListener('shutdown', onAgentShutdown);
84 });
85 +
86 + return () => {
87 + subs.forEach(fn => fn());
88 + };
89 }
src/backend/renderer.js
+35 -26
@@ -39,6 +39,7 @@ import { inspectHooksOfFiber } from './ReactDebugHooks';
39 import type {
40 CommitDetailsBackend,
41 DevToolsHook,
42 + ExportedProfilingDataFromRenderer,
43 Fiber,
44 FiberCommitsBackend,
45 InteractionBackend,
@@ -772,25 +773,25 @@ export function attach(
773 if (existingID !== undefined) {
774 return existingID;
775 }
775 - const id = pendingStringTable.size + 1;
776 - pendingStringTable.set(str, id);
776 + const stringID = pendingStringTable.size + 1;
777 + pendingStringTable.set(str, stringID);
778 // The string table total length needs to account
779 // both for the string length, and for the array item
780 // that contains the length itself. Hence + 1.
781 pendingStringTableLength += str.length + 1;
781 - return id;
782 + return stringID;
783 }
784
785 function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
786 const isRoot = fiber.tag === HostRoot;
786 - const id = getFiberID(getPrimaryFiber(fiber));
787 + const fiberID = getFiberID(getPrimaryFiber(fiber));
788
789 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
790 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
791
792 if (isRoot) {
793 pushOperation(TREE_OPERATION_ADD);
793 - pushOperation(id);
794 + pushOperation(fiberID);
795 pushOperation(ElementTypeRoot);
796 pushOperation(isProfilingSupported ? 1 : 0);
797 pushOperation(hasOwnerMetadata ? 1 : 0);
@@ -809,7 +810,7 @@ export function attach(
810 let displayNameStringID = getStringID(displayName);
811 let keyStringID = getStringID(key);
812 pushOperation(TREE_OPERATION_ADD);
812 - pushOperation(id);
813 + pushOperation(fiberID);
814 pushOperation(elementType);
815 pushOperation(parentID);
816 pushOperation(ownerID);
@@ -818,7 +819,7 @@ export function attach(
819 }
820
821 if (isProfilingSupported) {
821 - idToRootMap.set(id, currentRootID);
822 + idToRootMap.set(fiberID, currentRootID);
823
824 recordProfilingDurations(fiber);
825 }
@@ -971,10 +972,10 @@ export function attach(
972 }
973
974 function recordProfilingDurations(fiber: Fiber) {
974 - const id = getFiberID(getPrimaryFiber(fiber));
975 + const fiberID = getFiberID(getPrimaryFiber(fiber));
976 const { actualDuration, treeBaseDuration } = fiber;
977
977 - idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration || 0);
978 + idToTreeBaseDurationMap.set(fiberID, fiber.treeBaseDuration || 0);
979
980 if (isProfiling) {
981 const { alternate } = fiber;
@@ -989,7 +990,7 @@ export function attach(
990 (fiber.treeBaseDuration || 0) * 1000
991 );
992 pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION);
992 - pushOperation(id);
993 + pushOperation(fiberID);
994 pushOperation(treeBaseDuration);
995 }
996
@@ -1013,7 +1014,7 @@ export function attach(
1014 // In some cases actualDuration might be 0 for fibers we worked on (particularly if we're using Date.now)
1015 // In other cases (e.g. Memo) actualDuration might be greater than 0 even if we "bailed out".
1016 const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
1016 - metadata.durations.push(id, actualDuration, selfDuration);
1017 + metadata.durations.push(fiberID, actualDuration, selfDuration);
1018 metadata.maxActualDuration = Math.max(
1019 metadata.maxActualDuration,
1020 actualDuration
@@ -1990,8 +1991,8 @@ export function attach(
1991
1992 return {
1993 commitIndex,
1993 - interactions: [],
1994 durations: [],
1995 + interactions: [],
1996 rootID,
1997 };
1998 }
@@ -2072,20 +2073,28 @@ export function attach(
2073 };
2074 }
2075
2075 - function getProfilingDataForDownload(rootID: number): Object {
2076 - const commitDetails = [];
2076 + function getCommitDetailsForEachCommit(
2077 + rootID: number
2078 + ): Array<CommitDetailsBackend> {
2079 + const commitDetailsForEachCommit = [];
2080 const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
2081 rootID
2082 );
2083 if (commitProfilingMetadata != null) {
2084 for (let index = 0; index < commitProfilingMetadata.length; index++) {
2082 - commitDetails.push(getCommitDetails(rootID, index));
2085 + commitDetailsForEachCommit.push(getCommitDetails(rootID, index));
2086 }
2087 }
2088 + return commitDetailsForEachCommit;
2089 + }
2090 +
2091 + function getExportedProfilingData(
2092 + rootID: number
2093 + ): ExportedProfilingDataFromRenderer {
2094 return {
2095 version: PROFILER_EXPORT_VERSION,
2096 profilingSummary: getProfilingSummary(rootID),
2088 - commitDetails,
2097 + commitDetails: getCommitDetailsForEachCommit(rootID),
2098 interactions: getInteractions(rootID),
2099 };
2100 }
@@ -2110,14 +2119,14 @@ export function attach(
2119
2120 const initialTreeBaseDurations = [];
2121 if (initialTreeBaseDurationsMap != null) {
2113 - initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
2122 + initialTreeBaseDurationsMap.forEach((treeBaseDuration, fiberID) => {
2123 if (
2124 initialIDToRootMap != null &&
2116 - initialIDToRootMap.get(id) === rootID
2125 + initialIDToRootMap.get(fiberID) === rootID
2126 ) {
2127 // We don't need to convert milliseconds to microseconds in this case,
2128 // because the profiling summary is JSON serialized.
2120 - initialTreeBaseDurations.push(id, treeBaseDuration);
2129 + initialTreeBaseDurations.push(fiberID, treeBaseDuration);
2130 }
2131 });
2132 }
@@ -2166,11 +2175,11 @@ export function attach(
2175
2176 let forceFallbackForSuspenseIDs = new Set();
2177 function shouldSuspendFiberAccordingToSet(fiber) {
2169 - const id = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
2170 - return forceFallbackForSuspenseIDs.has(id);
2178 + const fiberID = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
2179 + return forceFallbackForSuspenseIDs.has(fiberID);
2180 }
2181
2173 - function overrideSuspense(id, forceFallback) {
2182 + function overrideSuspense(fiberID, forceFallback) {
2183 if (
2184 typeof setSuspenseHandler !== 'function' ||
2185 typeof scheduleUpdate !== 'function'
@@ -2180,19 +2189,19 @@ export function attach(
2189 );
2190 }
2191 if (forceFallback) {
2183 - forceFallbackForSuspenseIDs.add(id);
2192 + forceFallbackForSuspenseIDs.add(fiberID);
2193 if (forceFallbackForSuspenseIDs.size === 1) {
2194 // First override is added. Switch React to slower path.
2195 setSuspenseHandler(shouldSuspendFiberAccordingToSet);
2196 }
2197 } else {
2189 - forceFallbackForSuspenseIDs.delete(id);
2198 + forceFallbackForSuspenseIDs.delete(fiberID);
2199 if (forceFallbackForSuspenseIDs.size === 0) {
2200 // Last override is gone. Switch React back to fast path.
2201 setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
2202 }
2203 }
2195 - const fiber = idToFiberMap.get(id);
2204 + const fiber = idToFiberMap.get(fiberID);
2205 scheduleUpdate(fiber);
2206 }
2207
@@ -2409,7 +2418,7 @@ export function attach(
2418 findNativeByFiberID,
2419 getOwnersList,
2420 getPathForElement,
2412 - getProfilingDataForDownload,
2421 + getExportedProfilingData,
2422 getProfilingSummary,
2423 handleCommitFiberRoot,
2424 handleCommitFiberUnmount,
src/backend/types.js
+12 -2
@@ -123,7 +123,7 @@ export type InteractionBackend = {|
123
124 export type CommitDetailsBackend = {|
125 commitIndex: number,
126 - // Tuple of id, actual duration, and (computed) self duration
126 + // An interleaved array: fiberID at [i], actualDuration at [i + 1], computed selfDuration at [i + 2].
127 durations: Array<number>,
128 interactions: Array<InteractionBackend>,
129 rootID: number,
@@ -148,11 +148,19 @@ export type InteractionsBackend = {|
148 export type ProfilingSummaryBackend = {|
149 commitDurations: Array<number>,
150 commitTimes: Array<number>,
151 + // An interleaved array: fiberID at [i], initialTreeBaseDuration at [i + 1].
152 initialTreeBaseDurations: Array<number>,
153 interactionCount: number,
154 rootID: number,
155 |};
156
157 +export type ExportedProfilingDataFromRenderer = {|
158 + version: 3,
159 + profilingSummary: ProfilingSummaryBackend,
160 + commitDetails: Array<CommitDetailsBackend>,
161 + interactions: InteractionsBackend,
162 +|};
163 +
164 export type PathFrame = {|
165 key: string | null,
166 index: number,
@@ -180,7 +188,9 @@ export type RendererInterface = {
188 getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
189 getInteractions: (rootID: number) => InteractionsBackend,
190 getOwnersList: (id: number) => Array<Owner> | null,
183 - getProfilingDataForDownload: (rootID: number) => Object,
191 + getExportedProfilingData: (
192 + rootID: number
193 + ) => ExportedProfilingDataFromRenderer,
194 getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
195 getPathForElement: (id: number) => Array<PathFrame> | null,
196 handleCommitFiberRoot: (fiber: Object) => void,
src/devtools/ProfilingCache.js
+103 -82
@@ -20,7 +20,6 @@ import {
20 } from 'src/devtools/views/Profiler/RankedChartBuilder';
21
22 import type { Resource } from './cache';
23 -import type { Bridge } from '../types';
23 import type {
24 CommitDetailsBackend,
25 FiberCommitsBackend,
@@ -38,6 +37,7 @@ import type {
37 import type { ChartData as FlamegraphChartData } from 'src/devtools/views/Profiler/FlamegraphChartBuilder';
38 import type { ChartData as InteractionsChartData } from 'src/devtools/views/Profiler/InteractionsChartBuilder';
39 import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder';
40 +import type { Bridge } from 'src/types';
41
42 type CommitDetailsParams = {|
43 commitIndex: number,
@@ -97,23 +97,24 @@ export default class ProfilingCache {
97 > = createResource(
98 ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
99 return new Promise(resolve => {
100 + const pendingKey = `${rootID}-${commitIndex}`;
101 const importedProfilingData = this._store.importedProfilingData;
102 if (importedProfilingData !== null) {
102 - const { commitDetails } = (importedProfilingData: any);
103 - if (commitDetails != null && commitIndex < commitDetails.length) {
104 - const response = commitDetails[commitIndex];
105 - this._pendingCommitDetailsMap.set(
106 - `${response.rootID}-${commitIndex}`,
107 - resolve
108 - );
109 - this.onCommitDetails(response);
110 - return;
103 + const commitDetailsByCommitIndex =
104 + importedProfilingData.commitDetails;
105 + if (
106 + commitDetailsByCommitIndex != null &&
107 + commitIndex < commitDetailsByCommitIndex.length
108 + ) {
109 + const commitDetails = commitDetailsByCommitIndex[commitIndex];
110 + if (commitDetails != null) {
111 + this._pendingCommitDetailsMap.delete(pendingKey);
112 + resolve(commitDetails);
113 + return;
114 + }
115 }
116 } else if (this._store.profilingOperations.has(rootID)) {
113 - this._pendingCommitDetailsMap.set(
114 - `${rootID}-${commitIndex}`,
115 - resolve
116 - );
117 + this._pendingCommitDetailsMap.set(pendingKey, resolve);
118 this._bridge.send('getCommitDetails', {
119 commitIndex,
120 rendererID,
@@ -123,6 +124,7 @@ export default class ProfilingCache {
124 }
125
126 // If no profiling data was recorded for this root, skip the round trip.
127 + this._pendingCommitDetailsMap.delete(pendingKey);
128 resolve({
129 rootID,
130 commitIndex,
@@ -143,29 +145,27 @@ export default class ProfilingCache {
145 > = createResource(
146 ({ fiberID, rendererID, rootID }: FiberCommitsParams) => {
147 return new Promise(resolve => {
148 + const pendingKey = `${rootID}-${fiberID}`;
149 const importedProfilingData = this._store.importedProfilingData;
150 if (importedProfilingData !== null) {
148 - const { commitDetails } = (importedProfilingData: any);
149 - if (commitDetails != null) {
150 - const commitDurations = [];
151 - commitDetails.forEach(({ durations }, commitIndex) => {
152 - for (let i = 0; i < durations.length; i += 3) {
153 - if (durations[i] === fiberID) {
154 - commitDurations.push(commitIndex, durations[i + 2]);
155 - break;
156 - }
157 - }
158 - });
159 - this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
160 - this.onFiberCommits({
161 - commitDurations,
162 - fiberID,
163 - rootID,
164 - });
165 - return;
166 - }
151 + const { commitDetails } = importedProfilingData;
152 + const commitDurations = [];
153 + commitDetails.forEach(({ selfDurations }, commitIndex) => {
154 + const selfDuration = selfDurations.get(fiberID);
155 + if (selfDuration != null) {
156 + commitDurations.push(commitIndex, selfDuration);
157 + }
158 + });
159 + const fiberCommitsFrontend: FiberCommitsFrontend = {
160 + commitDurations,
161 + fiberID,
162 + rootID,
163 + };
164 + this._pendingFiberCommitsMap.delete(pendingKey);
165 + resolve(fiberCommitsFrontend);
166 + return;
167 } else if (this._store.profilingOperations.has(rootID)) {
168 - this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
168 + this._pendingFiberCommitsMap.set(pendingKey, resolve);
169 this._bridge.send('getFiberCommits', {
170 fiberID,
171 rendererID,
@@ -175,11 +175,13 @@ export default class ProfilingCache {
175 }
176
177 // If no profiling data was recorded for this root, skip the round trip.
178 - resolve({
178 + this._pendingFiberCommitsMap.delete(pendingKey);
179 + const fiberCommitsFrontend: FiberCommitsFrontend = {
180 commitDurations: [],
181 fiberID,
182 rootID,
182 - });
183 + };
184 + resolve(fiberCommitsFrontend);
185 });
186 },
187 ({ fiberID, rendererID, rootID }: FiberCommitsParams) =>
@@ -193,16 +195,18 @@ export default class ProfilingCache {
195 > = createResource(
196 ({ rendererID, rootID }: InteractionsParams) => {
197 return new Promise(resolve => {
198 + const pendingKey = rootID;
199 const importedProfilingData = this._store.importedProfilingData;
200 if (importedProfilingData !== null) {
198 - const { interactions } = (importedProfilingData: any);
199 - if (interactions != null) {
200 - this._pendingInteractionsMap.set(interactions.rootID, resolve);
201 - this.onInteractions(interactions);
201 + const interactionsFrontend: InteractionsFrontend =
202 + importedProfilingData.interactions;
203 + if (interactionsFrontend != null) {
204 + this._pendingInteractionsMap.delete(pendingKey);
205 + resolve(interactionsFrontend);
206 return;
207 }
208 } else if (this._store.profilingOperations.has(rootID)) {
205 - this._pendingInteractionsMap.set(rootID, resolve);
209 + this._pendingInteractionsMap.set(pendingKey, resolve);
210 this._bridge.send('getInteractions', {
211 rendererID,
212 rootID,
@@ -211,7 +215,12 @@ export default class ProfilingCache {
215 }
216
217 // If no profiling data was recorded for this root, skip the round trip.
214 - resolve([]);
218 + this._pendingInteractionsMap.delete(pendingKey);
219 + const interactionsFrontend: InteractionsFrontend = {
220 + interactions: [],
221 + rootID,
222 + };
223 + resolve(interactionsFrontend);
224 });
225 },
226 ({ rendererID, rootID }: ProfilingSummaryParams) => rootID
@@ -224,31 +233,32 @@ export default class ProfilingCache {
233 > = createResource(
234 ({ rendererID, rootID }: ProfilingSummaryParams) => {
235 return new Promise(resolve => {
236 + const pendingKey = rootID;
237 const importedProfilingData = this._store.importedProfilingData;
238 if (importedProfilingData !== null) {
229 - const { profilingSummary } = (importedProfilingData: any);
230 - if (profilingSummary != null) {
231 - this._pendingProfileSummaryMap.set(
232 - profilingSummary.rootID,
233 - resolve
234 - );
235 - this.onProfileSummary(profilingSummary);
239 + const profilingSummaryFrontend: ProfilingSummaryFrontend =
240 + importedProfilingData.profilingSummary;
241 + if (profilingSummaryFrontend != null) {
242 + this._pendingProfileSummaryMap.delete(pendingKey);
243 + resolve(profilingSummaryFrontend);
244 return;
245 }
246 } else if (this._store.profilingOperations.has(rootID)) {
239 - this._pendingProfileSummaryMap.set(rootID, resolve);
247 + this._pendingProfileSummaryMap.set(pendingKey, resolve);
248 this._bridge.send('getProfilingSummary', { rendererID, rootID });
249 return;
250 }
251
252 // If no profiling data was recorded for this root, skip the round trip.
245 - resolve({
253 + this._pendingProfileSummaryMap.delete(pendingKey);
254 + const profilingSummaryFrontend: ProfilingSummaryFrontend = {
255 rootID,
256 commitDurations: [],
257 commitTimes: [],
258 initialTreeBaseDurations: new Map(),
259 interactionCount: 0,
251 - });
260 + };
261 + resolve(profilingSummaryFrontend);
262 });
263 },
264 ({ rendererID, rootID }: ProfilingSummaryParams) => rootID
@@ -289,7 +299,6 @@ export default class ProfilingCache {
299 getInteractionsChartData = ({
300 interactions,
301 profilingSummary,
292 - rootID,
302 }: {|
303 interactions: Array<InteractionWithCommitsFrontend>,
304 profilingSummary: ProfilingSummaryFrontend,
@@ -328,6 +337,8 @@ export default class ProfilingCache {
337 invalidateRankedChartData();
338
339 this._pendingCommitDetailsMap.clear();
340 + this._pendingFiberCommitsMap.clear();
341 + this._pendingInteractionsMap.clear();
342 this._pendingProfileSummaryMap.clear();
343 }
344
@@ -342,21 +353,23 @@ export default class ProfilingCache {
353 if (resolve != null) {
354 this._pendingCommitDetailsMap.delete(key);
355
345 - const actualDurationsMap = new Map();
346 - const selfDurationsMap = new Map();
356 + const actualDurationsMap = new Map<number, number>();
357 + const selfDurationsMap = new Map<number, number>();
358 for (let i = 0; i < durations.length; i += 3) {
348 - const id = durations[i];
349 - actualDurationsMap.set(id, durations[i + 1]);
350 - selfDurationsMap.set(id, durations[i + 2]);
359 + const fiberID = durations[i];
360 + actualDurationsMap.set(fiberID, durations[i + 1]);
361 + selfDurationsMap.set(fiberID, durations[i + 2]);
362 }
363
353 - resolve({
354 - rootID,
355 - commitIndex,
356 - actualDurations: actualDurationsMap,
357 - selfDurations: selfDurationsMap,
358 - interactions,
359 - });
364 + resolve(
365 + ({
366 + actualDurations: actualDurationsMap,
367 + commitIndex,
368 + interactions,
369 + rootID,
370 + selfDurations: selfDurationsMap,
371 + }: CommitDetailsFrontend)
372 + );
373 }
374 };
375
@@ -370,11 +383,13 @@ export default class ProfilingCache {
383 if (resolve != null) {
384 this._pendingFiberCommitsMap.delete(key);
385
373 - resolve({
374 - commitDurations,
375 - fiberID,
376 - rootID,
377 - });
386 + resolve(
387 + ({
388 + commitDurations,
389 + fiberID,
390 + rootID,
391 + }: FiberCommitsFrontend)
392 + );
393 }
394 };
395
@@ -383,7 +398,12 @@ export default class ProfilingCache {
398 if (resolve != null) {
399 this._pendingInteractionsMap.delete(rootID);
400
386 - resolve(interactions);
401 + resolve(
402 + ({
403 + interactions,
404 + rootID,
405 + }: InteractionsFrontend)
406 + );
407 }
408 };
409
@@ -400,19 +420,20 @@ export default class ProfilingCache {
420
421 const initialTreeBaseDurationsMap = new Map();
422 for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
403 - initialTreeBaseDurationsMap.set(
404 - initialTreeBaseDurations[i],
405 - initialTreeBaseDurations[i + 1]
406 - );
423 + const fiberID = initialTreeBaseDurations[i];
424 + const initialTreeBaseDuration = initialTreeBaseDurations[i + 1];
425 + initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration);
426 }
427
409 - resolve({
410 - rootID,
411 - commitDurations,
412 - commitTimes,
413 - initialTreeBaseDurations: initialTreeBaseDurationsMap,
414 - interactionCount,
415 - });
428 + resolve(
429 + ({
430 + commitDurations,
431 + commitTimes,
432 + initialTreeBaseDurations: initialTreeBaseDurationsMap,
433 + interactionCount,
434 + rootID,
435 + }: ProfilingSummaryFrontend)
436 + );
437 }
438 };
439 }
src/devtools/store.js
+3 -2
@@ -715,13 +715,14 @@ export default class Store extends EventEmitter {
715 ) => {
716 const element = this.getElementByID(elementID);
717 if (element !== null) {
718 - profilingSnapshots.set(elementID, {
718 + const snapshotNode: ProfilingSnapshotNode = {
719 id: elementID,
720 children: element.children.slice(0),
721 displayName: element.displayName,
722 key: element.key,
723 type: element.type,
724 - });
724 + };
725 + profilingSnapshots.set(elementID, snapshotNode);
726
727 element.children.forEach(childID =>
728 this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
src/devtools/views/Profiler/Interactions.js
+1 -1
@@ -51,7 +51,7 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
51 } = useContext(ProfilerContext);
52 const { profilingCache } = useContext(StoreContext);
53
54 - const interactions = profilingCache.Interactions.read({
54 + const { interactions } = profilingCache.Interactions.read({
55 rendererID: ((rendererID: any): number),
56 rootID: ((rootID: any): number),
57 });
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+12 -14
@@ -6,12 +6,13 @@ import { ModalDialogContext } from '../ModalDialog';
6 import Button from '../Button';
7 import ButtonIcon from '../ButtonIcon';
8 import { BridgeContext, StoreContext } from '../context';
9 -import { prepareProfilingExport, prepareProfilingImport } from './utils';
9 +import {
10 + prepareExportedProfilingSummary,
11 + prepareImportedProfilingData,
12 +} from './utils';
13
14 import styles from './ProfilingImportExportButtons.css';
15
13 -import type { ImportedProfilingData } from './types';
14 -
16 export default function ProfilingImportExportButtons() {
17 const bridge = useContext(BridgeContext);
18 const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext(
@@ -28,15 +29,13 @@ export default function ProfilingImportExportButtons() {
29 return;
30 }
31
31 - bridge.send(
32 - 'exportProfilingSummary',
33 - prepareProfilingExport(
34 - store.profilingOperations,
35 - store.profilingSnapshots,
36 - rootID,
37 - rendererID
38 - )
32 + const exportedProfilingSummary = prepareExportedProfilingSummary(
33 + store.profilingOperations,
34 + store.profilingSnapshots,
35 + rootID,
36 + rendererID
37 );
38 + bridge.send('exportProfilingSummary', exportedProfilingSummary);
39 }, [
40 bridge,
41 rendererID,
@@ -58,9 +57,7 @@ export default function ProfilingImportExportButtons() {
57 fileReader.addEventListener('load', () => {
58 try {
59 const raw = ((fileReader.result: any): string);
61 - const data = prepareProfilingImport(raw);
62 -
63 - store.importedProfilingData = ((data: any): ImportedProfilingData);
60 + store.importedProfilingData = prepareImportedProfilingData(raw);
61 } catch (error) {
62 modalDialogDispatch({
63 type: 'SHOW',
@@ -76,6 +73,7 @@ export default function ProfilingImportExportButtons() {
73 });
74 }
75 });
76 + // TODO (profiling) Handle fileReader errors.
77 fileReader.readAsText(input.files[0]);
78 }
79 }, [modalDialogDispatch, store]);
src/devtools/views/Profiler/SidebarInteractions.js
+1 -1
@@ -25,7 +25,7 @@ export default function SidebarInteractions(_: Props) {
25 return <div className={styles.NothingSelected}>Nothing selected</div>;
26 }
27
28 - const interactions = profilingCache.Interactions.read({
28 + const { interactions } = profilingCache.Interactions.read({
29 rendererID: ((rendererID: any): number),
30 rootID: ((rootID: any): number),
31 });
src/devtools/views/Profiler/types.js
+38 -6
@@ -1,6 +1,11 @@
1 // @flow
2
3 import type { ElementType } from 'src/types';
4 +import type {
5 + CommitDetailsBackend,
6 + InteractionsBackend,
7 + ProfilingSummaryBackend,
8 +} from 'src/backend/types';
9
10 export type CommitTreeNodeFrontend = {|
11 id: number,
@@ -28,14 +33,17 @@ export type InteractionWithCommitsFrontend = {|
33 commits: Array<number>,
34 |};
35
31 -export type InteractionsFrontend = Array<InteractionWithCommitsFrontend>;
36 +export type InteractionsFrontend = {|
37 + interactions: Array<InteractionWithCommitsFrontend>,
38 + rootID: number,
39 +|};
40
41 export type CommitDetailsFrontend = {|
34 - rootID: number,
35 - commitIndex: number,
42 actualDurations: Map<number, number>,
37 - selfDurations: Map<number, number>,
43 + commitIndex: number,
44 interactions: Array<InteractionFrontend>,
45 + rootID: number,
46 + selfDurations: Map<number, number>,
47 |};
48
49 export type FiberCommitsFrontend = {|
@@ -68,10 +76,34 @@ export type ProfilingSnapshotNode = {|
76 |};
77
78 export type ImportedProfilingData = {|
71 - version: number,
79 + version: 3,
80 profilingOperations: Map<number, Array<Uint32Array>>,
81 profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
74 - commitDetails: CommitDetailsFrontend,
82 + commitDetails: Array<CommitDetailsFrontend>,
83 interactions: InteractionsFrontend,
84 profilingSummary: ProfilingSummaryFrontend,
85 |};
86 +
87 +export type SerializableProfilingDataOperationsByRootID = Array<
88 + [number, Array<Array<number>>]
89 +>;
90 +export type SerializableProfilingDataSnapshotsByRootID = Array<
91 + [number, Array<[number, ProfilingSnapshotNode]>]
92 +>;
93 +
94 +export type ExportedProfilingSummaryFromFrontend = {|
95 + version: 3,
96 + profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID,
97 + profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID,
98 + rendererID: number,
99 + rootID: number,
100 +|};
101 +
102 +export type ExportedProfilingData = {|
103 + version: 3,
104 + profilingOperationsByRootID: SerializableProfilingDataOperationsByRootID,
105 + profilingSnapshotsByRootID: SerializableProfilingDataSnapshotsByRootID,
106 + commitDetails: Array<CommitDetailsBackend>,
107 + interactions: InteractionsBackend,
108 + profilingSummary: ProfilingSummaryBackend,
109 +|};
src/devtools/views/Profiler/utils.js
+126 -22
@@ -2,7 +2,14 @@
2
3 import { PROFILER_EXPORT_VERSION } from 'src/constants';
4
5 -import type { ProfilingSnapshotNode } from './types';
5 +import type {
6 + ExportedProfilingSummaryFromFrontend,
7 + ExportedProfilingData,
8 + ImportedProfilingData,
9 + ProfilingSnapshotNode,
10 +} from './types';
11 +
12 +import type { ExportedProfilingDataFromRenderer } from 'src/backend/types';
13
14 const commitGradient = [
15 'var(--color-commit-gradient-0)',
@@ -17,7 +24,7 @@ const commitGradient = [
24 'var(--color-commit-gradient-9)',
25 ];
26
20 -export const prepareProfilingExport = (
27 +export const prepareExportedProfilingSummary = (
28 profilingOperations: Map<number, Array<Uint32Array>>,
29 profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
30 rendererID: number,
@@ -26,45 +33,142 @@ export const prepareProfilingExport = (
33 const profilingOperationsForRoot = [];
34 const operations = profilingOperations.get(rootID);
35 if (operations != null) {
29 - operations.forEach(operations => {
30 - // Convert typed Array before JSON serialization, or it will be converted to an Object.
31 - profilingOperationsForRoot.push(Array.from<number>(operations));
36 + operations.forEach(operationsTypedArray => {
37 + // Convert typed array to plain array before JSON serialization, or it will be converted to an Object.
38 + const operationsPlainArray = Array.from(operationsTypedArray);
39 + profilingOperationsForRoot.push(operationsPlainArray);
40 });
41 }
42
35 - // Convert Map to Object or JSON.stringify will clobber the contents.
36 - const profilingSnapshotsForRoot = {};
43 + // Convert Map to Array of key-value pairs or JSON.stringify will clobber the contents.
44 + const profilingSnapshotsForRoot = [];
45 const profilingSnapshotsMap = profilingSnapshots.get(rootID);
46 if (profilingSnapshotsMap != null) {
39 - for (let [id, snapshot] of profilingSnapshotsMap.entries()) {
40 - profilingSnapshotsForRoot[id] = snapshot;
47 + for (const [elementID, snapshotNode] of profilingSnapshotsMap.entries()) {
48 + profilingSnapshotsForRoot.push([elementID, snapshotNode]);
49 }
50 }
51
44 - return {
45 - profilingOperations: profilingOperationsForRoot,
46 - profilingSnapshots: profilingSnapshotsForRoot,
52 + const exportedProfilingSummary: ExportedProfilingSummaryFromFrontend = {
53 + version: PROFILER_EXPORT_VERSION,
54 + profilingOperationsByRootID: [[rootID, profilingOperationsForRoot]],
55 + profilingSnapshotsByRootID: [[rootID, profilingSnapshotsForRoot]],
56 rendererID,
57 rootID,
58 };
59 + return exportedProfilingSummary;
60 };
61
52 -export const prepareProfilingImport = (raw: string) => {
53 - const parsed = JSON.parse(raw);
62 +export const prepareExportedProfilingData = (
63 + exportedProfilingDataFromRenderer: ExportedProfilingDataFromRenderer,
64 + exportedProfilingSummary: ExportedProfilingSummaryFromFrontend
65 +): ExportedProfilingData => {
66 + if (exportedProfilingDataFromRenderer.version !== PROFILER_EXPORT_VERSION) {
67 + throw new Error(
68 + `Unsupported profiling data version ${
69 + exportedProfilingDataFromRenderer.version
70 + } from renderer with id "${exportedProfilingSummary.rendererID}"`
71 + );
72 + }
73 + if (exportedProfilingSummary.version !== PROFILER_EXPORT_VERSION) {
74 + throw new Error(
75 + `Unsupported profiling summary version ${
76 + exportedProfilingSummary.version
77 + } from renderer with id "${exportedProfilingSummary.rendererID}"`
78 + );
79 + }
80 + const exportedProfilingData: ExportedProfilingData = {
81 + version: PROFILER_EXPORT_VERSION,
82 + profilingSummary: exportedProfilingDataFromRenderer.profilingSummary,
83 + commitDetails: exportedProfilingDataFromRenderer.commitDetails,
84 + interactions: exportedProfilingDataFromRenderer.interactions,
85 + profilingOperationsByRootID:
86 + exportedProfilingSummary.profilingOperationsByRootID,
87 + profilingSnapshotsByRootID:
88 + exportedProfilingSummary.profilingSnapshotsByRootID,
89 + };
90 + return exportedProfilingData;
91 +};
92 +
93 +/**
94 + * This function should mirror `prepareExportedProfilingData` and `prepareExportedProfilingSummary`.
95 + */
96 +export const prepareImportedProfilingData = (
97 + exportedProfilingDataJsonString: string
98 +) => {
99 + const parsed = JSON.parse(exportedProfilingDataJsonString);
100
101 if (parsed.version !== PROFILER_EXPORT_VERSION) {
102 throw Error(`Unsupported profiler export version "${parsed.version}".`);
103 }
104
59 - const entries = [];
60 - Object.values(parsed.profilingSnapshots).forEach(snapshot => {
61 - entries.push([(snapshot: any).id, snapshot]);
62 - });
105 + // Some "exported" types in `parsed` are `...Backend`, see `prepareExportedProfilingData`,
106 + // they come to `ExportedProfilingData` from `ExportedProfilingDataFromRenderer`.
107 + // But the "imported" types in `ImportedProfilingData` are `...Frontend`,
108 + // and some of them aren't exactly the same as `...Backend` (i.e. an interleaved array versus a map).
109 + // The type annotations here help us to spot the incompatibilities and properly convert.
110 +
111 + const exportedProfilingData: ExportedProfilingData = parsed;
112
64 - const rootID = parsed.profilingSummary.rootID;
65 - parsed.profilingOperations = new Map([[rootID, parsed.profilingOperations]]);
66 - parsed.profilingSnapshots = new Map([[rootID, new Map(entries)]]);
67 - return parsed;
113 + const profilingSummaryExported = exportedProfilingData.profilingSummary;
114 + const initialTreeBaseDurations =
115 + profilingSummaryExported.initialTreeBaseDurations;
116 + const initialTreeBaseDurationsMap = new Map();
117 + for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
118 + const fiberID = initialTreeBaseDurations[i];
119 + const initialTreeBaseDuration = initialTreeBaseDurations[i + 1];
120 + initialTreeBaseDurationsMap.set(fiberID, initialTreeBaseDuration);
121 + }
122 +
123 + const importedProfilingData: ImportedProfilingData = {
124 + version: parsed.version,
125 + profilingOperations: new Map(
126 + exportedProfilingData.profilingOperationsByRootID.map(
127 + ([rootID, profilingOperationsForRoot]) => [
128 + rootID,
129 + profilingOperationsForRoot.map(operations =>
130 + Uint32Array.from(operations)
131 + ),
132 + ]
133 + )
134 + ),
135 + profilingSnapshots: new Map(
136 + exportedProfilingData.profilingSnapshotsByRootID.map(
137 + ([rootID, profilingSnapshotsForRoot]) => [
138 + rootID,
139 + new Map(profilingSnapshotsForRoot),
140 + ]
141 + )
142 + ),
143 + commitDetails: exportedProfilingData.commitDetails.map(
144 + commitDetailsBackendItem => {
145 + const durations = commitDetailsBackendItem.durations;
146 + const actualDurationsMap = new Map<number, number>();
147 + const selfDurationsMap = new Map<number, number>();
148 + for (let i = 0; i < durations.length; i += 3) {
149 + const fiberID = durations[i];
150 + actualDurationsMap.set(fiberID, durations[i + 1]);
151 + selfDurationsMap.set(fiberID, durations[i + 2]);
152 + }
153 + return {
154 + actualDurations: actualDurationsMap,
155 + selfDurations: selfDurationsMap,
156 + commitIndex: commitDetailsBackendItem.commitIndex,
157 + interactions: commitDetailsBackendItem.interactions,
158 + rootID: commitDetailsBackendItem.rootID,
159 + };
160 + }
161 + ),
162 + interactions: exportedProfilingData.interactions,
163 + profilingSummary: {
164 + rootID: profilingSummaryExported.rootID,
165 + commitDurations: profilingSummaryExported.commitDurations,
166 + commitTimes: profilingSummaryExported.commitTimes,
167 + initialTreeBaseDurations: initialTreeBaseDurationsMap,
168 + interactionCount: profilingSummaryExported.interactionCount,
169 + },
170 + };
171 + return importedProfilingData;
172 };
173
174 export const getGradientColor = (value: number) => {