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

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 }