@samitouri / QOS-React-2 / commits / a8ed95445c

Store profiler snapshot data by root (and clear on root unmount)

Brian Vaughn committed May 3, 2019 at 14:59 UTC a8ed95445c56be24e0200f315c44904aadac28ea
5 files changed +89 -77
src/backend/renderer.js
+43 -32
@@ -651,7 +651,8 @@ export function attach(
651 let pendingSimulatedUnmountedIDs: Array<number> = [];
652 let pendingOperationsQueue: Array<Uint32Array> | null = [];
653 let pendingStringTable: Map<string, number> = new Map();
654 - let pendingStringTableLength = 0;
654 + let pendingStringTableLength: number = 0;
655 + let pendingUnmountedRootID: number | null = null;
656
657 function pushOperation(op: number): void {
658 if (__DEV__) {
@@ -669,7 +670,8 @@ export function attach(
670 if (
671 pendingOperations.length === 0 &&
672 pendingRealUnmountedIDs.length === 0 &&
672 - pendingSimulatedUnmountedIDs.length === 0
673 + pendingSimulatedUnmountedIDs.length === 0 &&
674 + pendingUnmountedRootID === null
675 ) {
676 // If we're currently profiling, send an "operations" method even if there are no mutations to the tree.
677 // The frontend needs this no-op info to know how to reconstruct the tree for each commit,
@@ -679,17 +681,21 @@ export function attach(
681 }
682 }
683
684 + const numUnmountIDs =
685 + pendingRealUnmountedIDs.length +
686 + pendingSimulatedUnmountedIDs.length +
687 + (pendingUnmountedRootID === null ? 0 : 1);
688 +
689 const ops = new Uint32Array(
690 // Identify which renderer this update is coming from.
691 2 + // [rendererID, rootFiberID]
692 // How big is the string table?
693 1 + // [stringTableLength]
687 - // Then goes the actual string table.
688 - pendingStringTableLength +
689 - // All unmounts are batched in a single message.
690 - 2 + // [TREE_OPERATION_REMOVE, removedIDLength]
691 - pendingRealUnmountedIDs.length +
692 - pendingSimulatedUnmountedIDs.length +
694 + // Then goes the actual string table.
695 + pendingStringTableLength +
696 + // All unmounts are batched in a single message.
697 + // [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
698 + (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
699 // Regular operations
700 pendingOperations.length
701 );
@@ -699,7 +705,7 @@ export function attach(
705 // Which in turn enables fiber props, states, and hooks to be inspected.
706 let i = 0;
707 ops[i++] = rendererID;
702 - ops[i++] = getFiberID(getPrimaryFiber(root.current));
708 + ops[i++] = currentRootID; // Use this ID in case the root was unmounted!
709
710 // Now fill in the string table.
711 // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
@@ -710,24 +716,30 @@ export function attach(
716 i += key.length;
717 });
718
713 - // All unmounts except roots are batched in a single message.
714 - ops[i++] = TREE_OPERATION_REMOVE;
715 - // The first number is how many unmounted IDs we're gonna send.
716 - ops[i++] =
717 - pendingRealUnmountedIDs.length + pendingSimulatedUnmountedIDs.length;
718 - // Fill in the real unmounts in the reverse order.
719 - // They were inserted parents-first by React, but we want children-first.
720 - // So we traverse our array backwards.
721 - for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
722 - ops[i++] = pendingRealUnmountedIDs[j];
723 - }
724 - // Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
725 - // (We want children to go before parents.)
726 - // They go *after* the real unmounts because we know for sure they won't be
727 - // children of already pushed "real" IDs. If they were, we wouldn't be able
728 - // to discover them during the traversal, as they would have been deleted.
729 - ops.set(pendingSimulatedUnmountedIDs, i);
730 - i += pendingSimulatedUnmountedIDs.length;
719 + if (numUnmountIDs > 0) {
720 + // All unmounts except roots are batched in a single message.
721 + ops[i++] = TREE_OPERATION_REMOVE;
722 + // The first number is how many unmounted IDs we're gonna send.
723 + ops[i++] = numUnmountIDs;
724 + // Fill in the real unmounts in the reverse order.
725 + // They were inserted parents-first by React, but we want children-first.
726 + // So we traverse our array backwards.
727 + for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
728 + ops[i++] = pendingRealUnmountedIDs[j];
729 + }
730 + // Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
731 + // (We want children to go before parents.)
732 + // They go *after* the real unmounts because we know for sure they won't be
733 + // children of already pushed "real" IDs. If they were, we wouldn't be able
734 + // to discover them during the traversal, as they would have been deleted.
735 + ops.set(pendingSimulatedUnmountedIDs, i);
736 + i += pendingSimulatedUnmountedIDs.length;
737 + // The root ID should always be unmounted last.
738 + if (pendingUnmountedRootID !== null) {
739 + ops[i] = pendingUnmountedRootID;
740 + i++;
741 + }
742 + }
743 // Fill in the rest of the operations.
744 ops.set(pendingOperations, i);
745
@@ -747,6 +759,7 @@ export function attach(
759 pendingOperations.length = 0;
760 pendingRealUnmountedIDs.length = 0;
761 pendingSimulatedUnmountedIDs.length = 0;
762 + pendingUnmountedRootID = null;
763 pendingStringTable.clear();
764 pendingStringTableLength = 0;
765 }
@@ -858,11 +871,9 @@ export function attach(
871 }
872 const id = getFiberID(primaryFiber);
873 if (isRoot) {
861 - // Removing a root needs to happen at the end
862 - // so we don't batch it with other unmounts.
863 - pushOperation(TREE_OPERATION_REMOVE);
864 - pushOperation(1); // Remove one item
865 - pushOperation(id);
874 + // Roots must be removed only after all children (pending and simultated) have been removed.
875 + // So we track it separately.
876 + pendingUnmountedRootID = id;
877 } else if (!shouldFilterFiber(fiber)) {
878 // To maintain child-first ordering,
879 // we'll push it into one of these queues,
src/devtools/store.js
+29 -34
@@ -3,6 +3,7 @@
3 import EventEmitter from 'events';
4 import memoize from 'memoize-one';
5 import throttle from 'lodash.throttle';
6 +import { inspect } from 'util';
7 import {
8 TREE_OPERATION_ADD,
9 TREE_OPERATION_REMOVE,
@@ -106,9 +107,9 @@ export default class Store extends EventEmitter {
107 // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
108 // to reconstruct the state of each root for each commit.
109 // It's okay to use a single root to store this information because node IDs are unique across all roots.
109 - _profilingSnapshotsByElementID: Map<
110 + _profilingSnapshotsByRootID: Map<
111 number,
111 - ProfilingSnapshotNode
112 + Map<number, ProfilingSnapshotNode>
113 > = new Map();
114
115 // Incremented each time the store is mutated.
@@ -203,8 +204,8 @@ export default class Store extends EventEmitter {
204 '_profilingScreenshotsByRootID'
205 );
206 this.assertEmptyMap(
206 - this._profilingSnapshotsByElementID,
207 - '_profilingSnapshotsByElementID'
207 + this._profilingSnapshotsByRootID,
208 + '_profilingSnapshotsByRootID'
209 );
210 this.assertEmptyMap(this._rootIDToCapabilities, '_rootIDToCapabilities');
211 this.assertEmptyMap(this._rootIDToRendererID, '_rootIDToRendererID');
@@ -214,9 +215,9 @@ export default class Store extends EventEmitter {
215 assertEmptyMap(map: Map<any, any>, mapName: string) {
216 if (map.size !== 0) {
217 throw new Error(
217 - `Expected ${mapName} to be empty, got ${
218 - map.size
219 - }: ${require('util').inspect(this, { depth: 20 })}`
218 + `Expected ${mapName} to be empty, got ${map.size}: ${inspect(map, {
219 + depth: 20,
220 + })}`
221 );
222 }
223 }
@@ -290,7 +291,7 @@ export default class Store extends EventEmitter {
291 this._importedProfilingData = value;
292 this._profilingOperationsByRootID = new Map();
293 this._profilingScreenshotsByRootID = new Map();
293 - this._profilingSnapshotsByElementID = new Map();
294 + this._profilingSnapshotsByRootID = new Map();
295 this._profilingCache.invalidate();
296
297 this.emit('importedProfilingData');
@@ -316,8 +317,8 @@ export default class Store extends EventEmitter {
317 return this._profilingScreenshotsByRootID;
318 }
319
319 - get profilingSnapshot(): Map<number, ProfilingSnapshotNode> {
320 - return this._profilingSnapshotsByElementID;
320 + get profilingSnapshots(): Map<number, Map<number, ProfilingSnapshotNode>> {
321 + return this._profilingSnapshotsByRootID;
322 }
323
324 get revision(): number {
@@ -348,7 +349,7 @@ export default class Store extends EventEmitter {
349 this._importedProfilingData = null;
350 this._profilingOperationsByRootID = new Map();
351 this._profilingScreenshotsByRootID = new Map();
351 - this._profilingSnapshotsByElementID = new Map();
352 + this._profilingSnapshotsByRootID = new Map();
353
354 // Invalidate suspense cache if profiling data is being (re-)recorded.
355 // Note that we clear now because any existing data is "stale".
@@ -415,10 +416,8 @@ export default class Store extends EventEmitter {
416
417 getElementByID(id: number): Element | null {
418 const element = this._idToElement.get(id);
418 -
419 if (element == null) {
420 console.warn(`No element found with id "${id}"`);
421 -
421 return null;
422 }
423
@@ -687,26 +686,22 @@ export default class Store extends EventEmitter {
686 THROTTLE_CAPTURE_SCREENSHOT_DURATION
687 );
688
690 - _takeProfilingSnapshotRecursive = (elementID: number) => {
689 + _takeProfilingSnapshotRecursive = (
690 + elementID: number,
691 + profilingSnapshot: Map<number, ProfilingSnapshotNode>
692 + ) => {
693 const element = this.getElementByID(elementID);
694 if (element !== null) {
693 - this._profilingSnapshotsByElementID.set(elementID, {
695 + profilingSnapshot.set(elementID, {
696 id: elementID,
697 children: element.children.slice(0),
698 displayName: element.displayName,
699 key: element.key,
700 });
701
700 - element.children.forEach(this._takeProfilingSnapshotRecursive);
701 - }
702 - };
703 -
704 - _clearProfilingSnapshotRecursive = (elementID: number) => {
705 - const element = this.getElementByID(elementID);
706 - if (element !== null) {
707 - this._profilingSnapshotsByElementID.delete(elementID);
708 -
709 - element.children.forEach(this._clearProfilingSnapshotRecursive);
702 + element.children.forEach(childID =>
703 + this._takeProfilingSnapshotRecursive(childID, profilingSnapshot)
704 + );
705 }
706 };
707
@@ -921,6 +916,8 @@ export default class Store extends EventEmitter {
916 throw new Error(`Node ${id} was removed before its children.`);
917 }
918
919 + this._idToElement.delete(id);
920 +
921 let parentElement = null;
922 if (parentID === 0) {
923 if (__DEBUG__) {
@@ -933,11 +930,7 @@ export default class Store extends EventEmitter {
930
931 this._profilingOperationsByRootID.delete(id);
932 this._profilingScreenshotsByRootID.delete(id);
936 -
937 - // The following call depends on `getElementByID`
938 - // which depends on the element being in `_idToElement`,
939 - // so we have to do it before removing the element from `_idToElement`.
940 - this._clearProfilingSnapshotRecursive(id);
933 + this._profilingSnapshotsByRootID.delete(id);
934
935 haveRootsChanged = true;
936 } else {
@@ -954,8 +947,6 @@ export default class Store extends EventEmitter {
947 parentElement.children.splice(index, 1);
948 }
949
957 - this._idToElement.delete(id);
958 -
950 this._adjustParentTreeWeight(parentElement, -weight);
951 removedElementIDs.set(id, parentID);
952
@@ -1051,8 +1042,12 @@ export default class Store extends EventEmitter {
1042 this._importedProfilingData = null;
1043 this._profilingOperationsByRootID = new Map();
1044 this._profilingScreenshotsByRootID = new Map();
1054 - this._profilingSnapshotsByElementID = new Map();
1055 - this.roots.forEach(this._takeProfilingSnapshotRecursive);
1045 + this._profilingSnapshotsByRootID = new Map();
1046 + this.roots.forEach(rootID => {
1047 + const profilingSnapshot = new Map();
1048 + this._profilingSnapshotsByRootID.set(rootID, profilingSnapshot);
1049 + this._takeProfilingSnapshotRecursive(rootID, profilingSnapshot);
1050 + });
1051 }
1052
1053 if (this._isProfiling !== isProfiling) {
src/devtools/views/Profiler/CommitTreeBuilder.js
+14 -8
@@ -15,6 +15,7 @@ import type { ElementType } from 'src/types';
15 import type {
16 CommitTreeFrontend,
17 CommitTreeNodeFrontend,
18 + ProfilingSnapshotNode,
19 ProfilingSummaryFrontend,
20 } from 'src/devtools/views/Profiler/types';
21
@@ -66,13 +67,23 @@ export function getCommitTree({
67 if (commitIndex === 0) {
68 const nodes = new Map();
69
70 + const { importedProfilingData } = store;
71 + const profilingSnapshot =
72 + importedProfilingData != null
73 + ? importedProfilingData.profilingSnapshots.get(rootID)
74 + : store.profilingSnapshots.get(rootID);
75 +
76 + if (profilingSnapshot == null) {
77 + throw Error(`Could not find profiling snapshot for root "${rootID}"`);
78 + }
79 +
80 // Construct the initial tree.
81 recursivelyInitializeTree(
82 rootID,
83 0,
84 nodes,
85 profilingSummary.initialTreeBaseDurations,
75 - store
86 + profilingSnapshot
87 );
88
89 // Mutate the tree
@@ -122,13 +133,8 @@ function recursivelyInitializeTree(
133 parentID: number,
134 nodes: Map<number, CommitTreeNodeFrontend>,
135 initialTreeBaseDurations: Map<number, number>,
125 - store: Store
136 + profilingSnapshot: Map<number, ProfilingSnapshotNode>
137 ): void {
127 - const { importedProfilingData } = store;
128 - const profilingSnapshot =
129 - importedProfilingData != null
130 - ? importedProfilingData.profilingSnapshot
131 - : store.profilingSnapshot;
138 const node = profilingSnapshot.get(id);
139 if (node != null) {
140 nodes.set(id, {
@@ -146,7 +152,7 @@ function recursivelyInitializeTree(
152 id,
153 nodes,
154 initialTreeBaseDurations,
149 - store
155 + profilingSnapshot
156 )
157 );
158 }
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+2 -2
@@ -37,7 +37,7 @@ export default function ProfilingImportExportButtons() {
37 const queue = [rootID];
38 while (queue.length) {
39 const id = queue.pop();
40 - profilingSnapshotForRoot.push([id, store.profilingSnapshot.get(id)]);
40 + profilingSnapshotForRoot.push([id, store.profilingSnapshots.get(id)]);
41 }
42
43 bridge.send('exportProfilingSummary', {
@@ -51,7 +51,7 @@ export default function ProfilingImportExportButtons() {
51 rendererID,
52 rootID,
53 store.profilingOperations,
54 - store.profilingSnapshot,
54 + store.profilingSnapshots,
55 ]);
56
57 const uploadData = useCallback(() => {
src/devtools/views/Profiler/types.js
+1 -1
@@ -65,7 +65,7 @@ export type ProfilingSnapshotNode = {|
65 export type ImportedProfilingData = {|
66 version: number,
67 profilingOperations: Map<number, Array<Uint32Array>>,
68 - profilingSnapshot: Map<number, ProfilingSnapshotNode>,
68 + profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
69 commitDetails: CommitDetailsFrontend,
70 interactions: InteractionsFrontend,
71 profilingSummary: ProfilingSummaryFrontend,