Moved some things from ProfilerContext reducer into (root) Store
Brian Vaughn committed
Mar 11, 2019 at 11:26 UTC
f5ba99e6babec6c4831dce26c83f3ab0758bdddb
3 files changed
+72
-171
src/devtools/store.js
+62
-2
@@ -25,6 +25,13 @@ const debug = (methodName, ...args) => {
25
}
26
};
27
28
+type ProfilingSnapshotNode = {|
29
+ id: number,
30
+ children: Array<number>,
31
+ displayName: string | null,
32
+ key: number | string | null,
33
+|};
34
+
35
export type Capabilities = {|
36
supportsProfiling: boolean,
37
|};
@@ -40,10 +47,24 @@ export default class Store extends EventEmitter {
47
// Elements are mutable (for now) to avoid excessive cloning during tree updates.
48
_idToElement: Map<number, Element> = new Map();
49
50
+ // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
51
+ _isProfiling: boolean = false;
52
+
53
// Total number of visible elements (within all roots).
54
// Used for windowing purposes.
55
_numElements: number = 0;
56
57
+ // List of tree mutation that occur during profiling.
58
+ // Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
59
+ // to reconstruct the state of each root for each commit.
60
+ _profilingOperations: Map<number, Array<Uint32Array>> = new Map();
61
+
62
+ // Snapshot of the state of the main Store (including all roots) when profiling started.
63
+ // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
64
+ // to reconstruct the state of each root for each commit.
65
+ // It's okay to use a single root to store this information because node IDs are unique across all roots.
66
+ _profilingSnapshot: Map<number, ProfilingSnapshotNode> = new Map();
67
+
68
// Incremented each time the store is mutated.
69
// This enables a passive effect to detect a mutation between render and commit phase.
70
_revision: number = 0;
@@ -65,8 +86,13 @@ export default class Store extends EventEmitter {
86
debug('constructor', 'subscribing to Bridge');
87
88
this._bridge = bridge;
68
- this._bridge.addListener('operations', this.onBridgeOperations);
69
- this._bridge.addListener('shutdown', this.onBridgeShutdown);
89
+ bridge.addListener('operations', this.onBridgeOperations);
90
+ bridge.addListener('profilingStatus', this.onProfilingStatus);
91
+ bridge.addListener('shutdown', this.onBridgeShutdown);
92
+
93
+ // It's possible that profiling has already started (e.g. "reload and start profiling")
94
+ // so the frontend needs to ask the backend for its status after mounting.
95
+ bridge.send('getProfilingStatus');
96
}
97
98
get numElements(): number {
@@ -216,6 +242,20 @@ export default class Store extends EventEmitter {
242
return null;
243
}
244
245
+ _takeProfilingSnapshotRecursive = (id: number) => {
246
+ const element = this.getElementByID(id);
247
+ if (element !== null) {
248
+ this._profilingSnapshot.set(id, {
249
+ id,
250
+ children: element.children.slice(0),
251
+ displayName: element.displayName,
252
+ key: element.key,
253
+ });
254
+
255
+ element.children.forEach(this._takeProfilingSnapshotRecursive);
256
+ }
257
+ };
258
+
259
onBridgeOperations = (operations: Uint32Array) => {
260
if (!(operations instanceof Uint32Array)) {
261
// $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
@@ -228,6 +268,15 @@ export default class Store extends EventEmitter {
268
269
const rendererID = operations[0];
270
271
+ if (this._isProfiling) {
272
+ const profilingOperations = this._profilingOperations.get(rendererID);
273
+ if (profilingOperations == null) {
274
+ this._profilingOperations.set(rendererID, [operations]);
275
+ } else {
276
+ profilingOperations.push(operations);
277
+ }
278
+ }
279
+
280
let addedElementIDs: Uint32Array = new Uint32Array(0);
281
let removedElementIDs: Uint32Array = new Uint32Array(0);
282
@@ -432,10 +481,21 @@ export default class Store extends EventEmitter {
481
this.emit('mutated', [addedElementIDs, removedElementIDs]);
482
};
483
484
+ onProfilingStatus = (isProfiling: boolean) => {
485
+ this._isProfiling = isProfiling;
486
+
487
+ if (isProfiling) {
488
+ this._profilingSnapshot = new Map();
489
+
490
+ this.roots.forEach(this._takeProfilingSnapshotRecursive);
491
+ }
492
+ };
493
+
494
onBridgeShutdown = () => {
495
debug('onBridgeShutdown', 'unsubscribing from Bridge');
496
497
this._bridge.removeListener('operations', this.onBridgeOperations);
498
+ this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
499
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
500
};
501
src/devtools/views/Profiler/ProfilerContext.js
+10
-55
@@ -4,19 +4,12 @@ import React, {
4
createContext,
5
useCallback,
6
useContext,
7
- useLayoutEffect,
7
useMemo,
9
- useReducer,
8
+ useState,
9
} from 'react';
10
import { BridgeContext, StoreContext } from '../context';
12
-import reducer from './reducer';
11
14
-import type {
15
- HANDLE_OPERATIONS_ACTION,
16
- HANDLE_PROFILING_STATUS_CHANGE_ACTION,
17
- SEND_START_PROFILING_ACTION,
18
- SEND_STOP_PROFILING_ACTION,
19
-} from './reducer';
12
+// TODO (profiling) Connect to store and listen for new roots and load data.
13
14
type Context = {|
15
hasProfilingData: boolean,
@@ -36,65 +29,27 @@ function ProfilerContextController({ children }: Props) {
29
const bridge = useContext(BridgeContext);
30
const store = useContext(StoreContext);
31
39
- // Some of this reducer's actions require access to the store.
40
- // The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
41
- // so we don't need to re-init the reducer in any special way.
42
- const [state, dispatch] = useReducer(reducer, {
43
- hasProfilingData: false,
44
- isProfiling: false,
45
- _operations: [],
46
- _snapshot: new Map(),
47
- });
32
+ const [isProfiling, setIsProfiling] = useState(false);
33
34
const startProfiling = useCallback(() => {
35
bridge.send('startProfiling');
51
- dispatch(({ type: 'SEND_START_PROFILING' }: SEND_START_PROFILING_ACTION));
52
- }, [bridge, dispatch]);
36
+ setIsProfiling(true);
37
+ }, [bridge]);
38
const stopProfiling = useCallback(() => {
39
bridge.send('stopProfiling');
55
- dispatch(({ type: 'SEND_STOP_PROFILING' }: SEND_STOP_PROFILING_ACTION));
56
- }, [bridge, dispatch]);
40
+ setIsProfiling(false);
41
+ }, [bridge]);
42
43
const value = useMemo(
44
() => ({
60
- hasProfilingData: state.hasProfilingData,
61
- isProfiling: state.isProfiling,
45
+ hasProfilingData: false, // TODO (profiling) Connect to store and listen for new roots and load data.
46
+ isProfiling,
47
startProfiling,
48
stopProfiling,
49
}),
65
- [state, startProfiling, stopProfiling]
50
+ [isProfiling, startProfiling, stopProfiling]
51
);
52
68
- useLayoutEffect(() => {
69
- const handleOperations = (operations: Uint32Array) =>
70
- dispatch(
71
- ({
72
- type: 'HANDLE_OPERATIONS',
73
- payload: operations,
74
- }: HANDLE_OPERATIONS_ACTION)
75
- );
76
-
77
- const handleProfilingStatus = (isProfiling: boolean) =>
78
- dispatch(
79
- ({
80
- type: 'HANDLE_PROFILING_STATUS_CHANGE',
81
- payload: { isProfiling, store },
82
- }: HANDLE_PROFILING_STATUS_CHANGE_ACTION)
83
- );
84
-
85
- bridge.addListener('operations', handleOperations);
86
- bridge.addListener('profilingStatus', handleProfilingStatus);
87
-
88
- // It's possible that profiling has already started (e.g. "reload and start profiling")
89
- // so the frontend needs to ask the backend for its status after mounting.
90
- bridge.send('getProfilingStatus');
91
-
92
- return () => {
93
- bridge.removeListener('operations', handleOperations);
94
- bridge.removeListener('profilingStatus', handleProfilingStatus);
95
- };
96
- }, [bridge, dispatch, store]);
97
-
53
return (
54
<ProfilerContext.Provider value={value}>
55
{children}
src/devtools/views/Profiler/reducer.js
deleted
-114
@@ -1,114 +0,0 @@
1
-// @flow
2
-
3
-import Store from '../../store';
4
-
5
-type Node = {|
6
- id: number,
7
- children: Array<number>,
8
- displayName: string | null,
9
- key: number | string | null,
10
-|};
11
-
12
-export type State = {|
13
- hasProfilingData: boolean,
14
- isProfiling: boolean,
15
-
16
- // List of tree mutation that occur during profiling.
17
- // Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
18
- // to reconstruct the state of each root for each commit.
19
- _operations: Array<Uint32Array>,
20
-
21
- // Snapshot of the state of the main Store (including all roots) when profiling started.
22
- // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
23
- // to reconstruct the state of each root for each commit.
24
- // It's okay to use a single root to store this information because node IDs are unique across all roots.
25
- _snapshot: Map<number, Node>,
26
-|};
27
-
28
-export type HANDLE_OPERATIONS_ACTION = {|
29
- type: 'HANDLE_OPERATIONS',
30
- payload: Uint32Array,
31
-|};
32
-
33
-export type HANDLE_PROFILING_STATUS_CHANGE_ACTION = {|
34
- type: 'HANDLE_PROFILING_STATUS_CHANGE',
35
- payload: {
36
- isProfiling: boolean,
37
- store: Store,
38
- },
39
-|};
40
-
41
-export type SEND_START_PROFILING_ACTION = {|
42
- type: 'SEND_START_PROFILING',
43
-|};
44
-
45
-export type SEND_STOP_PROFILING_ACTION = {|
46
- type: 'SEND_STOP_PROFILING',
47
-|};
48
-
49
-type Action =
50
- | HANDLE_OPERATIONS_ACTION
51
- | HANDLE_PROFILING_STATUS_CHANGE_ACTION
52
- | SEND_START_PROFILING_ACTION
53
- | SEND_STOP_PROFILING_ACTION;
54
-
55
-// TODO (profiling) Lift this state up so it's shared between tabs.
56
-
57
-export default function reducer(state: State, action: Action): State {
58
- const { type } = action;
59
- switch (type) {
60
- case 'HANDLE_OPERATIONS':
61
- if (state.isProfiling) {
62
- const operations = ((action: any): HANDLE_OPERATIONS_ACTION).payload;
63
- return {
64
- ...state,
65
- hasProfilingData: true,
66
- _operations: state._operations.concat(operations),
67
- };
68
- } else {
69
- return state;
70
- }
71
- case 'HANDLE_PROFILING_STATUS_CHANGE':
72
- const {
73
- isProfiling,
74
- store,
75
- } = ((action: any): HANDLE_PROFILING_STATUS_CHANGE_ACTION).payload;
76
-
77
- if (isProfiling) {
78
- const snapshot = new Map();
79
-
80
- const recursiveSnapshot = id => {
81
- const element = store.getElementByID(id);
82
- if (element !== null) {
83
- snapshot.set(id, {
84
- id,
85
- children: element.children.slice(0),
86
- displayName: element.displayName,
87
- key: element.key,
88
- });
89
- element.children.forEach(id => recursiveSnapshot(id));
90
- }
91
- };
92
-
93
- store.roots.forEach(rootID => recursiveSnapshot(rootID));
94
-
95
- return {
96
- ...state,
97
- isProfiling,
98
- _operations: [],
99
- _snapshot: snapshot,
100
- };
101
- } else {
102
- return {
103
- ...state,
104
- isProfiling,
105
- };
106
- }
107
- case 'SEND_START_PROFILING':
108
- return { ...state, hasProfilingData: false, isProfiling: true };
109
- case 'SEND_STOP_PROFILING':
110
- return { ...state, isProfiling: false };
111
- default:
112
- throw new Error(`Unrecognized action "${type}"`);
113
- }
114
-}