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;
_cache: ProfilingCache;
_dataBackends: Array<ProfilingDataBackend> = [];
_dataFrontend: ProfilingDataFrontend | null = null;
_initialRendererIDs: Set<number> = new Set();
_initialSnapshotsByRootID: Map<number, Map<number, SnapshotNode>> = new Map();
_inProgressOperationsByRootID: Map<number, Array<Array<number>>> = new Map();
_isProfiling: boolean = false;
_rendererIDsThatReportedProfilingData: Set<number> = new Set();
_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);
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}"`);
}
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();
this._cache.invalidate();
this.emit('profilingData');
}
startProfiling(): void {
this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
}
stopProfiling(): void {
this._bridge.send('stopProfiling');
}
_takeProfilingSnapshotRecursive: (
elementID: number,
profilingSnapshots: Map<number, SnapshotNode>,
) => void = (elementID, profilingSnapshots) => {
const element = this._store.getElementByID(elementID);
if (element !== null) {
const snapshotNode: SnapshotNode = {
id: elementID,
children: element.children.slice(0),
displayName: element.displayName,
hocDisplayNames: element.hocDisplayNames,
key: element.key,
type: element.type,
compiledWithForget: element.compiledWithForget,
};
profilingSnapshots.set(elementID, snapshotNode);
element.children.forEach(childID =>
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots),
);
}
};
onBridgeOperations: (operations: Array<number>) => void = operations => {
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) => void =
dataBackend => {
if (this._isProfiling) {
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: () => void = () => {
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) => void = isProfiling => {
if (this._isProfiling === isProfiling) {
return;
}
if (isProfiling) {
this._dataBackends.splice(0);
this._dataFrontend = null;
this._initialRendererIDs.clear();
this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear();
this._rendererIDsThatReportedProfilingData.clear();
this._rendererQueue.clear();
for (const rendererID of this._store.rootIDToRendererID.values()) {
if (!this._initialRendererIDs.has(rendererID)) {
this._initialRendererIDs.add(rendererID);
}
}
this._store.roots.forEach(rootID => {
const profilingSnapshots = new Map<number, SnapshotNode>();
this._initialSnapshotsByRootID.set(rootID, profilingSnapshots);
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
});
}
this._isProfiling = isProfiling;
this._cache.invalidate();
this.emit('isProfiling');
if (!isProfiling) {
this._dataBackends.splice(0);
this._rendererQueue.clear();
this._rendererIDsThatReportedProfilingData.forEach(rendererID => {
if (!this._rendererQueue.has(rendererID)) {
this._rendererQueue.add(rendererID);
this._bridge.send('getProfilingData', {rendererID});
}
});
this.emit('isProcessingData');
}
};
}