From f5ba99e6babec6c4831dce26c83f3ab0758bdddb Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 11 Mar 2019 11:26:30 -0700 Subject: [PATCH] Moved some things from ProfilerContext reducer into (root) Store --- src/devtools/store.js | 64 +++++++++- .../views/Profiler/ProfilerContext.js | 65 ++-------- src/devtools/views/Profiler/reducer.js | 114 ------------------ 3 files changed, 72 insertions(+), 171 deletions(-) delete mode 100644 src/devtools/views/Profiler/reducer.js diff --git a/src/devtools/store.js b/src/devtools/store.js index c9004475d2..334eecbe34 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -25,6 +25,13 @@ const debug = (methodName, ...args) => { } }; +type ProfilingSnapshotNode = {| + id: number, + children: Array, + displayName: string | null, + key: number | string | null, +|}; + export type Capabilities = {| supportsProfiling: boolean, |}; @@ -40,10 +47,24 @@ export default class Store extends EventEmitter { // Elements are mutable (for now) to avoid excessive cloning during tree updates. _idToElement: Map = new Map(); + // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees. + _isProfiling: boolean = false; + // Total number of visible elements (within all roots). // Used for windowing purposes. _numElements: number = 0; + // List of tree mutation that occur during profiling. + // Once profiling is finished, these mutations can be used, along with the initial tree snapshots, + // to reconstruct the state of each root for each commit. + _profilingOperations: Map> = new Map(); + + // Snapshot of the state of the main Store (including all roots) when profiling started. + // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling, + // to reconstruct the state of each root for each commit. + // It's okay to use a single root to store this information because node IDs are unique across all roots. + _profilingSnapshot: Map = new Map(); + // Incremented each time the store is mutated. // This enables a passive effect to detect a mutation between render and commit phase. _revision: number = 0; @@ -65,8 +86,13 @@ export default class Store extends EventEmitter { debug('constructor', 'subscribing to Bridge'); this._bridge = bridge; - this._bridge.addListener('operations', this.onBridgeOperations); - this._bridge.addListener('shutdown', this.onBridgeShutdown); + bridge.addListener('operations', this.onBridgeOperations); + bridge.addListener('profilingStatus', this.onProfilingStatus); + bridge.addListener('shutdown', this.onBridgeShutdown); + + // It's possible that profiling has already started (e.g. "reload and start profiling") + // so the frontend needs to ask the backend for its status after mounting. + bridge.send('getProfilingStatus'); } get numElements(): number { @@ -216,6 +242,20 @@ export default class Store extends EventEmitter { return null; } + _takeProfilingSnapshotRecursive = (id: number) => { + const element = this.getElementByID(id); + if (element !== null) { + this._profilingSnapshot.set(id, { + id, + children: element.children.slice(0), + displayName: element.displayName, + key: element.key, + }); + + element.children.forEach(this._takeProfilingSnapshotRecursive); + } + }; + onBridgeOperations = (operations: Uint32Array) => { if (!(operations instanceof Uint32Array)) { // $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 { const rendererID = operations[0]; + if (this._isProfiling) { + const profilingOperations = this._profilingOperations.get(rendererID); + if (profilingOperations == null) { + this._profilingOperations.set(rendererID, [operations]); + } else { + profilingOperations.push(operations); + } + } + let addedElementIDs: Uint32Array = new Uint32Array(0); let removedElementIDs: Uint32Array = new Uint32Array(0); @@ -432,10 +481,21 @@ export default class Store extends EventEmitter { this.emit('mutated', [addedElementIDs, removedElementIDs]); }; + onProfilingStatus = (isProfiling: boolean) => { + this._isProfiling = isProfiling; + + if (isProfiling) { + this._profilingSnapshot = new Map(); + + this.roots.forEach(this._takeProfilingSnapshotRecursive); + } + }; + onBridgeShutdown = () => { debug('onBridgeShutdown', 'unsubscribing from Bridge'); this._bridge.removeListener('operations', this.onBridgeOperations); + this._bridge.removeListener('profilingStatus', this.onProfilingStatus); this._bridge.removeListener('shutdown', this.onBridgeShutdown); }; diff --git a/src/devtools/views/Profiler/ProfilerContext.js b/src/devtools/views/Profiler/ProfilerContext.js index e491f40709..a2ff8e89cf 100644 --- a/src/devtools/views/Profiler/ProfilerContext.js +++ b/src/devtools/views/Profiler/ProfilerContext.js @@ -4,19 +4,12 @@ import React, { createContext, useCallback, useContext, - useLayoutEffect, useMemo, - useReducer, + useState, } from 'react'; import { BridgeContext, StoreContext } from '../context'; -import reducer from './reducer'; -import type { - HANDLE_OPERATIONS_ACTION, - HANDLE_PROFILING_STATUS_CHANGE_ACTION, - SEND_START_PROFILING_ACTION, - SEND_STOP_PROFILING_ACTION, -} from './reducer'; +// TODO (profiling) Connect to store and listen for new roots and load data. type Context = {| hasProfilingData: boolean, @@ -36,65 +29,27 @@ function ProfilerContextController({ children }: Props) { const bridge = useContext(BridgeContext); const store = useContext(StoreContext); - // Some of this reducer's actions require access to the store. - // The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools, - // so we don't need to re-init the reducer in any special way. - const [state, dispatch] = useReducer(reducer, { - hasProfilingData: false, - isProfiling: false, - _operations: [], - _snapshot: new Map(), - }); + const [isProfiling, setIsProfiling] = useState(false); const startProfiling = useCallback(() => { bridge.send('startProfiling'); - dispatch(({ type: 'SEND_START_PROFILING' }: SEND_START_PROFILING_ACTION)); - }, [bridge, dispatch]); + setIsProfiling(true); + }, [bridge]); const stopProfiling = useCallback(() => { bridge.send('stopProfiling'); - dispatch(({ type: 'SEND_STOP_PROFILING' }: SEND_STOP_PROFILING_ACTION)); - }, [bridge, dispatch]); + setIsProfiling(false); + }, [bridge]); const value = useMemo( () => ({ - hasProfilingData: state.hasProfilingData, - isProfiling: state.isProfiling, + hasProfilingData: false, // TODO (profiling) Connect to store and listen for new roots and load data. + isProfiling, startProfiling, stopProfiling, }), - [state, startProfiling, stopProfiling] + [isProfiling, startProfiling, stopProfiling] ); - useLayoutEffect(() => { - const handleOperations = (operations: Uint32Array) => - dispatch( - ({ - type: 'HANDLE_OPERATIONS', - payload: operations, - }: HANDLE_OPERATIONS_ACTION) - ); - - const handleProfilingStatus = (isProfiling: boolean) => - dispatch( - ({ - type: 'HANDLE_PROFILING_STATUS_CHANGE', - payload: { isProfiling, store }, - }: HANDLE_PROFILING_STATUS_CHANGE_ACTION) - ); - - bridge.addListener('operations', handleOperations); - bridge.addListener('profilingStatus', handleProfilingStatus); - - // It's possible that profiling has already started (e.g. "reload and start profiling") - // so the frontend needs to ask the backend for its status after mounting. - bridge.send('getProfilingStatus'); - - return () => { - bridge.removeListener('operations', handleOperations); - bridge.removeListener('profilingStatus', handleProfilingStatus); - }; - }, [bridge, dispatch, store]); - return ( {children} diff --git a/src/devtools/views/Profiler/reducer.js b/src/devtools/views/Profiler/reducer.js deleted file mode 100644 index 117a55e373..0000000000 --- a/src/devtools/views/Profiler/reducer.js +++ /dev/null @@ -1,114 +0,0 @@ -// @flow - -import Store from '../../store'; - -type Node = {| - id: number, - children: Array, - displayName: string | null, - key: number | string | null, -|}; - -export type State = {| - hasProfilingData: boolean, - isProfiling: boolean, - - // List of tree mutation that occur during profiling. - // Once profiling is finished, these mutations can be used, along with the initial tree snapshots, - // to reconstruct the state of each root for each commit. - _operations: Array, - - // Snapshot of the state of the main Store (including all roots) when profiling started. - // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling, - // to reconstruct the state of each root for each commit. - // It's okay to use a single root to store this information because node IDs are unique across all roots. - _snapshot: Map, -|}; - -export type HANDLE_OPERATIONS_ACTION = {| - type: 'HANDLE_OPERATIONS', - payload: Uint32Array, -|}; - -export type HANDLE_PROFILING_STATUS_CHANGE_ACTION = {| - type: 'HANDLE_PROFILING_STATUS_CHANGE', - payload: { - isProfiling: boolean, - store: Store, - }, -|}; - -export type SEND_START_PROFILING_ACTION = {| - type: 'SEND_START_PROFILING', -|}; - -export type SEND_STOP_PROFILING_ACTION = {| - type: 'SEND_STOP_PROFILING', -|}; - -type Action = - | HANDLE_OPERATIONS_ACTION - | HANDLE_PROFILING_STATUS_CHANGE_ACTION - | SEND_START_PROFILING_ACTION - | SEND_STOP_PROFILING_ACTION; - -// TODO (profiling) Lift this state up so it's shared between tabs. - -export default function reducer(state: State, action: Action): State { - const { type } = action; - switch (type) { - case 'HANDLE_OPERATIONS': - if (state.isProfiling) { - const operations = ((action: any): HANDLE_OPERATIONS_ACTION).payload; - return { - ...state, - hasProfilingData: true, - _operations: state._operations.concat(operations), - }; - } else { - return state; - } - case 'HANDLE_PROFILING_STATUS_CHANGE': - const { - isProfiling, - store, - } = ((action: any): HANDLE_PROFILING_STATUS_CHANGE_ACTION).payload; - - if (isProfiling) { - const snapshot = new Map(); - - const recursiveSnapshot = id => { - const element = store.getElementByID(id); - if (element !== null) { - snapshot.set(id, { - id, - children: element.children.slice(0), - displayName: element.displayName, - key: element.key, - }); - element.children.forEach(id => recursiveSnapshot(id)); - } - }; - - store.roots.forEach(rootID => recursiveSnapshot(rootID)); - - return { - ...state, - isProfiling, - _operations: [], - _snapshot: snapshot, - }; - } else { - return { - ...state, - isProfiling, - }; - } - case 'SEND_START_PROFILING': - return { ...state, hasProfilingData: false, isProfiling: true }; - case 'SEND_STOP_PROFILING': - return { ...state, isProfiling: false }; - default: - throw new Error(`Unrecognized action "${type}"`); - } -}