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,