Initial change tracking implemented * User profiling setting * Background collects changed props/state (not added/removed or hooks yet) * Tests updated
Initial change tracking implemented * User profiling setting * Background collects changed props/state (not added/removed or hooks yet) * Tests updated
Brian Vaughn committed
Jun 7, 2019 at 15:38 UTC
bd5c2e4621c093cc8995dd51c9855d5b5dcc562f
19 files changed
+649
-41
src/__tests__/__snapshots__/profilingCache-test.js.snap
+283
@@ -2,6 +2,7 @@
2
3
exports[`ProfilingCache should calculate a self duration based on actual children (not filtered children): CommitDetails with filtered self durations 1`] = `
4
Object {
5
+ "changeDescriptions": Map {},
6
"duration": 16,
7
"fiberActualDurations": Map {
8
1 => 16,
@@ -24,6 +25,7 @@ Object {
25
26
exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 1`] = `
27
Object {
28
+ "changeDescriptions": Map {},
29
"duration": 15,
30
"fiberActualDurations": Map {
31
1 => 15,
@@ -46,6 +48,7 @@ Object {
48
49
exports[`ProfilingCache should calculate self duration correctly for suspended views: CommitDetails with filtered self durations 2`] = `
50
Object {
51
+ "changeDescriptions": Map {},
52
"duration": 3,
53
"fiberActualDurations": Map {
54
5 => 3,
@@ -64,6 +67,7 @@ Object {
67
68
exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 0 1`] = `
69
Object {
70
+ "changeDescriptions": Map {},
71
"duration": 12,
72
"fiberActualDurations": Map {
73
1 => 12,
@@ -88,6 +92,25 @@ Object {
92
93
exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 1 1`] = `
94
Object {
95
+ "changeDescriptions": Map {
96
+ 3 => Object {
97
+ "didHooksChange": false,
98
+ "props": Array [],
99
+ "state": Array [],
100
+ },
101
+ 4 => Object {
102
+ "didHooksChange": false,
103
+ "props": Array [],
104
+ "state": Array [],
105
+ },
106
+ 2 => Object {
107
+ "didHooksChange": false,
108
+ "props": Array [
109
+ "count",
110
+ ],
111
+ "state": Array [],
112
+ },
113
+ },
114
"duration": 13,
115
"fiberActualDurations": Map {
116
3 => 0,
@@ -112,6 +135,20 @@ Object {
135
136
exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 2 1`] = `
137
Object {
138
+ "changeDescriptions": Map {
139
+ 3 => Object {
140
+ "didHooksChange": false,
141
+ "props": Array [],
142
+ "state": Array [],
143
+ },
144
+ 2 => Object {
145
+ "didHooksChange": false,
146
+ "props": Array [
147
+ "count",
148
+ ],
149
+ "state": Array [],
150
+ },
151
+ },
152
"duration": 10,
153
"fiberActualDurations": Map {
154
3 => 0,
@@ -132,6 +169,15 @@ Object {
169
170
exports[`ProfilingCache should collect data for each commit: CommitDetails commitIndex: 3 1`] = `
171
Object {
172
+ "changeDescriptions": Map {
173
+ 2 => Object {
174
+ "didHooksChange": false,
175
+ "props": Array [
176
+ "count",
177
+ ],
178
+ "state": Array [],
179
+ },
180
+ },
181
"duration": 10,
182
"fiberActualDurations": Map {
183
2 => 10,
@@ -154,6 +200,7 @@ Object {
200
Object {
201
"commitData": Array [
202
Object {
203
+ "changeDescriptions": Array [],
204
"duration": 12,
205
"fiberActualDurations": Array [
206
Array [
@@ -205,6 +252,34 @@ Object {
252
"timestamp": 12,
253
},
254
Object {
255
+ "changeDescriptions": Array [
256
+ Array [
257
+ 3,
258
+ Object {
259
+ "didHooksChange": false,
260
+ "props": Array [],
261
+ "state": Array [],
262
+ },
263
+ ],
264
+ Array [
265
+ 4,
266
+ Object {
267
+ "didHooksChange": false,
268
+ "props": Array [],
269
+ "state": Array [],
270
+ },
271
+ ],
272
+ Array [
273
+ 2,
274
+ Object {
275
+ "didHooksChange": false,
276
+ "props": Array [
277
+ "count",
278
+ ],
279
+ "state": Array [],
280
+ },
281
+ ],
282
+ ],
283
"duration": 13,
284
"fiberActualDurations": Array [
285
Array [
@@ -256,6 +331,26 @@ Object {
331
"timestamp": 25,
332
},
333
Object {
334
+ "changeDescriptions": Array [
335
+ Array [
336
+ 3,
337
+ Object {
338
+ "didHooksChange": false,
339
+ "props": Array [],
340
+ "state": Array [],
341
+ },
342
+ ],
343
+ Array [
344
+ 2,
345
+ Object {
346
+ "didHooksChange": false,
347
+ "props": Array [
348
+ "count",
349
+ ],
350
+ "state": Array [],
351
+ },
352
+ ],
353
+ ],
354
"duration": 10,
355
"fiberActualDurations": Array [
356
Array [
@@ -291,6 +386,18 @@ Object {
386
"timestamp": 35,
387
},
388
Object {
389
+ "changeDescriptions": Array [
390
+ Array [
391
+ 2,
392
+ Object {
393
+ "didHooksChange": false,
394
+ "props": Array [
395
+ "count",
396
+ ],
397
+ "state": Array [],
398
+ },
399
+ ],
400
+ ],
401
"duration": 10,
402
"fiberActualDurations": Array [
403
Array [
@@ -507,6 +614,7 @@ Object {
614
Object {
615
"commitData": Array [
616
Object {
617
+ "changeDescriptions": Array [],
618
"duration": 11,
619
"fiberActualDurations": Array [
620
Array [
@@ -550,6 +658,26 @@ Object {
658
"timestamp": 11,
659
},
660
Object {
661
+ "changeDescriptions": Array [
662
+ Array [
663
+ 3,
664
+ Object {
665
+ "didHooksChange": false,
666
+ "props": Array [],
667
+ "state": Array [],
668
+ },
669
+ ],
670
+ Array [
671
+ 2,
672
+ Object {
673
+ "didHooksChange": false,
674
+ "props": Array [
675
+ "count",
676
+ ],
677
+ "state": Array [],
678
+ },
679
+ ],
680
+ ],
681
"duration": 11,
682
"fiberActualDurations": Array [
683
Array [
@@ -593,6 +721,34 @@ Object {
721
"timestamp": 22,
722
},
723
Object {
724
+ "changeDescriptions": Array [
725
+ Array [
726
+ 3,
727
+ Object {
728
+ "didHooksChange": false,
729
+ "props": Array [],
730
+ "state": Array [],
731
+ },
732
+ ],
733
+ Array [
734
+ 5,
735
+ Object {
736
+ "didHooksChange": false,
737
+ "props": Array [],
738
+ "state": Array [],
739
+ },
740
+ ],
741
+ Array [
742
+ 2,
743
+ Object {
744
+ "didHooksChange": false,
745
+ "props": Array [
746
+ "count",
747
+ ],
748
+ "state": Array [],
749
+ },
750
+ ],
751
+ ],
752
"duration": 13,
753
"fiberActualDurations": Array [
754
Array [
@@ -791,6 +947,25 @@ exports[`ProfilingCache should collect data for each root (including ones added
947
Object {
948
"commitData": Array [
949
Object {
950
+ "changeDescriptions": Map {
951
+ 3 => Object {
952
+ "didHooksChange": false,
953
+ "props": Array [],
954
+ "state": Array [],
955
+ },
956
+ 4 => Object {
957
+ "didHooksChange": false,
958
+ "props": Array [],
959
+ "state": Array [],
960
+ },
961
+ 2 => Object {
962
+ "didHooksChange": false,
963
+ "props": Array [
964
+ "count",
965
+ ],
966
+ "state": Array [],
967
+ },
968
+ },
969
"duration": 13,
970
"fiberActualDurations": Map {
971
3 => 0,
@@ -812,6 +987,20 @@ Object {
987
"timestamp": 13,
988
},
989
Object {
990
+ "changeDescriptions": Map {
991
+ 3 => Object {
992
+ "didHooksChange": false,
993
+ "props": Array [],
994
+ "state": Array [],
995
+ },
996
+ 2 => Object {
997
+ "didHooksChange": false,
998
+ "props": Array [
999
+ "count",
1000
+ ],
1001
+ "state": Array [],
1002
+ },
1003
+ },
1004
"duration": 10,
1005
"fiberActualDurations": Map {
1006
3 => 0,
@@ -829,6 +1018,15 @@ Object {
1018
"timestamp": 34,
1019
},
1020
Object {
1021
+ "changeDescriptions": Map {
1022
+ 2 => Object {
1023
+ "didHooksChange": false,
1024
+ "props": Array [
1025
+ "count",
1026
+ ],
1027
+ "state": Array [],
1028
+ },
1029
+ },
1030
"duration": 10,
1031
"fiberActualDurations": Map {
1032
2 => 10,
@@ -971,6 +1169,7 @@ exports[`ProfilingCache should collect data for each root (including ones added
1169
Object {
1170
"commitData": Array [
1171
Object {
1172
+ "changeDescriptions": Map {},
1173
"duration": 11,
1174
"fiberActualDurations": Map {
1175
11 => 11,
@@ -1063,6 +1262,7 @@ exports[`ProfilingCache should collect data for each root (including ones added
1262
Object {
1263
"commitData": Array [
1264
Object {
1265
+ "changeDescriptions": Map {},
1266
"duration": 0,
1267
"fiberActualDurations": Map {},
1268
"fiberSelfDurations": Map {},
@@ -1139,6 +1339,34 @@ Object {
1339
Object {
1340
"commitData": Array [
1341
Object {
1342
+ "changeDescriptions": Array [
1343
+ Array [
1344
+ 3,
1345
+ Object {
1346
+ "didHooksChange": false,
1347
+ "props": Array [],
1348
+ "state": Array [],
1349
+ },
1350
+ ],
1351
+ Array [
1352
+ 4,
1353
+ Object {
1354
+ "didHooksChange": false,
1355
+ "props": Array [],
1356
+ "state": Array [],
1357
+ },
1358
+ ],
1359
+ Array [
1360
+ 2,
1361
+ Object {
1362
+ "didHooksChange": false,
1363
+ "props": Array [
1364
+ "count",
1365
+ ],
1366
+ "state": Array [],
1367
+ },
1368
+ ],
1369
+ ],
1370
"duration": 13,
1371
"fiberActualDurations": Array [
1372
Array [
@@ -1190,6 +1418,26 @@ Object {
1418
"timestamp": 13,
1419
},
1420
Object {
1421
+ "changeDescriptions": Array [
1422
+ Array [
1423
+ 3,
1424
+ Object {
1425
+ "didHooksChange": false,
1426
+ "props": Array [],
1427
+ "state": Array [],
1428
+ },
1429
+ ],
1430
+ Array [
1431
+ 2,
1432
+ Object {
1433
+ "didHooksChange": false,
1434
+ "props": Array [
1435
+ "count",
1436
+ ],
1437
+ "state": Array [],
1438
+ },
1439
+ ],
1440
+ ],
1441
"duration": 10,
1442
"fiberActualDurations": Array [
1443
Array [
@@ -1225,6 +1473,18 @@ Object {
1473
"timestamp": 34,
1474
},
1475
Object {
1476
+ "changeDescriptions": Array [
1477
+ Array [
1478
+ 2,
1479
+ Object {
1480
+ "didHooksChange": false,
1481
+ "props": Array [
1482
+ "count",
1483
+ ],
1484
+ "state": Array [],
1485
+ },
1486
+ ],
1487
+ ],
1488
"duration": 10,
1489
"fiberActualDurations": Array [
1490
Array [
@@ -1406,6 +1666,7 @@ Object {
1666
Object {
1667
"commitData": Array [
1668
Object {
1669
+ "changeDescriptions": Array [],
1670
"duration": 11,
1671
"fiberActualDurations": Array [
1672
Array [
@@ -1519,6 +1780,7 @@ Object {
1780
Object {
1781
"commitData": Array [
1782
Object {
1783
+ "changeDescriptions": Array [],
1784
"duration": 0,
1785
"fiberActualDurations": Array [],
1786
"fiberSelfDurations": Array [],
@@ -1639,6 +1901,7 @@ Object {
1901
Object {
1902
"commitData": Array [
1903
Object {
1904
+ "changeDescriptions": Array [],
1905
"duration": 11,
1906
"fiberActualDurations": Array [
1907
Array [
@@ -1684,6 +1947,26 @@ Object {
1947
"timestamp": 11,
1948
},
1949
Object {
1950
+ "changeDescriptions": Array [
1951
+ Array [
1952
+ 3,
1953
+ Object {
1954
+ "didHooksChange": false,
1955
+ "props": Array [],
1956
+ "state": Array [],
1957
+ },
1958
+ ],
1959
+ Array [
1960
+ 2,
1961
+ Object {
1962
+ "didHooksChange": false,
1963
+ "props": Array [
1964
+ "count",
1965
+ ],
1966
+ "state": Array [],
1967
+ },
1968
+ ],
1969
+ ],
1970
"duration": 11,
1971
"fiberActualDurations": Array [
1972
Array [
src/__tests__/profilerContext-test.js
+1
@@ -29,6 +29,7 @@ describe('ProfilerContext', () => {
29
bridge = global.bridge;
30
store = global.store;
31
store.collapseNodesByDefault = false;
32
+ store.recordChangeDescriptions = true;
33
34
React = require('react');
35
ReactDOM = require('react-dom');
src/__tests__/profilerStore-test.js
+1
@@ -14,6 +14,7 @@ describe('ProfilerStore', () => {
14
15
store = global.store;
16
store.collapseNodesByDefault = false;
17
+ store.recordChangeDescriptions = true;
18
19
React = require('react');
20
ReactDOM = require('react-dom');
src/__tests__/profilingCache-test.js
+1
@@ -21,6 +21,7 @@ describe('ProfilingCache', () => {
21
bridge = global.bridge;
22
store = global.store;
23
store.collapseNodesByDefault = false;
24
+ store.recordChangeDescriptions = true;
25
26
React = require('react');
27
ReactDOM = require('react-dom');
src/__tests__/profilingCharts-test.js
+1
@@ -18,6 +18,7 @@ describe('profiling charts', () => {
18
19
store = global.store;
20
store.collapseNodesByDefault = false;
21
+ store.recordChangeDescriptions = true;
22
23
React = require('react');
24
ReactDOM = require('react-dom');
src/__tests__/profilingCommitTreeBuilder-test.js
+1
@@ -17,6 +17,7 @@ describe('commit tree', () => {
17
18
store = global.store;
19
store.collapseNodesByDefault = false;
20
+ store.recordChangeDescriptions = true;
21
22
React = require('react');
23
ReactDOM = require('react-dom');
src/__tests__/storeComponentFilters-test.js
+1
@@ -21,6 +21,7 @@ describe('Store component filters', () => {
21
store = global.store;
22
store.collapseNodesByDefault = false;
23
store.componentFilters = [];
24
+ store.recordChangeDescriptions = true;
25
26
React = require('react');
27
ReactDOM = require('react-dom');
src/backend/agent.js
+17
-4
@@ -6,6 +6,7 @@ import throttle from 'lodash.throttle';
6
import {
7
SESSION_STORAGE_LAST_SELECTION_KEY,
8
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
9
+ SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
10
__DEBUG__,
11
} from '../constants';
12
import {
@@ -69,6 +70,7 @@ type PersistedSelection = {|
70
export default class Agent extends EventEmitter {
71
_bridge: Bridge;
72
_isProfiling: boolean = false;
73
+ _recordChangeDescriptions: boolean = false;
74
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
75
_persistedSelection: PersistedSelection | null = null;
76
_persistedSelectionMatch: PathMatch | null = null;
@@ -79,8 +81,13 @@ export default class Agent extends EventEmitter {
81
if (
82
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
83
) {
84
+ this._recordChangeDescriptions =
85
+ sessionStorageGetItem(
86
+ SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY
87
+ ) === 'true';
88
this._isProfiling = true;
89
90
+ sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
91
sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
92
}
93
@@ -250,8 +257,12 @@ export default class Agent extends EventEmitter {
257
}
258
};
259
253
- reloadAndProfile = () => {
260
+ reloadAndProfile = (recordChangeDescriptions: boolean) => {
261
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
262
+ sessionStorageSetItem(
263
+ SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
264
+ recordChangeDescriptions ? 'true' : 'false'
265
+ );
266
267
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
268
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
@@ -358,7 +369,7 @@ export default class Agent extends EventEmitter {
369
this._rendererInterfaces[rendererID] = rendererInterface;
370
371
if (this._isProfiling) {
361
- rendererInterface.startProfiling();
372
+ rendererInterface.startProfiling(this._recordChangeDescriptions);
373
}
374
375
// When the renderer is attached, we need to tell it whether
@@ -397,13 +408,14 @@ export default class Agent extends EventEmitter {
408
window.addEventListener('pointerup', this._onPointerUp, true);
409
};
410
400
- startProfiling = () => {
411
+ startProfiling = (recordChangeDescriptions: boolean) => {
412
+ this._recordChangeDescriptions = recordChangeDescriptions;
413
this._isProfiling = true;
414
for (let rendererID in this._rendererInterfaces) {
415
const renderer = ((this._rendererInterfaces[
416
(rendererID: any)
417
]: any): RendererInterface);
406
- renderer.startProfiling();
418
+ renderer.startProfiling(recordChangeDescriptions);
419
}
420
this._bridge.send('profilingStatus', this._isProfiling);
421
};
@@ -422,6 +434,7 @@ export default class Agent extends EventEmitter {
434
435
stopProfiling = () => {
436
this._isProfiling = false;
437
+ this._recordChangeDescriptions = false;
438
for (let rendererID in this._rendererInterfaces) {
439
const renderer = ((this._rendererInterfaces[
440
(rendererID: any)
src/backend/renderer.js
+112
-6
@@ -30,6 +30,7 @@ import { cleanForBridge, copyWithSet, setInObject } from './utils';
30
import {
31
__DEBUG__,
32
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
33
+ SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
34
TREE_OPERATION_ADD,
35
TREE_OPERATION_REMOVE,
36
TREE_OPERATION_REORDER_CHILDREN,
@@ -38,6 +39,7 @@ import {
39
import { inspectHooksOfFiber } from './ReactDebugHooks';
40
41
import type {
42
+ ChangeDescription,
43
CommitDataBackend,
44
DevToolsHook,
45
Fiber,
@@ -350,7 +352,7 @@ export function attach(
352
// The unmount operations are already significantly smaller than mount opreations though.
353
// This is something to keep in mind for later.
354
function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
353
- if (this._isProfiling) {
355
+ if (isProfiling) {
356
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
357
// If necessary, we could support this- but it doesn't seem like a necessary use case.
358
throw Error('Cannot modify filter preferences while profiling');
@@ -646,8 +648,87 @@ export function attach(
648
return ((fiberToIDMap.get(primaryFiber): any): number);
649
}
650
651
+ function getChangeDescription(
652
+ prevFiber: Fiber,
653
+ nextFiber: Fiber
654
+ ): ChangeDescription | null {
655
+ switch (getElementTypeForFiber(nextFiber)) {
656
+ case ElementTypeClass:
657
+ case ElementTypeFunction:
658
+ case ElementTypeMemo:
659
+ case ElementTypeForwardRef:
660
+ return {
661
+ didHooksChange: didHooksChange(
662
+ prevFiber.memoizedState,
663
+ nextFiber.memoizedState
664
+ ),
665
+ props: getChangedKeys(
666
+ prevFiber.memoizedProps,
667
+ nextFiber.memoizedProps
668
+ ),
669
+ state: getChangedKeys(
670
+ prevFiber.memoizedState,
671
+ nextFiber.memoizedState
672
+ ),
673
+ };
674
+ default:
675
+ return null;
676
+ }
677
+ }
678
+
679
+ function didHooksChange(prev: any, next: any): boolean {
680
+ if (next == null) {
681
+ return false;
682
+ }
683
+
684
+ // We can't report anything meaningful for hooks changes.
685
+ if (
686
+ next.hasOwnProperty('baseState') &&
687
+ next.hasOwnProperty('memoizedState') &&
688
+ next.hasOwnProperty('next') &&
689
+ next.hasOwnProperty('queue')
690
+ ) {
691
+ while (next !== null) {
692
+ if (next.memoizedState !== prev.memoizedState) {
693
+ return true;
694
+ } else {
695
+ next = next.next;
696
+ prev = prev.next;
697
+ }
698
+ }
699
+ }
700
+
701
+ return false;
702
+ }
703
+
704
+ function getChangedKeys(prev: any, next: any): Array<string> {
705
+ const keys = [];
706
+
707
+ if (next == null) {
708
+ return keys;
709
+ }
710
+
711
+ // We can't report anything meaningful for hooks changes.
712
+ if (
713
+ next.hasOwnProperty('baseState') &&
714
+ next.hasOwnProperty('memoizedState') &&
715
+ next.hasOwnProperty('next') &&
716
+ next.hasOwnProperty('queue')
717
+ ) {
718
+ return keys;
719
+ }
720
+
721
+ // TODO (change descriptions) This does not account for props that were added or removed.
722
+ for (let key in prev) {
723
+ if (prev[key] !== next[key]) {
724
+ keys.push(key);
725
+ }
726
+ }
727
+ return keys;
728
+ }
729
+
730
// eslint-disable-next-line no-unused-vars
650
- function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
731
+ function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean {
732
switch (nextFiber.tag) {
733
case ClassComponent:
734
case FunctionComponent:
@@ -1024,7 +1105,7 @@ export function attach(
1105
pushOperation(treeBaseDuration);
1106
}
1107
1027
- if (alternate == null || hasDataChanged(alternate, fiber)) {
1108
+ if (alternate == null || didFiberRender(alternate, fiber)) {
1109
if (actualDuration != null) {
1110
// The actual duration reported by React includes time spent working on children.
1111
// This is useful information, but it's also useful to be able to exclude child durations.
@@ -1049,6 +1130,16 @@ export function attach(
1130
metadata.maxActualDuration,
1131
actualDuration
1132
);
1133
+
1134
+ if (recordChangeDescriptions && metadata.changeDescriptions) {
1135
+ const changeDescription =
1136
+ alternate === null
1137
+ ? null
1138
+ : getChangeDescription(alternate, fiber);
1139
+ if (changeDescription !== null) {
1140
+ metadata.changeDescriptions.set(id, changeDescription);
1141
+ }
1142
+ }
1143
}
1144
}
1145
}
@@ -1110,7 +1201,7 @@ export function attach(
1201
mostRecentlyInspectedElementID !== null &&
1202
mostRecentlyInspectedElementID ===
1203
getFiberID(getPrimaryFiber(nextFiber)) &&
1113
- hasDataChanged(prevFiber, nextFiber)
1204
+ didFiberRender(prevFiber, nextFiber)
1205
) {
1206
// If this Fiber has updated, clear cached inspected data.
1207
// If it is inspected again, it may need to be re-run to obtain updated hooks values.
@@ -1300,6 +1391,7 @@ export function attach(
1391
// If profiling is active, store commit time and duration, and the current interactions.
1392
// The frontend may request this information after profiling has stopped.
1393
currentCommitProfilingMetadata = {
1394
+ changeDescriptions: recordChangeDescriptions ? new Map() : null,
1395
durations: [],
1396
commitTime: performance.now() - profilingStartTime,
1397
interactions: Array.from(root.memoizedInteractions).map(
@@ -1343,6 +1435,7 @@ export function attach(
1435
// If profiling is active, store commit time and duration, and the current interactions.
1436
// The frontend may request this information after profiling has stopped.
1437
currentCommitProfilingMetadata = {
1438
+ changeDescriptions: recordChangeDescriptions ? new Map() : null,
1439
durations: [],
1440
commitTime: performance.now() - profilingStartTime,
1441
interactions: Array.from(root.memoizedInteractions).map(
@@ -2058,6 +2151,7 @@ export function attach(
2151
}
2152
2153
type CommitProfilingData = {|
2154
+ changeDescriptions: Map<number, ChangeDescription> | null,
2155
commitTime: number,
2156
durations: Array<number>,
2157
interactions: Array<Interaction>,
@@ -2074,6 +2168,7 @@ export function attach(
2168
let initialIDToRootMap: Map<number, number> | null = null;
2169
let isProfiling: boolean = false;
2170
let profilingStartTime: number = 0;
2171
+ let recordChangeDescriptions: boolean = false;
2172
let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null;
2173
2174
function getProfilingData(): ProfilingDataBackend {
@@ -2111,6 +2206,7 @@ export function attach(
2206
2207
commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => {
2208
const {
2209
+ changeDescriptions,
2210
durations,
2211
interactions,
2212
maxActualDuration,
@@ -2144,6 +2240,10 @@ export function attach(
2240
}
2241
2242
commitData.push({
2243
+ changeDescriptions:
2244
+ changeDescriptions !== null
2245
+ ? Array.from(changeDescriptions.entries())
2246
+ : null,
2247
duration: maxActualDuration,
2248
fiberActualDurations,
2249
fiberSelfDurations,
@@ -2170,11 +2270,13 @@ export function attach(
2270
};
2271
}
2272
2173
- function startProfiling() {
2273
+ function startProfiling(shouldRecordChangeDescriptions: boolean) {
2274
if (isProfiling) {
2275
return;
2276
}
2277
2278
+ recordChangeDescriptions = shouldRecordChangeDescriptions;
2279
+
2280
// Capture initial values as of the time profiling starts.
2281
// It's important we snapshot both the durations and the id-to-root map,
2282
// since either of these may change during the profiling session
@@ -2198,13 +2300,17 @@ export function attach(
2300
2301
function stopProfiling() {
2302
isProfiling = false;
2303
+ recordChangeDescriptions = false;
2304
}
2305
2306
// Automatically start profiling so that we don't miss timing info from initial "mount".
2307
if (
2308
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
2309
) {
2207
- startProfiling();
2310
+ startProfiling(
2311
+ sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
2312
+ 'true'
2313
+ );
2314
}
2315
2316
// React will switch between these implementations depending on whether
src/backend/types.js
+10
-1
@@ -112,7 +112,16 @@ export type ReactRenderer = {
112
currentDispatcherRef?: {| current: null | Dispatcher |},
113
};
114
115
+// TODO (change descriptions) Is it important to handle context?
116
+export type ChangeDescription = {|
117
+ didHooksChange: boolean,
118
+ props: Array<string>,
119
+ state: Array<string>,
120
+|};
121
+
122
export type CommitDataBackend = {|
123
+ // Tuple of fiber ID and change description
124
+ changeDescriptions: Array<[number, ChangeDescription]> | null,
125
duration: number,
126
// Tuple of fiber ID and actual duration
127
fiberActualDurations: Array<[number, number]>,
@@ -226,7 +235,7 @@ export type RendererInterface = {
235
setInProps: (id: number, path: Array<string | number>, value: any) => void,
236
setInState: (id: number, path: Array<string | number>, value: any) => void,
237
setTrackedPath: (path: Array<PathFrame> | null) => void,
229
- startProfiling: () => void,
238
+ startProfiling: (recordChangeDescriptions: boolean) => void,
239
stopProfiling: () => void,
240
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
241
};
src/constants.js
+3
@@ -14,6 +14,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
14
export const SESSION_STORAGE_LAST_SELECTION_KEY =
15
'React::DevTools::lastSelection';
16
17
+export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
18
+ 'React::DevTools::recordChangeDescriptions';
19
+
20
export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
21
'React::DevTools::reloadAndProfile';
22
src/devtools/ProfilerStore.js
+1
-1
@@ -179,7 +179,7 @@ export default class ProfilerStore extends EventEmitter {
179
}
180
181
startProfiling(): void {
182
- this._bridge.send('startProfiling');
182
+ this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
183
184
// Don't actually update the local profiling boolean yet!
185
// Wait for onProfilingStatus() to confirm the status has changed.
src/devtools/store.js
+22
@@ -38,6 +38,8 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
38
'React::DevTools::captureScreenshots';
39
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
40
'React::DevTools::collapseNodesByDefault';
41
+const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
42
+ 'React::DevTools::recordChangeDescriptions';
43
44
type Config = {|
45
isProfiling?: boolean,
@@ -83,6 +85,8 @@ export default class Store extends EventEmitter {
85
86
_profilerStore: ProfilerStore;
87
88
+ _recordChangeDescriptions: boolean = false;
89
+
90
// Incremented each time the store is mutated.
91
// This enables a passive effect to detect a mutation between render and commit phase.
92
_revision: number = 0;
@@ -118,6 +122,10 @@ export default class Store extends EventEmitter {
122
localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !==
123
'false';
124
125
+ this._recordChangeDescriptions =
126
+ localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
127
+ 'true';
128
+
129
this._componentFilters = getSavedComponentFilters();
130
131
let isProfiling = false;
@@ -248,6 +256,20 @@ export default class Store extends EventEmitter {
256
return this._profilerStore;
257
}
258
259
+ get recordChangeDescriptions(): boolean {
260
+ return this._recordChangeDescriptions;
261
+ }
262
+ set recordChangeDescriptions(value: boolean): void {
263
+ this._recordChangeDescriptions = value;
264
+
265
+ localStorageSetItem(
266
+ LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
267
+ value ? 'true' : 'false'
268
+ );
269
+
270
+ this.emit('recordChangeDescriptions');
271
+ }
272
+
273
get revision(): number {
274
return this._revision;
275
}
src/devtools/views/Profiler/ReloadAndProfileButton.js
+23
-9
@@ -7,27 +7,41 @@ import { BridgeContext, StoreContext } from '../context';
7
import { useSubscription } from '../hooks';
8
import Store from 'src/devtools/store';
9
10
+type SubscriptionData = {|
11
+ recordChangeDescriptions: boolean,
12
+ supportsReloadAndProfile: boolean,
13
+|};
14
+
15
export default function ReloadAndProfileButton() {
16
const bridge = useContext(BridgeContext);
17
const store = useContext(StoreContext);
18
14
- const supportsReloadAndProfileSubscription = useMemo(
19
+ const subscription = useMemo(
20
() => ({
16
- getCurrentValue: () => store.supportsReloadAndProfile,
21
+ getCurrentValue: () => ({
22
+ recordChangeDescriptions: store.recordChangeDescriptions,
23
+ supportsReloadAndProfile: store.supportsReloadAndProfile,
24
+ }),
25
subscribe: (callback: Function) => {
26
+ store.addListener('recordChangeDescriptions', callback);
27
store.addListener('supportsReloadAndProfile', callback);
19
- return () => store.removeListener('supportsReloadAndProfile', callback);
28
+ return () => {
29
+ store.removeListener('recordChangeDescriptions', callback);
30
+ store.removeListener('supportsReloadAndProfile', callback);
31
+ };
32
},
33
}),
34
[store]
35
);
24
- const supportsReloadAndProfile = useSubscription<boolean, Store>(
25
- supportsReloadAndProfileSubscription
26
- );
36
+ const {
37
+ recordChangeDescriptions,
38
+ supportsReloadAndProfile,
39
+ } = useSubscription<SubscriptionData, Store>(subscription);
40
28
- const reloadAndProfile = useCallback(() => bridge.send('reloadAndProfile'), [
29
- bridge,
30
- ]);
41
+ const reloadAndProfile = useCallback(
42
+ () => bridge.send('reloadAndProfile', recordChangeDescriptions),
43
+ [bridge, recordChangeDescriptions]
44
+ );
45
46
if (!supportsReloadAndProfile) {
47
return null;
src/devtools/views/Profiler/SidebarSelectedFiberInfo.css
+19
@@ -56,3 +56,22 @@
56
.CurrentCommit:focus {
57
outline: none;
58
}
59
+
60
+.WhatChangedItem {
61
+ margin-top: 0.25rem;
62
+}
63
+
64
+.WhatChangedKey {
65
+ font-family: var(--font-family-monospace);
66
+ font-size: var(--font-size-monospace-small);
67
+ line-height: 1;
68
+}
69
+.WhatChangedKey:first-of-type::before {
70
+ content: ' (';
71
+}
72
+.WhatChangedKey::after {
73
+ content: ', ';
74
+}
75
+.WhatChangedKey:last-of-type::after {
76
+ content: ')';
77
+}
src/devtools/views/Profiler/SidebarSelectedFiberInfo.js
+78
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React, { Fragment, useContext } from 'react';
4
+import ProfilerStore from 'src/devtools/ProfilerStore';
5
import { ProfilerContext } from './ProfilerContext';
6
import { formatDuration, formatTime } from './utils';
7
import { StoreContext } from '../context';
@@ -67,9 +68,86 @@ export default function SidebarSelectedFiberInfo(_: Props) {
68
<ButtonIcon type="close" />
69
</Button>
70
</div>
71
+ <WhatChanged
72
+ commitIndex={((selectedCommitIndex: any): number)}
73
+ fiberID={((selectedFiberID: any): number)}
74
+ profilerStore={profilerStore}
75
+ rootID={((rootID: any): number)}
76
+ />
77
<div className={styles.Content}>
78
<label className={styles.Label}>Rendered at</label>: {listItems}
79
</div>
80
</Fragment>
81
);
82
}
83
+
84
+type WhatChangedProps = {|
85
+ commitIndex: number,
86
+ fiberID: number,
87
+ profilerStore: ProfilerStore,
88
+ rootID: number,
89
+|};
90
+
91
+function WhatChanged({
92
+ commitIndex,
93
+ fiberID,
94
+ profilerStore,
95
+ rootID,
96
+}: WhatChangedProps) {
97
+ const { changeDescriptions } = profilerStore.getCommitData(
98
+ ((rootID: any): number),
99
+ commitIndex
100
+ );
101
+ if (changeDescriptions === null) {
102
+ return null;
103
+ }
104
+
105
+ const changeDescription = changeDescriptions.get(fiberID);
106
+ if (changeDescription == null) {
107
+ return null;
108
+ }
109
+
110
+ const changes = [];
111
+ if (changeDescription.didHooksChange) {
112
+ changes.push(
113
+ <div key="hooks" className={styles.WhatChangedItem}>
114
+ • Hooks
115
+ </div>
116
+ );
117
+ }
118
+ if (changeDescription.props.length !== 0) {
119
+ changes.push(
120
+ <div key="props" className={styles.WhatChangedItem}>
121
+ • Props
122
+ {changeDescription.props.map(key => (
123
+ <span key={key} className={styles.WhatChangedKey}>
124
+ {key}
125
+ </span>
126
+ ))}
127
+ </div>
128
+ );
129
+ }
130
+ if (changeDescription.state.length !== 0) {
131
+ changes.push(
132
+ <div key="state" className={styles.WhatChangedItem}>
133
+ • State
134
+ {changeDescription.state.map(key => (
135
+ <span key={key} className={styles.WhatChangedKey}>
136
+ {key}
137
+ </span>
138
+ ))}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ if (changes.length === 0) {
144
+ changes.push(<div className={styles.WhatChangedItem}>Nothing</div>);
145
+ }
146
+
147
+ return (
148
+ <div className={styles.Content}>
149
+ <label className={styles.Label}>What changed?</label>
150
+ {changes}
151
+ </div>
152
+ );
153
+}
src/devtools/views/Profiler/types.js
+11
@@ -31,7 +31,17 @@ export type SnapshotNode = {|
31
type: ElementType,
32
|};
33
34
+// TODO (change descriptions) Is it important to handle context?
35
+export type ChangeDescription = {|
36
+ didHooksChange: boolean,
37
+ props: Array<string>,
38
+ state: Array<string>,
39
+|};
40
+
41
export type CommitDataFrontend = {|
42
+ // Map of Fiber (ID) to a description of what changed in this commit.
43
+ changeDescriptions: Map<number, ChangeDescription> | null,
44
+
45
// How long was this commit?
46
duration: number,
47
@@ -93,6 +103,7 @@ export type ProfilingDataFrontend = {|
103
|};
104
105
export type CommitDataExport = {|
106
+ changeDescriptions: Array<[number, ChangeDescription]> | null,
107
duration: number,
108
// Tuple of fiber ID and actual duration
109
fiberActualDurations: Array<[number, number]>,
src/devtools/views/Profiler/utils.js
+12
@@ -58,6 +58,10 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
58
59
dataForRoots.set(rootID, {
60
commitData: commitData.map((commitDataBackend, commitIndex) => ({
61
+ changeDescriptions:
62
+ commitDataBackend.changeDescriptions != null
63
+ ? new Map(commitDataBackend.changeDescriptions)
64
+ : null,
65
duration: commitDataBackend.duration,
66
fiberActualDurations: new Map(
67
commitDataBackend.fiberActualDurations
@@ -109,6 +113,7 @@ export function prepareProfilingDataFrontendFromExport(
113
dataForRoots.set(rootID, {
114
commitData: commitData.map(
115
({
116
+ changeDescriptions,
117
duration,
118
fiberActualDurations,
119
fiberSelfDurations,
@@ -117,6 +122,8 @@ export function prepareProfilingDataFrontendFromExport(
122
screenshot,
123
timestamp,
124
}) => ({
125
+ changeDescriptions:
126
+ changeDescriptions != null ? new Map(changeDescriptions) : null,
127
duration,
128
fiberActualDurations: new Map(fiberActualDurations),
129
fiberSelfDurations: new Map(fiberSelfDurations),
@@ -159,6 +166,7 @@ export function prepareProfilingDataExport(
166
dataForRoots.push({
167
commitData: commitData.map(
168
({
169
+ changeDescriptions,
170
duration,
171
fiberActualDurations,
172
fiberSelfDurations,
@@ -167,6 +175,10 @@ export function prepareProfilingDataExport(
175
screenshot,
176
timestamp,
177
}) => ({
178
+ changeDescriptions:
179
+ changeDescriptions != null
180
+ ? Array.from(changeDescriptions.entries())
181
+ : null,
182
duration,
183
fiberActualDurations: Array.from(fiberActualDurations.entries()),
184
fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
src/devtools/views/Settings/Settings.js
+52
-20
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { useCallback, useContext, useMemo } from 'react';
3
+import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4
import { useSubscription } from '../hooks';
5
import { StoreContext } from '../context';
6
import { SettingsContext } from './SettingsContext';
@@ -43,6 +43,20 @@ function Settings(_: {||}) {
43
collapseNodesByDefaultSubscription
44
);
45
46
+ const recordChangeDescriptionsSubscription = useMemo(
47
+ () => ({
48
+ getCurrentValue: () => store.recordChangeDescriptions,
49
+ subscribe: (callback: Function) => {
50
+ store.addListener('recordChangeDescriptions', callback);
51
+ return () => store.removeListener('recordChangeDescriptions', callback);
52
+ },
53
+ }),
54
+ [store]
55
+ );
56
+ const recordChangeDescriptions = useSubscription<boolean, Store>(
57
+ recordChangeDescriptionsSubscription
58
+ );
59
+
60
const updateDisplayDensity = useCallback(
61
({ currentTarget }) => {
62
setDisplayDensity(currentTarget.value);
@@ -69,6 +83,12 @@ function Settings(_: {||}) {
83
},
84
[store]
85
);
86
+ const updateRecordChangeDescriptions = useCallback(
87
+ ({ currentTarget }) => {
88
+ store.recordChangeDescriptions = currentTarget.checked;
89
+ },
90
+ [store]
91
+ );
92
93
return (
94
<div className={styles.Settings}>
@@ -145,25 +165,37 @@ function Settings(_: {||}) {
165
</label>
166
</div>
167
148
- {store.supportsCaptureScreenshots && (
149
- <div className={styles.Section}>
150
- <div className={styles.Header}>Profiler</div>
151
- <label className={styles.CheckboxOption}>
152
- <input
153
- type="checkbox"
154
- checked={captureScreenshots}
155
- onChange={updateCaptureScreenshotsWhileProfiling}
156
- />{' '}
157
- Capture screenshots while profiling
158
- </label>
159
- {captureScreenshots && (
160
- <div className={styles.ScreenshotThrottling}>
161
- Screenshots will be throttled in order to reduce the negative
162
- impact on performance.
163
- </div>
164
- )}
165
- </div>
166
- )}
168
+ <div className={styles.Section}>
169
+ <div className={styles.Header}>Profiler</div>
170
+
171
+ <label className={styles.CheckboxOption}>
172
+ <input
173
+ type="checkbox"
174
+ checked={recordChangeDescriptions}
175
+ onChange={updateRecordChangeDescriptions}
176
+ />{' '}
177
+ Record which props/state/hooks changed while profiling
178
+ </label>
179
+
180
+ {store.supportsCaptureScreenshots && (
181
+ <Fragment>
182
+ <label className={styles.CheckboxOption}>
183
+ <input
184
+ type="checkbox"
185
+ checked={captureScreenshots}
186
+ onChange={updateCaptureScreenshotsWhileProfiling}
187
+ />{' '}
188
+ Capture screenshots while profiling
189
+ </label>
190
+ {captureScreenshots && (
191
+ <div className={styles.ScreenshotThrottling}>
192
+ Screenshots will be throttled in order to reduce the negative
193
+ impact on performance.
194
+ </div>
195
+ )}
196
+ </Fragment>
197
+ )}
198
+ </div>
199
</div>
200
);
201
}