Added commit tree reconstruction
Brian Vaughn committed
Mar 15, 2019 at 20:05 UTC
821a6504b8b6341c0b1cac8ca436483e42ad30dd
17 files changed
+678
-90
OVERVIEW.md
+21
-8
@@ -16,7 +16,7 @@ Every React commit that changes the tree in a way DevTools cares about results i
16
17
The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). The rest of the array depends on the operations being made to the tree.
18
19
-We only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate "_inspectElement_" message.
19
+No updates are required for many commits because we only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate "_inspectElement_" message.
20
21
#### Adding a root node
22
@@ -25,7 +25,7 @@ Adding a root to the tree requires sending 4 numbers:
25
1. add operation constant (`1`)
26
1. fiber id
27
1. element type constant (`8 === ElementTypeRoot`)
28
-1. profiling flag
28
+1. profiling supported flag
29
30
For example, adding a root fiber with an id of 1:
31
```js
@@ -100,6 +100,23 @@ For example:
100
]
101
```
102
103
+#### Updating tree base duration
104
+
105
+While profiling is in progress, we send an extra operation any time a fiber is added or a updated in a way that affects its tree base duration. This information is needed by the Profiler UI in order to render the "snapshot" and "ranked" chart views.
106
+
107
+1. tree base duration constant (`4`)
108
+1. fiber id
109
+1. tree base duration
110
+
111
+For example, updating the base duration for a fiber with an id of 1:
112
+```js
113
+[
114
+ 4, // tree base duration operation
115
+ 1, // fiber id
116
+ 32, // new tree base duration value
117
+]
118
+```
119
+
120
## Reconstructing the tree
121
122
The frontend stores its information about the tree in a map of id to objects with the following keys:
@@ -251,12 +268,10 @@ Here is an example commit in which two elements were rendered and one interactio
268
commitIndex: 0,
269
interactions: [
270
{
254
- id: 1,
271
timestamp: 4,
272
name: "Foo"
273
},
274
{
259
- id: 2,
275
timestamp: 4,
276
name: "Bar"
277
}
@@ -265,13 +280,11 @@ Here is an example commit in which two elements were rendered and one interactio
280
{
281
id: 1,
282
baseDuration: 15,
268
- selfDuration: 4,
283
actualDuration: 15
284
},
285
{
286
id: 2,
287
baseDuration: 11,
274
- selfDuration: 8,
288
actualDuration: 11
289
}
290
}
@@ -319,16 +332,16 @@ Here is an example of a profiling session consisting of two interactions:
332
rootID: 1,
333
interactions: [
334
{
322
- id: 1,
335
name: "Foo",
336
+ timestamp: 4,
337
commits: [
338
0, // index of first commit
339
2 // index of second commit
340
]
341
},
342
{
330
- id: 2,
343
name: "Bar",
344
+ timestamp: 4,
345
commits: [
346
0 // index of first commit
347
]
src/backend/agent.js
+21
@@ -46,6 +46,7 @@ export default class Agent extends EventEmitter {
46
addBridge(bridge: Bridge) {
47
this._bridge = bridge;
48
49
+ bridge.addListener('getCommitDetails', this.getCommitDetails);
50
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
51
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
52
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
@@ -76,6 +77,26 @@ export default class Agent extends EventEmitter {
77
return null;
78
}
79
80
+ getCommitDetails = ({
81
+ commitIndex,
82
+ rendererID,
83
+ rootID,
84
+ }: {
85
+ commitIndex: number,
86
+ rendererID: number,
87
+ rootID: number,
88
+ }) => {
89
+ const renderer = this._rendererInterfaces[rendererID];
90
+ if (renderer == null) {
91
+ console.warn(`Invalid renderer id "${rendererID}"`);
92
+ } else {
93
+ this._bridge.send(
94
+ 'commitDetails',
95
+ renderer.getCommitDetails(rootID, commitIndex)
96
+ );
97
+ }
98
+ };
99
+
100
getProfilingStatus = () => {
101
this._bridge.send('profilingStatus', this._isProfiling);
102
};
src/backend/renderer.js
+57
-11
@@ -19,11 +19,13 @@ import {
19
TREE_OPERATION_ADD,
20
TREE_OPERATION_REMOVE,
21
TREE_OPERATION_RESET_CHILDREN,
22
+ TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
23
} from '../constants';
24
import { getUID } from '../utils';
25
import { inspectHooksOfFiber } from './ReactDebugHooks';
26
27
import type {
28
+ CommitDetails,
29
DevToolsHook,
30
Fiber,
31
FiberData,
@@ -474,6 +476,10 @@ export function attach(
476
// When a mount or update is in progress, this value tracks the root that is being operated on.
477
let currentRootID: number = -1;
478
479
+ function getInteractionID({ name, timestamp }: Interaction): string {
480
+ return `${timestamp}-${name}`;
481
+ }
482
+
483
function getFiberID(primaryFiber: Fiber): number {
484
if (!fiberToIDMap.has(primaryFiber)) {
485
const id = getUID();
@@ -623,6 +629,14 @@ export function attach(
629
}
630
addOperation(operation);
631
}
632
+
633
+ if (isProfiling) {
634
+ const operation = new Uint32Array(3);
635
+ operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
636
+ operation[1] = id;
637
+ operation[2] = fiber.treeBaseDuration;
638
+ addOperation(operation);
639
+ }
640
}
641
642
function enqueueUnmount(fiber) {
@@ -682,21 +696,26 @@ export function attach(
696
if (isProfiling) {
697
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
698
const id = getFiberID(getPrimaryFiber(fiber));
699
+ const { actualDuration, treeBaseDuration } = fiber;
700
+
701
+ const operation = new Uint32Array(3);
702
+ operation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
703
+ operation[1] = id;
704
+ operation[2] = treeBaseDuration;
705
+ addOperation(operation);
706
686
- idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration);
707
+ idToTreeBaseDurationMap.set(id, treeBaseDuration);
708
688
- if (fiber.actualDuration > 0) {
709
+ if (actualDuration > 0) {
710
// If profiling is active, store durations for elements that were rendered during the commit.
711
const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
712
metadata.committedFibers.push({
713
id,
693
- actualDuration: fiber.actualDuration,
694
- selfDuration: fiber.selfDuration,
695
- treeBaseDuration: fiber.treeBaseDuration,
714
+ actualDuration,
715
});
716
metadata.maxActualDuration = Math.max(
717
metadata.maxActualDuration,
699
- fiber.actualDuration
718
+ actualDuration
719
);
720
}
721
}
@@ -864,9 +883,10 @@ export function attach(
883
committedFibers: [],
884
commitTime: performance.now() - profilingStartTime,
885
interactions: Array.from(root.memoizedInteractions).map(
867
- ({ name, timestamp }: Interaction) => ({
868
- name,
869
- timestamp: timestamp - profilingStartTime,
886
+ (interaction: Interaction) => ({
887
+ id: getInteractionID(interaction),
888
+ name: interaction.name,
889
+ timestamp: interaction.timestamp - profilingStartTime,
890
})
891
),
892
maxActualDuration: 0,
@@ -1360,8 +1380,6 @@ export function attach(
1380
type CommittedFiber = {|
1381
actualDuration: number,
1382
id: number,
1363
- selfDuration: number,
1364
- treeBaseDuration: number,
1383
|};
1384
1385
type CommitProfilingData = {|
@@ -1379,6 +1397,33 @@ export function attach(
1397
let profilingStartTime: number = 0;
1398
let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null;
1399
1400
+ function getCommitDetails(
1401
+ rootID: number,
1402
+ commitIndex: number
1403
+ ): CommitDetails {
1404
+ const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
1405
+ rootID
1406
+ );
1407
+ if (commitProfilingMetadata != null) {
1408
+ const commitProfilingData = commitProfilingMetadata[commitIndex];
1409
+ if (commitProfilingData != null) {
1410
+ return {
1411
+ commitIndex,
1412
+ interactions: commitProfilingData.interactions,
1413
+ committedFibers: commitProfilingData.committedFibers,
1414
+ rootID,
1415
+ };
1416
+ }
1417
+ }
1418
+ // TODO (profiling) Is this right? Should I return null? Does it matter?
1419
+ return {
1420
+ commitIndex,
1421
+ interactions: [],
1422
+ committedFibers: [],
1423
+ rootID,
1424
+ };
1425
+ }
1426
+
1427
function getProfilingSummary(rootID: number): ProfilingSummary {
1428
const interactions = new Set();
1429
const commitDurations = [];
@@ -1428,6 +1473,7 @@ export function attach(
1473
1474
return {
1475
cleanup,
1476
+ getCommitDetails,
1477
getFiberIDFromNative,
1478
getNativeFromReactElement,
1479
getProfilingSummary,
src/backend/types.js
+17
-5
@@ -19,11 +19,6 @@ export type FiberData = {|
19
type: ElementType,
20
|};
21
22
-export type Interaction = {|
23
- name: string,
24
- timestamp: number,
25
-|};
26
-
22
export type NativeType = {};
23
export type RendererID = number;
24
@@ -54,6 +49,22 @@ export type ReactRenderer = {
49
currentDispatcherRef?: {| current: null | Dispatcher |},
50
};
51
52
+export type Interaction = {|
53
+ id: string,
54
+ name: string,
55
+ timestamp: number,
56
+|};
57
+
58
+export type CommitDetails = {|
59
+ commitIndex: number,
60
+ interactions: Array<Interaction>,
61
+ committedFibers: Array<{|
62
+ actualDuration: number,
63
+ id: number,
64
+ |}>,
65
+ rootID: number,
66
+|};
67
+
68
export type ProfilingSummary = {|
69
commitDurations: Array<number>,
70
commitTimes: Array<number>,
@@ -64,6 +75,7 @@ export type ProfilingSummary = {|
75
76
export type RendererInterface = {
77
cleanup: () => void,
78
+ getCommitDetails: (rootID: number, commitIndex: number) => CommitDetails,
79
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
80
getFiberIDFromNative: (
81
component: NativeType,
src/constants.js
+1
@@ -3,5 +3,6 @@
3
export const TREE_OPERATION_ADD = 1;
4
export const TREE_OPERATION_REMOVE = 2;
5
export const TREE_OPERATION_RESET_CHILDREN = 3;
6
+export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
7
8
export const __DEBUG__ = false;
src/devtools/ProfilingCache.js
+110
-8
@@ -2,30 +2,110 @@
2
3
import { createResource, invalidateResources } from './cache';
4
import Store from './store';
5
+import { getCommitTree } from 'src/devtools/views/Profiler/CommitTreeBuilder';
6
7
import type { Resource } from './cache';
8
import type { Bridge } from '../types';
8
-import type { ProfilingSummary as ProfilingSummaryBackend } from 'src/backend/types';
9
-import type { ProfilingSummary as ProfilingSummaryFrontend } from 'src/devtools/views/Profiler/types';
9
+import type {
10
+ CommitDetails as CommitDetailsBackend,
11
+ ProfilingSummary as ProfilingSummaryBackend,
12
+} from 'src/backend/types';
13
+import type {
14
+ CommitDetails as CommitDetailsFrontend,
15
+ CommitTree,
16
+ ProfilingSummary as ProfilingSummaryFrontend,
17
+} from 'src/devtools/views/Profiler/types';
18
11
-type RendererAndRootID = {|
19
+type CommitDetailsParams = {|
20
+ commitIndex: number,
21
+ rootID: number,
22
+ rendererID: number,
23
+|};
24
+
25
+type CommitTreeParams = {|
26
+ commitIndex: number,
27
+ profilingSummary: ProfilingSummaryFrontend,
28
+ rendererID: number,
29
+ rootID: number,
30
+|};
31
+
32
+type ProfilingSummaryParams = {|
33
rootID: number,
34
rendererID: number,
35
|};
36
37
export default class ProfilingCache {
38
+ _pendingCommitDetailsMap: Map<
39
+ string,
40
+ (commitDetails: CommitDetailsFrontend) => void
41
+ > = new Map();
42
+
43
_pendingProfileSummaryMap: Map<
44
number,
45
(profilingSummary: ProfilingSummaryFrontend) => void
46
> = new Map();
47
22
- ProfilingSummary: Resource<RendererAndRootID, ProfilingSummaryFrontend>;
48
+ CommitDetails: Resource<CommitDetailsParams, CommitDetailsFrontend>;
49
+ CommitTree: Resource<CommitTreeParams, CommitTree>;
50
+ ProfilingSummary: Resource<ProfilingSummaryParams, ProfilingSummaryFrontend>;
51
52
constructor(bridge: Bridge, store: Store) {
53
+ this.CommitDetails = createResource(
54
+ ({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
55
+ return new Promise(resolve => {
56
+ if (!store.profilingOperations.has(rootID)) {
57
+ // If no profiling data was recorded for this root, skip the round trip.
58
+ resolve({
59
+ committedFibers: [],
60
+ interactions: [],
61
+ });
62
+ } else {
63
+ this._pendingCommitDetailsMap.set(
64
+ `${rootID}-${commitIndex}`,
65
+ resolve
66
+ );
67
+ bridge.send('getCommitDetails', {
68
+ commitIndex,
69
+ rendererID,
70
+ rootID,
71
+ });
72
+ }
73
+ });
74
+ },
75
+ ({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
76
+ `${rootID}-${commitIndex}`
77
+ );
78
+
79
+ this.CommitTree = createResource(
80
+ ({
81
+ commitIndex,
82
+ profilingSummary,
83
+ rendererID,
84
+ rootID,
85
+ }: CommitTreeParams) =>
86
+ new Promise(resolve =>
87
+ resolve(
88
+ getCommitTree({
89
+ commitIndex,
90
+ profilingSummary,
91
+ rendererID,
92
+ rootID,
93
+ store,
94
+ })
95
+ )
96
+ ),
97
+ ({
98
+ commitIndex,
99
+ profilingSummary,
100
+ rendererID,
101
+ rootID,
102
+ }: CommitTreeParams) => `${rootID}-${commitIndex}`
103
+ );
104
+
105
this.ProfilingSummary = createResource(
26
- ({ rendererID, rootID }: RendererAndRootID) => {
106
+ ({ rendererID, rootID }: ProfilingSummaryParams) => {
107
return new Promise(resolve => {
28
- if (!store._profilingOperations.has(rootID)) {
108
+ if (!store.profilingOperations.has(rootID)) {
109
// If no profiling data was recorded for this root, skip the round trip.
110
resolve({
111
commitDurations: [],
@@ -39,17 +119,38 @@ export default class ProfilingCache {
119
}
120
});
121
},
42
- ({ rendererID, rootID }: RendererAndRootID) => rootID
122
+ ({ rendererID, rootID }: ProfilingSummaryParams) => rootID
123
);
124
125
+ bridge.addListener('commitDetails', this.onCommitDetails);
126
bridge.addListener('profilingSummary', this.onProfileSummary);
127
}
128
129
invalidate() {
130
invalidateResources();
50
- this._pendingProfileSummaryMap = new Map();
131
+
132
+ this._pendingCommitDetailsMap.clear();
133
+ this._pendingProfileSummaryMap.clear();
134
}
135
136
+ onCommitDetails = ({
137
+ commitIndex,
138
+ committedFibers,
139
+ interactions,
140
+ rootID,
141
+ }: CommitDetailsBackend) => {
142
+ const key = `${rootID}-${commitIndex}`;
143
+ const resolve = this._pendingCommitDetailsMap.get(key);
144
+ if (resolve != null) {
145
+ this._pendingCommitDetailsMap.delete(key);
146
+
147
+ resolve({
148
+ committedFibers,
149
+ interactions,
150
+ });
151
+ }
152
+ };
153
+
154
onProfileSummary = ({
155
commitDurations,
156
commitTimes,
@@ -59,6 +160,7 @@ export default class ProfilingCache {
160
}: ProfilingSummaryBackend) => {
161
const resolve = this._pendingProfileSummaryMap.get(rootID);
162
if (resolve != null) {
163
+ this._pendingProfileSummaryMap.delete(rootID);
164
const initialTreeBaseDurationsMap = new Map();
165
for (let i = 0; i < initialTreeBaseDurations.length; i++) {
166
initialTreeBaseDurationsMap.set(
src/devtools/store.js
+16
-1
@@ -5,6 +5,7 @@ import {
5
TREE_OPERATION_ADD,
6
TREE_OPERATION_REMOVE,
7
TREE_OPERATION_RESET_CHILDREN,
8
+ TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
9
} from '../constants';
10
import { ElementTypeRoot } from './types';
11
import { utfDecodeString } from '../utils';
@@ -12,7 +13,7 @@ import { __DEBUG__ } from '../constants';
13
import ProfilingCache from './ProfilingCache';
14
15
import type { ElementType } from './types';
15
-import type { Element } from './views/elements/types';
16
+import type { Element } from './views/Elements/types';
17
import type { Bridge } from '../types';
18
19
const debug = (methodName, ...args) => {
@@ -119,6 +120,14 @@ export default class Store extends EventEmitter {
120
return this._profilingCache;
121
}
122
123
+ get profilingOperations(): Map<number, Array<Uint32Array>> {
124
+ return this._profilingOperations;
125
+ }
126
+
127
+ get profilingSnapshot(): Map<number, ProfilingSnapshotNode> {
128
+ return this._profilingSnapshot;
129
+ }
130
+
131
get revision(): number {
132
return this._revision;
133
}
@@ -507,6 +516,12 @@ export default class Store extends EventEmitter {
516
517
weightDelta = childWeight + 1 - prevWeight;
518
break;
519
+ case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
520
+ // Base duration updates are only sent while profiling is in progress.
521
+ // We can ignore them at this point.
522
+ // The profiler UI uses them lazily in order to generate the tree.
523
+ i = i + 3;
524
+ break;
525
default:
526
throw Error(`Unsupported Bridge operation ${operation}`);
527
}
src/devtools/views/Profiler/CommitFlamegraph.css
src/devtools/views/Profiler/CommitFlamegraph.js
new
+35
@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React, { useContext } from 'react';
4
+import { ProfilerContext } from './ProfilerContext';
5
+import { StoreContext } from '../context';
6
+
7
+import styles from './CommitFlamegraph.css';
8
+
9
+export default function CommitFlamegraph(_: {||}) {
10
+ const { rendererID, rootID, selectedCommitIndex } = useContext(
11
+ ProfilerContext
12
+ );
13
+
14
+ const { profilingCache } = useContext(StoreContext);
15
+
16
+ const profilingSummary = profilingCache.ProfilingSummary.read({
17
+ rendererID: ((rendererID: any): number),
18
+ rootID: ((rootID: any): number),
19
+ });
20
+
21
+ const commitDetails = profilingCache.CommitDetails.read({
22
+ commitIndex: ((selectedCommitIndex: any): number),
23
+ rendererID: ((rendererID: any): number),
24
+ rootID: ((rootID: any): number),
25
+ });
26
+
27
+ const commitTree = profilingCache.CommitTree.read({
28
+ commitIndex: ((selectedCommitIndex: any): number),
29
+ profilingSummary,
30
+ rendererID: ((rendererID: any): number),
31
+ rootID: ((rootID: any): number),
32
+ });
33
+
34
+ return 'Coming soon: Flamegraph';
35
+}
src/devtools/views/Profiler/CommitRanked.css
src/devtools/views/Profiler/CommitRanked.js
new
+35
@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React, { useContext } from 'react';
4
+import { ProfilerContext } from './ProfilerContext';
5
+import { StoreContext } from '../context';
6
+
7
+import styles from './CommitRanked.css';
8
+
9
+export default function CommitRanked(_: {||}) {
10
+ const { rendererID, rootID, selectedCommitIndex } = useContext(
11
+ ProfilerContext
12
+ );
13
+
14
+ const { profilingCache } = useContext(StoreContext);
15
+
16
+ const profilingSummary = profilingCache.ProfilingSummary.read({
17
+ rendererID: ((rendererID: any): number),
18
+ rootID: ((rootID: any): number),
19
+ });
20
+
21
+ const commitDetails = profilingCache.CommitDetails.read({
22
+ commitIndex: ((selectedCommitIndex: any): number),
23
+ rendererID: ((rendererID: any): number),
24
+ rootID: ((rootID: any): number),
25
+ });
26
+
27
+ const commitTree = profilingCache.CommitTree.read({
28
+ commitIndex: ((selectedCommitIndex: any): number),
29
+ profilingSummary,
30
+ rendererID: ((rendererID: any): number),
31
+ rootID: ((rootID: any): number),
32
+ });
33
+
34
+ return 'Coming soon: Ranked';
35
+}
src/devtools/views/Profiler/CommitTreeBuilder.js
new
+235
@@ -0,0 +1,235 @@
1
+// @flow
2
+
3
+import {
4
+ TREE_OPERATION_ADD,
5
+ TREE_OPERATION_REMOVE,
6
+ TREE_OPERATION_RESET_CHILDREN,
7
+ TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
8
+} from 'src/constants';
9
+import { utfDecodeString } from 'src/utils';
10
+import { ElementTypeRoot } from 'src/devtools/types';
11
+import Store from 'src/devtools/store';
12
+
13
+import type { ElementType } from 'src/devtools/types';
14
+import type {
15
+ CommitTree,
16
+ Node,
17
+ ProfilingSummary as ProfilingSummaryFrontend,
18
+} from 'src/devtools/views/Profiler/types';
19
+
20
+const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
21
+
22
+export function getCommitTree({
23
+ commitIndex,
24
+ profilingSummary,
25
+ rendererID,
26
+ rootID,
27
+ store,
28
+}: {|
29
+ commitIndex: number,
30
+ profilingSummary: ProfilingSummaryFrontend,
31
+ rendererID: number,
32
+ rootID: number,
33
+ store: Store,
34
+|}): CommitTree {
35
+ if (store.profilingSnapshot.has(rootID)) {
36
+ if (!rootToCommitTreeMap.has(rootID)) {
37
+ rootToCommitTreeMap.set(rootID, []);
38
+ }
39
+
40
+ const commitTrees = ((rootToCommitTreeMap.get(
41
+ rootID
42
+ ): any): Array<CommitTree>);
43
+
44
+ if (commitIndex < commitTrees.length) {
45
+ return commitTrees[commitIndex];
46
+ }
47
+
48
+ // Commits are generated sequentially and cached.
49
+ // If this is the very first commit, start with the cached snapshot and apply the first mutation.
50
+ // Otherwise load (or generate) the previous commit and append a mutation to it.
51
+ if (commitIndex === 0) {
52
+ const initialCommitTree = {
53
+ nodes: new Map(),
54
+ rootID,
55
+ };
56
+
57
+ // Construct the initial tree.
58
+ const queue: Array<number> = [rootID];
59
+ while (queue.length > 0) {
60
+ const currentID = queue.pop();
61
+ const currentNode = ((store.profilingSnapshot.get(
62
+ currentID
63
+ ): any): Node);
64
+
65
+ initialCommitTree.nodes.set(currentID, {
66
+ id: currentID,
67
+ children: currentNode.children,
68
+ displayName: currentNode.displayName,
69
+ key: currentNode.key,
70
+ parentID: 0,
71
+ treeBaseDuration: ((profilingSummary.initialTreeBaseDurations.get(
72
+ currentID
73
+ ): any): number),
74
+ });
75
+
76
+ queue.push(...currentNode.children);
77
+ }
78
+
79
+ // Mutate the tree
80
+ const commitOperations = store.profilingOperations.get(rootID);
81
+ if (commitOperations != null && commitIndex < commitOperations.length) {
82
+ const commitTree = updateTree(
83
+ initialCommitTree,
84
+ commitOperations[commitIndex]
85
+ );
86
+ commitTrees.push(commitTree);
87
+ return commitTree;
88
+ }
89
+ } else {
90
+ const previousCommitTree = getCommitTree({
91
+ commitIndex: commitIndex - 1,
92
+ profilingSummary,
93
+ rendererID,
94
+ rootID,
95
+ store,
96
+ });
97
+ const commitOperations = store.profilingOperations.get(rootID);
98
+ if (commitOperations != null && commitIndex < commitOperations.length) {
99
+ const commitTree = updateTree(
100
+ previousCommitTree,
101
+ commitOperations[commitIndex]
102
+ );
103
+ commitTrees.push(commitTree);
104
+ return commitTree;
105
+ }
106
+ }
107
+ }
108
+
109
+ // TODO (profiling) Should I throw here? Is this ever expected?
110
+ return {
111
+ nodes: new Map(),
112
+ rootID,
113
+ };
114
+}
115
+
116
+function updateTree(
117
+ commitTree: CommitTree,
118
+ operations: Uint32Array
119
+): CommitTree {
120
+ const nodes = new Map(commitTree.nodes);
121
+
122
+ let i = 2;
123
+ while (i < operations.length) {
124
+ let id: number = ((null: any): number);
125
+ let node: Node = ((null: any): Node);
126
+ let parentID: number = ((null: any): number);
127
+ let parentNode: Node = ((null: any): Node);
128
+ let type: ElementType = ((null: any): ElementType);
129
+
130
+ const operation = operations[i];
131
+
132
+ switch (operation) {
133
+ case TREE_OPERATION_ADD:
134
+ id = ((operations[i + 1]: any): number);
135
+ type = ((operations[i + 2]: any): ElementType);
136
+
137
+ i = i + 3;
138
+
139
+ if (type === ElementTypeRoot) {
140
+ // No-op
141
+ } else {
142
+ parentID = ((operations[i]: any): number);
143
+ i++;
144
+
145
+ i++; // ownerID
146
+
147
+ const displayNameLength = operations[i];
148
+ i++;
149
+ const displayName =
150
+ displayNameLength === 0
151
+ ? null
152
+ : utfDecodeString(
153
+ (operations.slice(i, i + displayNameLength): any)
154
+ );
155
+ i += displayNameLength;
156
+
157
+ const keyLength = operations[i];
158
+ i++;
159
+ const key =
160
+ keyLength === 0
161
+ ? null
162
+ : utfDecodeString((operations.slice(i, i + keyLength): any));
163
+ i += +keyLength;
164
+
165
+ if (nodes.has(id)) {
166
+ // The renderer's tree walking approach sometimes mounts the same Fiber twice with Suspense and Lazy.
167
+ // For now, we avoid adding it to the tree twice by checking if it's already been mounted.
168
+ // Maybe in the future we'll revisit this.
169
+ } else {
170
+ parentNode = ((nodes.get(parentID): any): Node);
171
+ parentNode.children = parentNode.children.concat(id);
172
+
173
+ const node: Node = {
174
+ children: [],
175
+ displayName,
176
+ id,
177
+ key,
178
+ parentID,
179
+ treeBaseDuration: 0, // This will be updated by a subsequent operation
180
+ };
181
+
182
+ nodes.set(id, node);
183
+ }
184
+ }
185
+ break;
186
+ case TREE_OPERATION_REMOVE:
187
+ id = ((operations[i + 1]: any): number);
188
+
189
+ i = i + 2;
190
+
191
+ node = ((nodes.get(id): any): Node);
192
+ parentID = node.parentID;
193
+
194
+ nodes.delete(id);
195
+
196
+ parentNode = ((nodes.get(parentID): any): Node);
197
+ if (parentNode == null) {
198
+ // No-op
199
+ } else {
200
+ parentNode.children = parentNode.children.filter(
201
+ childID => childID !== id
202
+ );
203
+ }
204
+ break;
205
+ case TREE_OPERATION_RESET_CHILDREN:
206
+ id = ((operations[i + 1]: any): number);
207
+ const numChildren = ((operations[i + 2]: any): number);
208
+ const children = ((operations.slice(
209
+ i + 3,
210
+ i + 3 + numChildren
211
+ ): any): Array<number>);
212
+
213
+ i = i + 3 + numChildren;
214
+
215
+ node = ((nodes.get(id): any): Node);
216
+ node.children = Array.from(children);
217
+ break;
218
+ case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
219
+ id = operations[i + 1];
220
+
221
+ node = ((nodes.get(id): any): Node);
222
+ node.treeBaseDuration = operations[i + 2];
223
+
224
+ i = i + 3;
225
+ break;
226
+ default:
227
+ throw Error(`Unsupported Bridge operation ${operation}`);
228
+ }
229
+ }
230
+
231
+ return {
232
+ nodes,
233
+ rootID: commitTree.rootID,
234
+ };
235
+}
src/devtools/views/Profiler/Profiler.css
+10
@@ -81,3 +81,13 @@
81
font-size: var(--font-size-sans-large);
82
padding: 0.5rem;
83
}
84
+
85
+.Fallback {
86
+ width: 100%;
87
+ height: 100%;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ font-size: var(--font-size-sans-large);
92
+ color: var(--color-dim);
93
+}
src/devtools/views/Profiler/Profiler.js
+66
-19
@@ -5,6 +5,8 @@ import { ProfilerContext } from './ProfilerContext';
5
import Button from '../Button';
6
import ButtonIcon from '../ButtonIcon';
7
import TabBar from '../TabBar';
8
+import CommitFlamegraph from './CommitFlamegraph';
9
+import CommitRanked from './CommitRanked';
10
import FilterModal from './FilterModal';
11
import RecordToggle from './RecordToggle';
12
import SnapshotSelector from './SnapshotSelector';
@@ -12,24 +14,41 @@ import SnapshotSelector from './SnapshotSelector';
14
import styles from './Profiler.css';
15
16
export default function Profiler(_: {||}) {
15
- const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
17
+ const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
18
+ ProfilerContext
19
+ );
20
17
- if (isProfiling || !hasProfilingData) {
18
- return <NonSuspendingProfiler isProfiling={isProfiling} />;
19
- } else {
21
+ if (isProfiling || !rootHasProfilingData) {
22
return (
21
- <Suspense fallback={<ProfilerFallback />}>
22
- <SuspendingProfiler />
23
- </Suspense>
23
+ <NonSuspendingProfiler
24
+ hasProfilingData={hasProfilingData}
25
+ isProfiling={isProfiling}
26
+ />
27
);
28
+ } else {
29
+ return <SuspendingProfiler />;
30
}
31
}
32
33
// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
34
// Nothing in this view's subtree suspends.
35
// By not suspending while profiling is in progress, we avoid potential cache invalidation trickiness.
31
-function NonSuspendingProfiler({ isProfiling }: {| isProfiling: boolean |}) {
32
- const view = isProfiling ? <RecortdingInProgress /> : <NoProfilingData />;
36
+// NOTE that the structure of this UI should mirror SuspendingProfiler.
37
+function NonSuspendingProfiler({
38
+ hasProfilingData,
39
+ isProfiling,
40
+}: {|
41
+ hasProfilingData: boolean,
42
+ isProfiling: boolean,
43
+|}) {
44
+ let view = null;
45
+ if (isProfiling) {
46
+ view = <RecortdingInProgress />;
47
+ } else if (!hasProfilingData) {
48
+ view = <NoProfilingData />;
49
+ } else {
50
+ view = <NoProfilingDataForRoot />;
51
+ }
52
53
return (
54
<div className={styles.Profiler}>
@@ -58,21 +77,33 @@ function NonSuspendingProfiler({ isProfiling }: {| isProfiling: boolean |}) {
77
78
// TODO (profiling) Real fallback UI
79
function ProfilerFallback() {
61
- return <div>Loading...</div>;
80
+ return <div className={styles.Fallback}>Loading...</div>;
81
}
82
83
// This view is rendered when there is profiler data (even though there may not be any for the currently selected root).
84
// This view's subtree uses suspense to request profiler data from the backend.
66
-function SuspendingProfiler(_: {||}) {
85
+// NOTE that the structure of this UI should mirror NonSuspendingProfiler.
86
+function SuspendingProfiler() {
87
const [tab, setTab] = useState('flame-chart');
88
const [isFilterModalShowing, setIsFilterModalShowing] = useState(false);
89
90
const showFilterModal = useCallback(() => setIsFilterModalShowing(true));
91
const dismissFilterModal = useCallback(() => setIsFilterModalShowing(false));
92
73
- // TODO (profiling) Show selected "tab" view
74
- // TODO (profiling) Handle cases: no selected commit, no data for root
75
- const view = <div>Coming soon...</div>;
93
+ let view = null;
94
+ switch (tab) {
95
+ case 'flame-chart':
96
+ view = <CommitFlamegraph />;
97
+ break;
98
+ case 'ranked-chart':
99
+ view = <CommitRanked />;
100
+ break;
101
+ case 'interactions':
102
+ view = <div>Coming soon: Interactions</div>; // TODO (profiling)
103
+ break;
104
+ default:
105
+ break;
106
+ }
107
108
return (
109
<div className={styles.Profiler}>
@@ -95,19 +126,23 @@ function SuspendingProfiler(_: {||}) {
126
<Button onClick={showFilterModal} title="Filter commits by duration">
127
<ButtonIcon type="filter" />
128
</Button>
98
- <SnapshotSelector />
129
+ <Suspense fallback={<ProfilerFallback />}>
130
+ <SnapshotSelector />
131
+ </Suspense>
132
</div>
133
<div className={styles.Content}>
101
- {view}
134
+ <Suspense fallback={<ProfilerFallback />}>{view}</Suspense>
135
{isFilterModalShowing && (
136
<FilterModal dismissModal={dismissFilterModal} />
137
)}
138
</div>
139
</div>
140
<div className={styles.RightColumn}>
108
- {/* TODO (profiler) Dynamic information */}
109
- <div className={styles.Toolbar}>Commit information</div>
110
- <div className={styles.InspectedProperties} />
141
+ <Suspense fallback={<ProfilerFallback />}>
142
+ {/* TODO (profiler) Dynamic information */}
143
+ <div className={styles.Toolbar}>Commit information</div>
144
+ <div className={styles.InspectedProperties} />
145
+ </Suspense>
146
</div>
147
</div>
148
);
@@ -143,6 +178,18 @@ const NoProfilingData = () => (
178
</div>
179
);
180
181
+const NoProfilingDataForRoot = () => (
182
+ <div className={styles.Column}>
183
+ <div className={styles.Header}>
184
+ No profiling data has been recorded for the selected root.
185
+ </div>
186
+ <div className={styles.Row}>
187
+ Select a different root in the elements panel, or click the record button{' '}
188
+ <RecordToggle /> to start recording.
189
+ </div>
190
+ </div>
191
+);
192
+
193
const RecortdingInProgress = () => (
194
<div className={styles.Column}>
195
<div className={styles.Header}>Profiling is in progress...</div>
src/devtools/views/Profiler/ProfilerContext.js
+8
@@ -28,6 +28,7 @@ type Context = {|
28
// If nothing is selected though, this will default to the first root.
29
rendererID: number | null,
30
rootID: number | null,
31
+ rootHasProfilingData: boolean,
32
33
// Controls whether commits are filtered by duration.
34
// This value is controlled by a filter toggle UI in the Profiler toolbar.
@@ -85,14 +86,19 @@ function ProfilerContextController({ children }: Props) {
86
// or use a portal to share the contexts themselves between Chrome tabs.
87
let rendererID = null;
88
let rootID = null;
89
+ let rootHasProfilingData = false;
90
if (selectedElementID) {
91
rendererID = store.getRendererIDForElement(
92
((selectedElementID: any): number)
93
);
94
rootID = store.getRootIDForElement(((selectedElementID: any): number));
95
+ rootHasProfilingData = store.profilingOperations.has(
96
+ ((rootID: any): number)
97
+ );
98
} else if (store.roots.length > 0) {
99
// If no root is selected, assume the first root; many React apps are single root anyway.
100
rootID = store.roots[0];
101
+ rootHasProfilingData = store.profilingOperations.has(rootID);
102
rendererID = store.getRendererIDForElement(((rootID: any): number));
103
}
104
@@ -121,6 +127,7 @@ function ProfilerContextController({ children }: Props) {
127
128
rendererID,
129
rootID,
130
+ rootHasProfilingData,
131
132
isCommitFilterEnabled,
133
setIsCommitFilterEnabled,
@@ -138,6 +145,7 @@ function ProfilerContextController({ children }: Props) {
145
146
rendererID,
147
rootID,
148
+ rootHasProfilingData,
149
150
isCommitFilterEnabled,
151
setIsCommitFilterEnabled,
src/devtools/views/Profiler/SnapshotSelector.js
+27
-19
@@ -41,7 +41,7 @@ export default function SnapshotSelector(_: Props) {
41
[commitDurations, isCommitFilterEnabled, minCommitDuration]
42
);
43
44
- const numCommits = filteredCommitIndices.length;
44
+ const numFilteredCommits = filteredCommitIndices.length;
45
46
// Map the (unfiltered) selected commit index to an index within the filtered data.
47
const selectedFilteredCommitIndex = useMemo(() => {
@@ -56,19 +56,24 @@ export default function SnapshotSelector(_: Props) {
56
}, [filteredCommitIndices, selectedCommitIndex]);
57
58
if (selectedFilteredCommitIndex === null) {
59
- if (numCommits > 0) {
59
+ if (numFilteredCommits > 0) {
60
setSelectedCommitIndex(0);
61
}
62
- } else if (selectedFilteredCommitIndex >= numCommits) {
63
- setSelectedCommitIndex(numCommits === 0 ? null : numCommits - 1);
62
+ } else if (selectedFilteredCommitIndex >= numFilteredCommits) {
63
+ setSelectedCommitIndex(
64
+ numFilteredCommits === 0 ? null : numFilteredCommits - 1
65
+ );
66
}
67
66
- let currentCommitNumber = '-';
67
- if (numCommits > 0) {
68
- currentCommitNumber = `${selectedFilteredCommitIndex + 1}`.padStart(
69
- `${numCommits}`.length,
70
- '0'
71
- );
68
+ let label = null;
69
+ if (numFilteredCommits > 0) {
70
+ label =
71
+ `${selectedFilteredCommitIndex + 1}`.padStart(
72
+ `${numFilteredCommits}`.length,
73
+ '0'
74
+ ) +
75
+ ' / ' +
76
+ numFilteredCommits;
77
}
78
79
const viewNextCommit = useCallback(() => {
@@ -101,12 +106,10 @@ export default function SnapshotSelector(_: Props) {
106
return (
107
<Fragment>
108
<div className={styles.VRule} />
104
- <span className={styles.IndexLabel}>
105
- {numCommits > 0 ? `${currentCommitNumber} / ${numCommits}` : '-'}
106
- </span>
109
+ <span className={styles.IndexLabel}>{label}</span>
110
<Button
111
className={styles.Button}
109
- disabled={selectedFilteredCommitIndex === 0 || numCommits === 0}
112
+ disabled={selectedFilteredCommitIndex === 0 || numFilteredCommits === 0}
113
onClick={viewPrevCommit}
114
>
115
<ButtonIcon type="previous" />
@@ -114,11 +117,14 @@ export default function SnapshotSelector(_: Props) {
117
<div
118
className={styles.Commits}
119
style={{
117
- flex: numCommits > 0 ? '1 1 auto' : '0 0 auto',
118
- maxWidth: numCommits > 0 ? numCommits * maxBarWidth : undefined,
120
+ flex: numFilteredCommits > 0 ? '1 1 auto' : '0 0 auto',
121
+ maxWidth:
122
+ numFilteredCommits > 0
123
+ ? numFilteredCommits * maxBarWidth
124
+ : undefined,
125
}}
126
>
121
- {numCommits > 0 && (
127
+ {numFilteredCommits > 0 && (
128
<SnapshotCommitList
129
commitDurations={commitDurations}
130
commitTimes={commitTimes}
@@ -128,13 +134,15 @@ export default function SnapshotSelector(_: Props) {
134
setSelectedCommitIndex={setSelectedCommitIndex}
135
/>
136
)}
131
- {numCommits === 0 && <div className={styles.NoCommits}>No commits</div>}
137
+ {numFilteredCommits === 0 && (
138
+ <div className={styles.NoCommits}>No commits</div>
139
+ )}
140
</div>
141
<Button
142
className={styles.Button}
143
disabled={
144
selectedFilteredCommitIndex === null ||
137
- selectedFilteredCommitIndex >= numCommits - 1
145
+ selectedFilteredCommitIndex >= numFilteredCommits - 1
146
}
147
onClick={viewNextCommit}
148
>
src/devtools/views/Profiler/types.js
+19
-19
@@ -1,31 +1,31 @@
1
// @flow
2
3
-// Map of root ID to commit durations.
4
-// Roots that were committed during the profiling session will have a non-empty array of values.
5
-// Each value in the array represents the total amount of time the commit took.
6
-// More detailed information about a commit must be requested separately.
7
-export type CommitDurationsMap = Map<number, Array<number>>;
8
-
9
-export type Interaction = {|
10
- id: number,
11
- timestamp: number,
12
- label: string,
13
-|};
14
-
3
export type Node = {|
16
- actualDuration: number,
17
- baseDuration: number,
18
- displayName: string,
4
id: number,
5
+ children: Array<number>,
6
+ displayName: string | null,
7
+ key: number | string | null,
8
parentID: number,
21
- selfDuration: number,
9
+ treeBaseDuration: number,
10
+|};
11
+
12
+export type CommitTree = {|
13
+ nodes: Map<number, Node>,
14
+ rootID: number,
15
+|};
16
+
17
+export type Interaction = {|
18
+ id: string,
19
+ name: string,
20
+ timestamp: number,
21
|};
22
23
export type CommitDetails = {|
25
- duration: number,
24
interactions: Array<Interaction>,
27
- root: Node,
28
- timestamp: number,
25
+ committedFibers: Array<{|
26
+ actualDuration: number,
27
+ id: number,
28
+ |}>,
29
|};
30
31
export type ProfilingSummary = {|