Massively overhauled profiling data architecture

This commit is contained in:
Brian Vaughn
2019-05-22 06:05:25 -07:00
parent 7ce9f4859c
commit c6de014a9a
33 changed files with 3276 additions and 3289 deletions

View File

@@ -1,8 +1,6 @@
// @flow
import EventEmitter from 'events';
import memoize from 'memoize-one';
import throttle from 'lodash.throttle';
import { inspect } from 'util';
import {
TREE_OPERATION_ADD,
@@ -19,11 +17,12 @@ import {
import { __DEBUG__ } from '../constants';
import ProfilingCache from './ProfilingCache';
import { printStore } from 'src/__tests__/storeSerializer';
import ProfilerStore from './ProfilerStore';
import type { Element } from './views/Components/types';
import type {
ImportedProfilingData,
ProfilingSnapshotNode,
ProfilingDataFrontend,
SnapshotNode,
} from './views/Profiler/types';
import type { Bridge, ComponentFilter, ElementType } from '../types';
@@ -43,8 +42,6 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
'React::DevTools::collapseNodesByDefault';
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
type Config = {|
isProfiling?: boolean,
supportsCaptureScreenshots?: boolean,
@@ -80,37 +77,11 @@ export default class Store extends EventEmitter {
// The InspectedElementContext also relies on this mutability for its WeakMap usage.
_idToElement: Map<number, Element> = 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;
// Map of element (id) to the set of elements (ids) it owns.
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
_ownersMap: Map<number, Set<number>> = new Map();
// Suspense cache for reading profiling data.
_profilingCache: ProfilingCache;
// The user has imported a previously exported profiling session.
_profilingData: ImportedProfilingData | null = null;
// 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.
_profilingOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
// Map of root (id) to a Map of screenshots by commit ID.
// Stores screenshots for each commit (when profiling).
_profilingScreenshotsByRootID: Map<number, Map<number, string>> = 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.
_profilingSnapshotsByRootID: Map<
number,
Map<number, ProfilingSnapshotNode>
> = new Map();
_profilerStore: ProfilerStore;
// Incremented each time the store is mutated.
// This enables a passive effect to detect a mutation between render and commit phase.
@@ -150,17 +121,16 @@ export default class Store extends EventEmitter {
this._componentFilters = getSavedComponentFilters();
let isProfiling = false;
if (config != null) {
isProfiling = config.isProfiling === true;
const {
isProfiling,
supportsCaptureScreenshots,
supportsFileDownloads,
supportsProfiling,
supportsReloadAndProfile,
} = config;
if (isProfiling) {
this._isProfiling = true;
}
if (supportsCaptureScreenshots) {
this._supportsCaptureScreenshots = true;
this._captureScreenshots =
@@ -180,15 +150,9 @@ export default class Store extends EventEmitter {
this._bridge = bridge;
bridge.addListener('operations', this.onBridgeOperations);
bridge.addListener('profilingStatus', this.onProfilingStatus);
bridge.addListener('screenshotCaptured', this.onScreenshotCaptured);
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._profilingCache = new ProfilingCache(bridge, this);
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
}
// This is only used in tests to avoid memory leaks.
@@ -197,23 +161,6 @@ export default class Store extends EventEmitter {
// The only safe time to assert these maps are empty is when the store is empty.
this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
// These maps will be empty unless profiling mode has been started.
// After this, their size should always match the number of roots,
// but unless we want to track additional metadata about profiling history,
// the only safe time to assert this is when the store is empty.
this.assertMapSizeMatchesRootCount(
this._profilingOperationsByRootID,
'_profilingOperationsByRootID'
);
this.assertMapSizeMatchesRootCount(
this._profilingScreenshotsByRootID,
'_profilingScreenshotsByRootID'
);
this.assertMapSizeMatchesRootCount(
this._profilingSnapshotsByRootID,
'_profilingSnapshotsByRootID'
);
}
// These maps should always be the same size as the number of roots
@@ -273,7 +220,7 @@ export default class Store extends EventEmitter {
return this._componentFilters;
}
set componentFilters(value: Array<ComponentFilter>): void {
if (this._isProfiling) {
if (this._profilerStore.isProfiling) {
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
throw Error('Cannot modify filter preferences while profiling');
@@ -295,54 +242,65 @@ export default class Store extends EventEmitter {
return this._hasOwnerMetadata;
}
// Profiling data has been recorded for at least one root.
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get hasProfilingData(): boolean {
return (
this._profilingData !== null || this._profilingOperationsByRootID.size > 0
);
return this._profilerStore.hasProfilingData;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get isProcessingProfilingData(): boolean {
return this._profilerStore.isProcessingData;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get isProfiling(): boolean {
return this._isProfiling;
return this._profilerStore.isProfiling;
}
get numElements(): number {
return this._weightAcrossRoots;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingCache(): ProfilingCache {
return this._profilingCache;
return this._profilerStore.cache;
}
get profilingData(): ImportedProfilingData | null {
return this._profilingData;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingData(): ProfilingDataFrontend | null {
return this._profilerStore.profilingData;
}
set profilingData(value: ImportedProfilingData | null): void {
this._profilingData = value;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
this._profilingCache.invalidate();
this.emit('profilingData');
set profilingData(value: ProfilingDataFrontend | null): void {
this._profilerStore.profilingData = value;
}
get profilingOperations(): Map<number, Array<Uint32Array>> {
return this._profilingOperationsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingOperationsByRootID(): Map<number, Array<Uint32Array>> {
return this._profilerStore.inProgressOperationsByRootID;
}
get profilingScreenshots(): Map<number, Map<number, string>> {
return this._profilingScreenshotsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingScreenshotsByRootID(): Map<number, Map<number, string>> {
return this._profilerStore.inProgressScreenshotsByRootID;
}
get profilingSnapshots(): Map<number, Map<number, ProfilingSnapshotNode>> {
return this._profilingSnapshotsByRootID;
// TODO (profarc) Update views to use ProfilerStore directly to access this value.
get profilingSnapshotsByRootID(): Map<number, Map<number, SnapshotNode>> {
return this._profilerStore.initialSnapshotsByRootID;
}
get profilerStore(): ProfilerStore {
return this._profilerStore;
}
get revision(): number {
return this._revision;
}
get rootIDToRendererID(): Map<number, number> {
return this._rootIDToRendererID;
}
get roots(): $ReadOnlyArray<number> {
return this._roots;
}
@@ -363,17 +321,9 @@ export default class Store extends EventEmitter {
return this._supportsReloadAndProfile;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
clearProfilingData(): void {
this._profilingData = null;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
// Invalidate suspense cache if profiling data is being (re-)recorded.
// Note that we clear now because any existing data is "stale".
this._profilingCache.invalidate();
this.emit('isProfiling');
this._profilerStore.clear();
}
containsElement(id: number): boolean {
@@ -601,22 +551,14 @@ export default class Store extends EventEmitter {
return false;
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
startProfiling(): void {
this._bridge.send('startProfiling');
// 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.
this._profilerStore.startProfiling();
}
// TODO (profarc) Update views to use ProfilerStore directly to access this method.
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.
this._profilerStore.stopProfiling();
}
// TODO Maybe split this into two methods: expand() and collapse()
@@ -701,34 +643,6 @@ export default class Store extends EventEmitter {
}
}
_captureScreenshot = throttle(
memoize((rootID: number, commitIndex: number) => {
this._bridge.send('captureScreenshot', { commitIndex, rootID });
}),
THROTTLE_CAPTURE_SCREENSHOT_DURATION
);
_takeProfilingSnapshotRecursive = (
elementID: number,
profilingSnapshots: Map<number, ProfilingSnapshotNode>
) => {
const element = this.getElementByID(elementID);
if (element !== null) {
const snapshotNode: ProfilingSnapshotNode = {
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)
);
}
};
_adjustParentTreeWeight = (
parentElement: Element | null,
weightDelta: number
@@ -769,23 +683,8 @@ export default class Store extends EventEmitter {
let haveRootsChanged = false;
// The first two values are always rendererID and rootID
const rendererID = operations[0];
const rootID = operations[1];
if (this._isProfiling) {
let profilingOperations = this._profilingOperationsByRootID.get(rootID);
if (profilingOperations == null) {
profilingOperations = [operations];
this._profilingOperationsByRootID.set(rootID, profilingOperations);
} else {
profilingOperations.push(operations);
}
if (this._captureScreenshots) {
const commitIndex = profilingOperations.length - 1;
this._captureScreenshot(rootID, commitIndex);
}
}
const addedElementIDs: Array<number> = [];
// This is a mapping of removed ID -> parent ID:
@@ -857,10 +756,6 @@ export default class Store extends EventEmitter {
weight: 0,
});
if (this._isProfiling) {
this._profilingSnapshotsByRootID.set(id, new Map());
}
haveRootsChanged = true;
} else {
parentID = ((operations[i]: any): number);
@@ -956,10 +851,6 @@ export default class Store extends EventEmitter {
this._rootIDToRendererID.delete(id);
this._rootIDToCapabilities.delete(id);
this._profilingOperationsByRootID.delete(id);
this._profilingScreenshotsByRootID.delete(id);
this._profilingSnapshotsByRootID.delete(id);
haveRootsChanged = true;
} else {
if (__DEBUG__) {
@@ -1065,60 +956,12 @@ export default class Store extends EventEmitter {
this.emit('mutated', [addedElementIDs, removedElementIDs]);
};
onProfilingStatus = (isProfiling: boolean) => {
if (isProfiling) {
this._profilingData = null;
this._profilingOperationsByRootID = new Map();
this._profilingScreenshotsByRootID = new Map();
this._profilingSnapshotsByRootID = new Map();
this.roots.forEach(rootID => {
const profilingSnapshots = new Map();
this._profilingSnapshotsByRootID.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._profilingCache.invalidate();
this.emit('isProfiling');
}
};
onScreenshotCaptured = ({
commitIndex,
dataURL,
rootID,
}: {|
commitIndex: number,
dataURL: string,
rootID: number,
|}) => {
let profilingScreenshotsForRootByCommitIndex = this._profilingScreenshotsByRootID.get(
rootID
);
if (!profilingScreenshotsForRootByCommitIndex) {
profilingScreenshotsForRootByCommitIndex = new Map();
this._profilingScreenshotsByRootID.set(
rootID,
profilingScreenshotsForRootByCommitIndex
);
}
profilingScreenshotsForRootByCommitIndex.set(commitIndex, dataURL);
};
onBridgeShutdown = () => {
if (__DEBUG__) {
debug('onBridgeShutdown', 'unsubscribing from Bridge');
}
this._bridge.removeListener('operations', this.onBridgeOperations);
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
};
}