mirror of
https://github.com/facebook/react.git
synced 2026-02-25 05:03:03 +00:00
348 lines
12 KiB
JavaScript
348 lines
12 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import EventEmitter from 'events';
|
|
import {prepareProfilingDataFrontendFromBackendAndStore} from './views/Profiler/utils';
|
|
import ProfilingCache from './ProfilingCache';
|
|
import Store from './store';
|
|
|
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
|
import type {ProfilingDataBackend} from 'react-devtools-shared/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;
|
|
|
|
// Tracks whether a specific renderer logged any profiling data during the most recent session.
|
|
_rendererIDsThatReportedProfilingData: Set<number> = new Set();
|
|
|
|
// 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());
|
|
}
|
|
|
|
this._rendererIDsThatReportedProfilingData.add(rendererID);
|
|
}
|
|
};
|
|
|
|
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._rendererIDsThatReportedProfilingData.clear();
|
|
this._rendererQueue.clear();
|
|
|
|
// Record all renderer IDs initially too (in case of unmount)
|
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
|
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();
|
|
|
|
// Only request data from renderers that actually logged it.
|
|
// This avoids unnecessary bridge requests and also avoids edge case mixed renderer bugs.
|
|
// (e.g. when v15 and v16 are both present)
|
|
this._rendererIDsThatReportedProfilingData.forEach(rendererID => {
|
|
if (!this._rendererQueue.has(rendererID)) {
|
|
this._rendererQueue.add(rendererID);
|
|
|
|
this._bridge.send('getProfilingData', {rendererID});
|
|
}
|
|
});
|
|
|
|
this.emit('isProcessingData');
|
|
}
|
|
}
|
|
};
|
|
}
|