// @flow import EventEmitter from 'events'; import memoize from 'memoize-one'; import throttle from 'lodash.throttle'; import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; import { ElementTypeRoot } from '../types'; import { getSavedComponentFilters, saveComponentFilters, utfDecodeString, } from '../utils'; import { __DEBUG__ } from '../constants'; import ProfilingCache from './ProfilingCache'; import { printStore } from 'src/__tests__/storeSerializer'; import type { Element } from './views/Components/types'; import type { ImportedProfilingData, ProfilingSnapshotNode, } from './views/Profiler/types'; import type { Bridge, ComponentFilter, ElementType } from '../types'; const debug = (methodName, ...args) => { if (__DEBUG__) { console.log( `%cStore %c${methodName}`, 'color: green; font-weight: bold;', 'font-weight: bold;', ...args ); } }; const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY = 'React::DevTools::captureScreenshots'; const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY = 'React::DevTools::collapseNodesByDefault'; const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500; type Config = {| isProfiling?: boolean, supportsCaptureScreenshots?: boolean, supportsFileDownloads?: boolean, supportsReloadAndProfile?: boolean, supportsProfiling?: boolean, |}; export type Capabilities = {| hasOwnerMetadata: boolean, supportsProfiling: boolean, |}; /** * The store is the single source of truth for updates from the backend. * ContextProviders can subscribe to the Store for specific things they want to provide. */ export default class Store extends EventEmitter { _bridge: Bridge; _captureScreenshots: boolean = false; // Should new nodes be collapsed by default when added to the tree? _collapseNodesByDefault: boolean = true; _componentFilters: Array; // At least one of the injected renderers contains (DEV only) owner metadata. _hasOwnerMetadata: boolean = false; // Map of ID to (mutable) Element. // Elements are mutated to avoid excessive cloning during tree updates. // The InspectedElementContext also relies on this mutability for its WeakMap usage. _idToElement: Map = new Map(); // The user has imported a previously exported profiling session. _importedProfilingData: ImportedProfilingData | null = null; // 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> = new Map(); // Suspense cache for reading profiling data. _profilingCache: ProfilingCache; // 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> = new Map(); // Map of root (id) to a Map of screenshots by commit ID. // Stores screenshots for each commit (when profiling). _profilingScreenshotsByRootID: Map> = 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. _profilingSnapshotsByElementID: 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; // This Array must be treated as immutable! // Passive effects will check it for changes between render and mount. _roots: $ReadOnlyArray = []; _rootIDToCapabilities: Map = new Map(); // Renderer ID is needed to support inspection fiber props, state, and hooks. _rootIDToRendererID: Map = new Map(); // These options may be initially set by a confiugraiton option when constructing the Store. // In the case of "supportsProfiling", the option may be updated based on the injected renderers. _supportsCaptureScreenshots: boolean = false; _supportsFileDownloads: boolean = false; _supportsProfiling: boolean = false; _supportsReloadAndProfile: boolean = false; // Total number of visible elements (within all roots). // Used for windowing purposes. _weightAcrossRoots: number = 0; constructor(bridge: Bridge, config?: Config) { super(); if (__DEBUG__) { debug('constructor', 'subscribing to Bridge'); } // Default this setting to true unless otherwise specified. this._collapseNodesByDefault = localStorage.getItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !== 'false'; this._componentFilters = getSavedComponentFilters(); if (config != null) { const { isProfiling, supportsCaptureScreenshots, supportsFileDownloads, supportsProfiling, supportsReloadAndProfile, } = config; if (isProfiling) { this._isProfiling = true; } if (supportsCaptureScreenshots) { this._supportsCaptureScreenshots = true; this._captureScreenshots = localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) === 'true'; } if (supportsFileDownloads) { this._supportsFileDownloads = true; } if (supportsProfiling) { this._supportsProfiling = true; } if (supportsReloadAndProfile) { this._supportsReloadAndProfile = true; } } 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 is only used in tests to avoid memory leaks. assertEmptyMaps() { this.assertEmptyMap(this._idToElement, '_idToElement'); this.assertEmptyMap(this._ownersMap, '_ownersMap'); this.assertEmptyMap( this._profilingOperationsByRootID, '_profilingOperationsByRootID' ); this.assertEmptyMap( this._profilingScreenshotsByRootID, '_profilingScreenshotsByRootID' ); this.assertEmptyMap( this._profilingSnapshotsByElementID, '_profilingSnapshotsByElementID' ); this.assertEmptyMap(this._rootIDToCapabilities, '_rootIDToCapabilities'); this.assertEmptyMap(this._rootIDToRendererID, '_rootIDToRendererID'); } // This is only used in tests to avoid memory leaks. assertEmptyMap(map: Map, mapName: string) { if (map.size !== 0) { throw new Error( `Expected ${mapName} to be empty, got ${ map.size }: ${require('util').inspect(this, { depth: 20 })}` ); } } get captureScreenshots(): boolean { return this._captureScreenshots; } set captureScreenshots(value: boolean): void { this._captureScreenshots = value; localStorage.setItem( LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY, value ? 'true' : 'false' ); this.emit('captureScreenshots'); } get collapseNodesByDefault(): boolean { return this._collapseNodesByDefault; } set collapseNodesByDefault(value: boolean): void { this._collapseNodesByDefault = value; localStorage.setItem( LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY, value ? 'true' : 'false' ); this.emit('collapseNodesByDefault'); } get componentFilters(): Array { return this._componentFilters; } set componentFilters(value: Array): void { if (this._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'); } this._componentFilters = value; // Update persisted filter preferences stored in localStorage. saveComponentFilters(value); // Notify the renderer that filter prefernces have changed. // This is an expensive opreation; it unmounts and remounts the entire tree. this._bridge.send('updateComponentFilters', value); this.emit('componentFilters'); } get hasOwnerMetadata(): boolean { return this._hasOwnerMetadata; } // Profiling data has been recorded for at least one root. get hasProfilingData(): boolean { return ( this._importedProfilingData !== null || this._profilingOperationsByRootID.size > 0 ); } get importedProfilingData(): ImportedProfilingData | null { return this._importedProfilingData; } set importedProfilingData(value: ImportedProfilingData | null): void { this._importedProfilingData = value; this._profilingOperationsByRootID = new Map(); this._profilingScreenshotsByRootID = new Map(); this._profilingSnapshotsByElementID = new Map(); this._profilingCache.invalidate(); this.emit('importedProfilingData'); } get isProfiling(): boolean { return this._isProfiling; } get numElements(): number { return this._weightAcrossRoots; } get profilingCache(): ProfilingCache { return this._profilingCache; } get profilingOperations(): Map> { return this._profilingOperationsByRootID; } get profilingScreenshots(): Map> { return this._profilingScreenshotsByRootID; } get profilingSnapshot(): Map { return this._profilingSnapshotsByElementID; } get revision(): number { return this._revision; } get roots(): $ReadOnlyArray { return this._roots; } get supportsCaptureScreenshots(): boolean { return this._supportsCaptureScreenshots; } get supportsFileDownloads(): boolean { return this._supportsFileDownloads; } get supportsProfiling(): boolean { return this._supportsProfiling; } get supportsReloadAndProfile(): boolean { return this._supportsReloadAndProfile; } clearProfilingData(): void { this._importedProfilingData = null; this._profilingOperationsByRootID = new Map(); this._profilingScreenshotsByRootID = new Map(); this._profilingSnapshotsByElementID = 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'); } getElementAtIndex(index: number): Element | null { if (index < 0 || index >= this.numElements) { console.warn( `Invalid index ${index} specified; store contains ${ this.numElements } items.` ); return null; } // Find wich root this element is in... let rootID; let root; let rootWeight = 0; for (let i = 0; i < this._roots.length; i++) { rootID = this._roots[i]; root = ((this._idToElement.get(rootID): any): Element); if (root.children.length === 0) { continue; } else if (rootWeight + root.weight > index) { break; } else { rootWeight += root.weight; } } // Find the element in the tree using the weight of each node... // Skip over the root itself, because roots aren't visible in the Elements tree. let currentElement = ((root: any): Element); let currentWeight = rootWeight - 1; while (index !== currentWeight) { const numChildren = currentElement.children.length; for (let i = 0; i < numChildren; i++) { const childID = currentElement.children[i]; const child = ((this._idToElement.get(childID): any): Element); const childWeight = child.isCollapsed ? 1 : child.weight; if (index <= currentWeight + childWeight) { currentWeight++; currentElement = child; break; } else { currentWeight += childWeight; } } } return ((currentElement: any): Element) || null; } getElementIDAtIndex(index: number): number | null { const element: Element | null = this.getElementAtIndex(index); return element === null ? null : element.id; } getElementByID(id: number): Element | null { const element = this._idToElement.get(id); if (element == null) { console.warn(`No element found with id "${id}"`); return null; } return element; } getIndexOfElementID(id: number): number | null { const element = this.getElementByID(id); if (element === null || element.parentID === 0) { return null; } // Walk up the tree to the root. // Increment the index by one for each node we encounter, // and by the weight of all nodes to the left of the current one. // This should be a relatively fast way of determining the index of a node within the tree. let previousID = id; let currentID = element.parentID; let index = 0; while (true) { const current = ((this._idToElement.get(currentID): any): Element); const { children } = current; for (let i = 0; i < children.length; i++) { const childID = children[i]; if (childID === previousID) { break; } const child = ((this._idToElement.get(childID): any): Element); index += child.isCollapsed ? 1 : child.weight; } if (current.parentID === 0) { // We found the root; stop crawling. break; } index++; previousID = current.id; currentID = current.parentID; } // At this point, the current ID is a root (from the previous loop). // We also need to offset the index by previous root weights. for (let i = 0; i < this._roots.length; i++) { const rootID = this._roots[i]; if (rootID === currentID) { break; } const root = ((this._idToElement.get(rootID): any): Element); index += root.weight; } return index; } getOwnersListForElement(ownerID: number): Array { const list = []; let element = this._idToElement.get(ownerID); if (element != null) { list.push({ ...element, depth: 0, }); const unsortedIDs = this._ownersMap.get(ownerID); if (unsortedIDs !== undefined) { const depthMap: Map = new Map([[ownerID, 0]]); // Items in a set are ordered based on insertion. // This does not correlate with their order in the tree. // So first we need to order them. // I wish we could avoid this sorting operation; we could sort at insertion time, // but then we'd have to pay sorting costs even if the owners list was never used. // Seems better to defer the cost, since the set of ids is probably pretty small. const sortedIDs = Array.from(unsortedIDs).sort( (idA, idB) => ((this.getIndexOfElementID(idA): any): number) - ((this.getIndexOfElementID(idB): any): number) ); // Next we need to determine the appropriate depth for each element in the list. // The depth in the list may not correspond to the depth in the tree, // because the list has been filtered to remove intermediate components. // Perhaps the easiest way to do this is to walk up the tree until we reach either: // (1) another node that's already in the tree, or (2) the root (owner) // at which point, our depth is just the depth of that node plus one. sortedIDs.forEach(id => { const element = this._idToElement.get(id); if (element != null) { let parentID = element.parentID; let depth = 0; while (parentID > 0) { if (parentID === ownerID || unsortedIDs.has(parentID)) { depth = depthMap.get(parentID) + 1; depthMap.set(id, depth); break; } const parent = this._idToElement.get(parentID); if (parent == null) { break; } parentID = parent.parentID; } if (depth === 0) { throw Error('Invalid owners list'); } list.push({ ...element, depth }); } }); } } return list; } getRendererIDForElement(id: number): number | null { let current = this._idToElement.get(id); while (current != null) { if (current.parentID === 0) { const rendererID = this._rootIDToRendererID.get(current.id); return rendererID == null ? null : rendererID; } else { current = this._idToElement.get(current.parentID); } } return null; } getRootIDForElement(id: number): number | null { let current = this._idToElement.get(id); while (current != null) { if (current.parentID === 0) { return current.id; } else { current = this._idToElement.get(current.parentID); } } return null; } isInsideCollapsedSubTree(id: number): boolean { let current = this._idToElement.get(id); while (current != null) { if (current.parentID === 0) { return false; } else { current = this._idToElement.get(current.parentID); if (current != null && current.isCollapsed) { return true; } } } return false; } 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. } 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. } // TODO Maybe split this into two methods: expand() and collapse() toggleIsCollapsed(id: number, isCollapsed: boolean): void { let didMutate = false; const element = this.getElementByID(id); if (element !== null) { if (isCollapsed) { if (element.type === ElementTypeRoot) { throw Error('Root nodes cannot be collapsed'); } if (!element.isCollapsed) { didMutate = true; element.isCollapsed = true; const weightDelta = 1 - element.weight; let parentElement = ((this._idToElement.get( element.parentID ): any): Element); while (parentElement != null) { // We don't need to break on a collapsed parent in the same way as the expand case below. // That's because collapsing a node doesn't "bubble" and affect its parents. parentElement.weight += weightDelta; parentElement = this._idToElement.get(parentElement.parentID); } } } else { let currentElement = element; while (currentElement != null) { const oldWeight = currentElement.isCollapsed ? 1 : currentElement.weight; if (currentElement.isCollapsed) { didMutate = true; currentElement.isCollapsed = false; const newWeight = currentElement.isCollapsed ? 1 : currentElement.weight; const weightDelta = newWeight - oldWeight; let parentElement = ((this._idToElement.get( currentElement.parentID ): any): Element); while (parentElement != null) { parentElement.weight += weightDelta; if (parentElement.isCollapsed) { // It's important to break on a collapsed parent when expanding nodes. // That's because expanding a node "bubbles" up and expands all parents as well. // Breaking in this case prevents us from over-incrementing the expanded weights. break; } parentElement = this._idToElement.get(parentElement.parentID); } } currentElement = currentElement.parentID !== 0 ? this.getElementByID(currentElement.parentID) : null; } } // Only re-calculate weights and emit an "update" event if the store was mutated. if (didMutate) { let weightAcrossRoots = 0; this._roots.forEach(rootID => { const { weight } = ((this.getElementByID(rootID): any): Element); weightAcrossRoots += weight; }); this._weightAcrossRoots = weightAcrossRoots; // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed. // In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden). // Updating the selected search index later may require auto-expanding a collapsed subtree though. this.emit('mutated', [[], new Map()]); } } } _captureScreenshot = throttle( memoize((rootID: number, commitIndex: number) => { this._bridge.send('captureScreenshot', { commitIndex, rootID }); }), THROTTLE_CAPTURE_SCREENSHOT_DURATION ); _takeProfilingSnapshotRecursive = (elementID: number) => { const element = this.getElementByID(elementID); if (element !== null) { this._profilingSnapshotsByElementID.set(elementID, { id: elementID, children: element.children.slice(0), displayName: element.displayName, key: element.key, }); element.children.forEach(this._takeProfilingSnapshotRecursive); } }; _clearProfilingSnapshotRecursive = (elementID: number) => { const element = this.getElementByID(elementID); if (element !== null) { this._profilingSnapshotsByElementID.delete(elementID); element.children.forEach(this._clearProfilingSnapshotRecursive); } }; _adjustParentTreeWeight = ( parentElement: Element | null, weightDelta: number ) => { let isInsideCollapsedSubTree = false; while (parentElement != null) { parentElement.weight += weightDelta; // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent. // Their weight will bubble up when the parent is expanded. if (parentElement.isCollapsed) { isInsideCollapsedSubTree = true; break; } parentElement = ((this._idToElement.get( parentElement.parentID ): any): Element); } // Additions and deletions within a collapsed subtree should not affect the overall number of elements. if (!isInsideCollapsedSubTree) { this._weightAcrossRoots += weightDelta; } }; onBridgeOperations = (operations: Uint32Array) => { if (!(operations instanceof Uint32Array)) { // $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array. operations = Uint32Array.from(Object.values(operations)); } if (__DEBUG__) { console.groupCollapsed('onBridgeOperations'); debug('onBridgeOperations', operations.join(',')); } let haveRootsChanged = false; 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 = []; // This is a mapping of removed ID -> parent ID: const removedElementIDs: Map = new Map(); // We'll use the parent ID to adjust selection if it gets deleted. let i = 2; // Reassemble the string table. const stringTable = [ null, // ID = 0 corresponds to the null string. ]; const stringTableSize = operations[i++]; const stringTableEnd = i + stringTableSize; while (i < stringTableEnd) { const nextLength = operations[i++]; const nextString = utfDecodeString( (operations.slice(i, i + nextLength): any) ); stringTable.push(nextString); i += nextLength; } while (i < operations.length) { const operation = operations[i]; switch (operation) { case TREE_OPERATION_ADD: { const id = ((operations[i + 1]: any): number); const type = ((operations[i + 2]: any): ElementType); i = i + 3; if (this._idToElement.has(id)) { throw Error( `Cannot add node ${id} because a node with that id is already in the Store.` ); } let ownerID: number = 0; let parentID: number = ((null: any): number); if (type === ElementTypeRoot) { if (__DEBUG__) { debug('Add', `new root node ${id}`); } const supportsProfiling = operations[i] > 0; i++; const hasOwnerMetadata = operations[i] > 0; i++; this._roots = this._roots.concat(id); this._rootIDToRendererID.set(id, rendererID); this._rootIDToCapabilities.set(id, { hasOwnerMetadata, supportsProfiling, }); this._idToElement.set(id, { children: [], depth: -1, displayName: null, id, isCollapsed: false, // Never collapse roots; it would hide the entire tree. key: null, ownerID: 0, parentID: 0, type, weight: 0, }); haveRootsChanged = true; } else { parentID = ((operations[i]: any): number); i++; ownerID = ((operations[i]: any): number); i++; const displayNameStringID = operations[i]; const displayName = stringTable[displayNameStringID]; i++; const keyStringID = operations[i]; const key = stringTable[keyStringID]; i++; if (__DEBUG__) { debug( 'Add', `node ${id} (${displayName || 'null'}) as child of ${parentID}` ); } if (!this._idToElement.has(parentID)) { throw Error( `Cannot add child ${id} to parent ${parentID} because parent node was not found in the Store.` ); } const parentElement = ((this._idToElement.get( parentID ): any): Element); parentElement.children.push(id); const element: Element = { children: [], depth: parentElement.depth + 1, displayName, id, isCollapsed: this._collapseNodesByDefault, key, ownerID, parentID: parentElement.id, type, weight: 1, }; this._idToElement.set(id, element); addedElementIDs.push(id); this._adjustParentTreeWeight(parentElement, 1); if (ownerID > 0) { let set = this._ownersMap.get(ownerID); if (set === undefined) { set = new Set(); this._ownersMap.set(ownerID, set); } set.add(id); } } break; } case TREE_OPERATION_REMOVE: { const removeLength = ((operations[i + 1]: any): number); i = i + 2; for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) { const id = ((operations[i]: any): number); if (!this._idToElement.has(id)) { throw Error( `Cannot remove node ${id} because no matching node was found in the Store.` ); } i = i + 1; const element = ((this._idToElement.get(id): any): Element); const { children, ownerID, parentID, weight } = element; if (children.length > 0) { throw new Error(`Node ${id} was removed before its children.`); } // The following call depends on `getElementByID` // which depends on the element being in `_idToElement`, // so we have to do it before removing the element from `_idToElement`. this._clearProfilingSnapshotRecursive(id); this._idToElement.delete(id); let parentElement = null; if (parentID === 0) { if (__DEBUG__) { debug('Remove', `node ${id} root`); } this._roots = this._roots.filter(rootID => rootID !== id); this._rootIDToRendererID.delete(id); this._rootIDToCapabilities.delete(id); this._profilingOperationsByRootID.delete(id); this._profilingScreenshotsByRootID.delete(id); haveRootsChanged = true; } else { if (__DEBUG__) { debug('Remove', `node ${id} from parent ${parentID}`); } parentElement = ((this._idToElement.get(parentID): any): Element); if (parentElement === undefined) { throw Error( `Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.` ); } const index = parentElement.children.indexOf(id); parentElement.children.splice(index, 1); } this._adjustParentTreeWeight(parentElement, -weight); removedElementIDs.set(id, parentID); this._ownersMap.delete(id); if (ownerID > 0) { const set = this._ownersMap.get(ownerID); if (set !== undefined) { set.delete(id); } } } break; } case TREE_OPERATION_REORDER_CHILDREN: { const id = ((operations[i + 1]: any): number); const numChildren = ((operations[i + 2]: any): number); i = i + 3; if (!this._idToElement.has(id)) { throw Error( `Cannot reorder children for node ${id} because no matching node was found in the Store.` ); } const element = ((this._idToElement.get(id): any): Element); const children = element.children; if (children.length !== numChildren) { throw Error( `Children cannot be added or removed during a reorder operation.` ); } for (let j = 0; j < numChildren; j++) { const childID = operations[i + j]; children[j] = childID; if (__DEV__) { // This check is more expensive so it's gated by __DEV__. const childElement = this._idToElement.get(childID); if (childElement == null || childElement.parentID !== id) { console.error( `Children cannot be added or removed during a reorder operation.` ); } } } i = i + numChildren; if (__DEBUG__) { debug('Re-order', `Node ${id} children ${children.join(',')}`); } break; } case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: // Base duration updates are only sent while profiling is in progress. // We can ignore them at this point. // The profiler UI uses them lazily in order to generate the tree. i = i + 3; break; default: throw Error(`Unsupported Bridge operation ${operation}`); } } this._revision++; if (haveRootsChanged) { this._hasOwnerMetadata = false; this._supportsProfiling = false; this._rootIDToCapabilities.forEach( ({ hasOwnerMetadata, supportsProfiling }) => { if (hasOwnerMetadata) { this._hasOwnerMetadata = true; } if (supportsProfiling) { this._supportsProfiling = true; } } ); this.emit('roots'); } if (__DEBUG__) { console.log(printStore(this, true)); console.groupEnd(); } this.emit('mutated', [addedElementIDs, removedElementIDs]); }; onProfilingStatus = (isProfiling: boolean) => { if (isProfiling) { this._importedProfilingData = null; this._profilingOperationsByRootID = new Map(); this._profilingScreenshotsByRootID = new Map(); this._profilingSnapshotsByElementID = new Map(); this.roots.forEach(this._takeProfilingSnapshotRecursive); } 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); }; }