Moved some things from ProfilerContext reducer into (root) Store

This commit is contained in:
Brian Vaughn
2019-03-11 11:26:30 -07:00
parent 22a3c757fb
commit f5ba99e6ba
3 changed files with 72 additions and 171 deletions
+62 -2
View File
@@ -25,6 +25,13 @@ const debug = (methodName, ...args) => {
}
};
type ProfilingSnapshotNode = {|
id: number,
children: Array<number>,
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<number, Element> = 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<number, Array<Uint32Array>> = 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<number, ProfilingSnapshotNode> = 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);
};
+10 -55
View File
@@ -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 (
<ProfilerContext.Provider value={value}>
{children}
-114
View File
@@ -1,114 +0,0 @@
// @flow
import Store from '../../store';
type Node = {|
id: number,
children: Array<number>,
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<Uint32Array>,
// 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<number, Node>,
|};
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}"`);
}
}