@samitouri / QOS-React / commits / f5ba99e6ba

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 -}