mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
331 lines
12 KiB
JavaScript
331 lines
12 KiB
JavaScript
// @flow
|
||
|
||
import EventEmitter from 'events';
|
||
import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils';
|
||
import ProfilingCache from './ProfilingCache';
|
||
import Store from './store';
|
||
|
||
import type { FrontendBridge } from 'src/bridge';
|
||
import type { ProfilingDataBackend } from 'src/backend/types';
|
||
import type {
|
||
CommitDataFrontend,
|
||
ProfilingDataForRootFrontend,
|
||
ProfilingDataFrontend,
|
||
SnapshotNode,
|
||
} from './views/Profiler/types';
|
||
|
||
export default class ProfilerStore extends EventEmitter<{|
|
||
isProcessingData: [],
|
||
isProfiling: [],
|
||
profilingData: [],
|
||
|}> {
|
||
_bridge: FrontendBridge;
|
||
|
||
// Suspense cache for lazily calculating derived profiling data.
|
||
_cache: ProfilingCache;
|
||
|
||
// Temporary store of profiling data from the backend renderer(s).
|
||
// This data will be converted to the ProfilingDataFrontend format after being collected from all renderers.
|
||
_dataBackends: Array<ProfilingDataBackend> = [];
|
||
|
||
// Data from the most recently completed profiling session,
|
||
// or data that has been imported from a previously exported session.
|
||
// This object contains all necessary data to drive the Profiler UI interface,
|
||
// even though some of it is lazily parsed/derived via the ProfilingCache.
|
||
_dataFrontend: ProfilingDataFrontend | null = null;
|
||
|
||
// Snapshot of all attached renderer IDs.
|
||
// Once profiling is finished, this snapshot will be used to query renderers for profiling data.
|
||
//
|
||
// This map is initialized when profiling starts and updated when a new root is added while profiling;
|
||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||
_initialRendererIDs: Set<number> = new Set();
|
||
|
||
// 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.
|
||
//
|
||
// This map is initialized when profiling starts and updated when a new root is added while profiling;
|
||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||
_initialSnapshotsByRootID: Map<number, Map<number, SnapshotNode>> = new Map();
|
||
|
||
// Map of root (id) to a 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.
|
||
//
|
||
// This map is only updated while profiling is in progress;
|
||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||
_inProgressOperationsByRootID: Map<number, Array<Array<number>>> = new Map();
|
||
|
||
// The backend is currently profiling.
|
||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||
_isProfiling: boolean = false;
|
||
|
||
// After profiling, data is requested from each attached renderer using this queue.
|
||
// So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
|
||
_rendererQueue: Set<number> = new Set();
|
||
|
||
_store: Store;
|
||
|
||
constructor(
|
||
bridge: FrontendBridge,
|
||
store: Store,
|
||
defaultIsProfiling: boolean
|
||
) {
|
||
super();
|
||
|
||
this._bridge = bridge;
|
||
this._isProfiling = defaultIsProfiling;
|
||
this._store = store;
|
||
|
||
bridge.addListener('operations', this.onBridgeOperations);
|
||
bridge.addListener('profilingData', this.onBridgeProfilingData);
|
||
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');
|
||
|
||
this._cache = new ProfilingCache(this);
|
||
}
|
||
|
||
getCommitData(rootID: number, commitIndex: number): CommitDataFrontend {
|
||
if (this._dataFrontend !== null) {
|
||
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
|
||
if (dataForRoot != null) {
|
||
const commitDatum = dataForRoot.commitData[commitIndex];
|
||
if (commitDatum != null) {
|
||
return commitDatum;
|
||
}
|
||
}
|
||
}
|
||
|
||
throw Error(
|
||
`Could not find commit data for root "${rootID}" and commit ${commitIndex}`
|
||
);
|
||
}
|
||
|
||
getDataForRoot(rootID: number): ProfilingDataForRootFrontend {
|
||
if (this._dataFrontend !== null) {
|
||
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
|
||
if (dataForRoot != null) {
|
||
return dataForRoot;
|
||
}
|
||
}
|
||
|
||
throw Error(`Could not find commit data for root "${rootID}"`);
|
||
}
|
||
|
||
// Profiling data has been recorded for at least one root.
|
||
get didRecordCommits(): boolean {
|
||
return (
|
||
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
|
||
);
|
||
}
|
||
|
||
get isProcessingData(): boolean {
|
||
return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
|
||
}
|
||
|
||
get isProfiling(): boolean {
|
||
return this._isProfiling;
|
||
}
|
||
|
||
get profilingCache(): ProfilingCache {
|
||
return this._cache;
|
||
}
|
||
|
||
get profilingData(): ProfilingDataFrontend | null {
|
||
return this._dataFrontend;
|
||
}
|
||
set profilingData(value: ProfilingDataFrontend | null): void {
|
||
if (this._isProfiling) {
|
||
console.warn(
|
||
'Profiling data cannot be updated while profiling is in progress.'
|
||
);
|
||
return;
|
||
}
|
||
|
||
this._dataBackends.splice(0);
|
||
this._dataFrontend = value;
|
||
this._initialRendererIDs.clear();
|
||
this._initialSnapshotsByRootID.clear();
|
||
this._inProgressOperationsByRootID.clear();
|
||
this._cache.invalidate();
|
||
|
||
this.emit('profilingData');
|
||
}
|
||
|
||
clear(): void {
|
||
this._dataBackends.splice(0);
|
||
this._dataFrontend = null;
|
||
this._initialRendererIDs.clear();
|
||
this._initialSnapshotsByRootID.clear();
|
||
this._inProgressOperationsByRootID.clear();
|
||
this._rendererQueue.clear();
|
||
|
||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||
// Note that we clear now because any existing data is "stale".
|
||
this._cache.invalidate();
|
||
|
||
this.emit('profilingData');
|
||
}
|
||
|
||
startProfiling(): void {
|
||
this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
|
||
|
||
// Don't actually update the local profiling boolean yet!
|
||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||
}
|
||
|
||
stopProfiling(): void {
|
||
this._bridge.send('stopProfiling');
|
||
|
||
// Don't actually update the local profiling boolean yet!
|
||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||
}
|
||
|
||
_takeProfilingSnapshotRecursive = (
|
||
elementID: number,
|
||
profilingSnapshots: Map<number, SnapshotNode>
|
||
) => {
|
||
const element = this._store.getElementByID(elementID);
|
||
if (element !== null) {
|
||
const snapshotNode: SnapshotNode = {
|
||
id: elementID,
|
||
children: element.children.slice(0),
|
||
displayName: element.displayName,
|
||
key: element.key,
|
||
type: element.type,
|
||
};
|
||
profilingSnapshots.set(elementID, snapshotNode);
|
||
|
||
element.children.forEach(childID =>
|
||
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
|
||
);
|
||
}
|
||
};
|
||
|
||
onBridgeOperations = (operations: Array<number>) => {
|
||
// The first two values are always rendererID and rootID
|
||
const rendererID = operations[0];
|
||
const rootID = operations[1];
|
||
|
||
if (this._isProfiling) {
|
||
let profilingOperations = this._inProgressOperationsByRootID.get(rootID);
|
||
if (profilingOperations == null) {
|
||
profilingOperations = [operations];
|
||
this._inProgressOperationsByRootID.set(rootID, profilingOperations);
|
||
} else {
|
||
profilingOperations.push(operations);
|
||
}
|
||
|
||
if (!this._initialRendererIDs.has(rendererID)) {
|
||
this._initialRendererIDs.add(rendererID);
|
||
}
|
||
|
||
if (!this._initialSnapshotsByRootID.has(rootID)) {
|
||
this._initialSnapshotsByRootID.set(rootID, new Map());
|
||
}
|
||
}
|
||
};
|
||
|
||
onBridgeProfilingData = (dataBackend: ProfilingDataBackend) => {
|
||
if (this._isProfiling) {
|
||
// This should never happen, but if it does- ignore previous profiling data.
|
||
return;
|
||
}
|
||
|
||
const { rendererID } = dataBackend;
|
||
|
||
if (!this._rendererQueue.has(rendererID)) {
|
||
throw Error(
|
||
`Unexpected profiling data update from renderer "${rendererID}"`
|
||
);
|
||
}
|
||
|
||
this._dataBackends.push(dataBackend);
|
||
this._rendererQueue.delete(rendererID);
|
||
|
||
if (this._rendererQueue.size === 0) {
|
||
this._dataFrontend = prepareProfilingDataFrontendFromBackendAndStore(
|
||
this._dataBackends,
|
||
this._inProgressOperationsByRootID,
|
||
this._initialSnapshotsByRootID
|
||
);
|
||
|
||
this._dataBackends.splice(0);
|
||
|
||
this.emit('isProcessingData');
|
||
}
|
||
};
|
||
|
||
onBridgeShutdown = () => {
|
||
this._bridge.removeListener('operations', this.onBridgeOperations);
|
||
this._bridge.removeListener('profilingData', this.onBridgeProfilingData);
|
||
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
|
||
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
|
||
};
|
||
|
||
onProfilingStatus = (isProfiling: boolean) => {
|
||
if (isProfiling) {
|
||
this._dataBackends.splice(0);
|
||
this._dataFrontend = null;
|
||
this._initialRendererIDs.clear();
|
||
this._initialSnapshotsByRootID.clear();
|
||
this._inProgressOperationsByRootID.clear();
|
||
this._rendererQueue.clear();
|
||
|
||
// Record all renderer IDs initially too (in case of unmount)
|
||
for (let rendererID of this._store.rootIDToRendererID.values()) {
|
||
if (!this._initialRendererIDs.has(rendererID)) {
|
||
this._initialRendererIDs.add(rendererID);
|
||
}
|
||
}
|
||
|
||
// Record snapshot of tree at the time profiling is started.
|
||
// This info is required to handle cases of e.g. nodes being removed during profiling.
|
||
this._store.roots.forEach(rootID => {
|
||
const profilingSnapshots = new Map();
|
||
this._initialSnapshotsByRootID.set(rootID, profilingSnapshots);
|
||
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
|
||
});
|
||
}
|
||
|
||
if (this._isProfiling !== isProfiling) {
|
||
this._isProfiling = isProfiling;
|
||
|
||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||
// Note that we clear again, in case any views read from the cache while profiling.
|
||
// (That would have resolved a now-stale value without any profiling data.)
|
||
this._cache.invalidate();
|
||
|
||
this.emit('isProfiling');
|
||
|
||
// If we've just finished a profiling session, we need to fetch data stored in each renderer interface
|
||
// and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI.
|
||
// During this time, DevTools UI should probably not be interactive.
|
||
if (!isProfiling) {
|
||
this._dataBackends.splice(0);
|
||
this._rendererQueue.clear();
|
||
|
||
this._initialRendererIDs.forEach(rendererID => {
|
||
if (!this._rendererQueue.has(rendererID)) {
|
||
this._rendererQueue.add(rendererID);
|
||
|
||
this._bridge.send('getProfilingData', { rendererID });
|
||
}
|
||
});
|
||
|
||
this.emit('isProcessingData');
|
||
}
|
||
}
|
||
};
|
||
}
|