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) => {