@samitouri / QOS-React / commits / 821a6504b8

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 = {|