Initial profiling shell added
Lots of TODOs and unfinished views here.
Brian Vaughn committed
Mar 10, 2019 at 09:01 UTC
2d6800728889a5141b7a1c08842f2e90ffc9448a
28 files changed
+938
-185
OVERVIEW.md
+1
-1
@@ -175,7 +175,7 @@ Profiling information is stored on the backend. The backend push-notifies the fr
175
176
When profiling begins, the frontend takes a snapshot/copy of each root. This snapshot includes the id, name, key, and child IDs for each node in the tree. (This information is already present on the frontend, so it does not require any additional bridge traffic.) While profiling is active, each time React commits– the frontend also stores a copy of the "_operations_" message (described above). Once profiling has finished, the frontend can use the original snapshot along with each of the stored "_operations_" messages to reconstruct the tree for each of the profiled commits.
177
178
-While profiling is in progress, the backend also stores some information <sup>1</sup> about each commit:
178
+When profiling begins, the backend records the base durations of each fiber currently in the tree. While profiling is in progress, the backend also stores some information <sup>1</sup> about each commit:
179
* Commit time and duration
180
* Which elements were rendered during that commit.
181
* Which interactions (if any) were part of the commit.
src/backend/agent.js
+33
-2
@@ -40,11 +40,13 @@ type SetInParams = {|
40
41
export default class Agent extends EventEmitter {
42
_bridge: Bridge = ((null: any): Bridge);
43
+ _isProfiling: boolean = false;
44
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
45
46
addBridge(bridge: Bridge) {
47
this._bridge = bridge;
48
49
+ bridge.addListener('getProfilingStatus', this.getProfilingStatus);
50
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
51
bridge.addListener('inspectElement', this.inspectElement);
52
bridge.addListener('overrideContext', this.overrideContext);
@@ -53,7 +55,9 @@ export default class Agent extends EventEmitter {
55
bridge.addListener('overrideState', this.overrideState);
56
bridge.addListener('selectElement', this.selectElement);
57
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
58
+ bridge.addListener('startProfiling', this.startProfiling);
59
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
60
+ bridge.addListener('stopProfiling', this.stopProfiling);
61
bridge.addListener('shutdown', this.shutdown);
62
bridge.addListener('viewElementSource', this.viewElementSource);
63
}
@@ -62,14 +66,19 @@ export default class Agent extends EventEmitter {
66
for (let rendererID in this._rendererInterfaces) {
67
// A renderer will throw if it can't find a fiber for the specified node.
68
try {
65
- // $FlowFixMe
66
- const renderer = this._rendererInterfaces[rendererID];
69
+ const renderer = ((this._rendererInterfaces[
70
+ (rendererID: any)
71
+ ]: any): RendererInterface);
72
return renderer.getFiberIDFromNative(node, true);
73
} catch (e) {}
74
}
75
return null;
76
}
77
78
+ getProfilingStatus = () => {
79
+ this._bridge.send('profilingStatus', this._isProfiling);
80
+ };
81
+
82
highlightElementInDOM = ({
83
displayName,
84
id,
@@ -182,6 +191,17 @@ export default class Agent extends EventEmitter {
191
window.addEventListener('mouseover', this._onMouseOver, true);
192
};
193
194
+ startProfiling = () => {
195
+ this._isProfiling = true;
196
+ for (let rendererID in this._rendererInterfaces) {
197
+ const renderer = ((this._rendererInterfaces[
198
+ (rendererID: any)
199
+ ]: any): RendererInterface);
200
+ renderer.startProfiling();
201
+ }
202
+ this._bridge.send('profilingStatus', this._isProfiling);
203
+ };
204
+
205
stopInspectingDOM = () => {
206
hideOverlay();
207
@@ -190,6 +210,17 @@ export default class Agent extends EventEmitter {
210
window.removeEventListener('mouseover', this._onMouseOver, true);
211
};
212
213
+ stopProfiling = () => {
214
+ this._isProfiling = false;
215
+ for (let rendererID in this._rendererInterfaces) {
216
+ const renderer = ((this._rendererInterfaces[
217
+ (rendererID: any)
218
+ ]: any): RendererInterface);
219
+ renderer.startProfiling();
220
+ }
221
+ this._bridge.send('profilingStatus', this._isProfiling);
222
+ };
223
+
224
viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
225
const renderer = this._rendererInterfaces[rendererID];
226
if (renderer == null) {
src/backend/renderer.js
+102
-6
@@ -26,11 +26,12 @@ import { inspectHooksOfFiber } from './ReactDebugHooks';
26
import type {
27
DevToolsHook,
28
Fiber,
29
+ Interaction,
30
ReactRenderer,
31
FiberData,
32
RendererInterface,
33
} from './types';
33
-import type { InspectedElement } from 'src/devtools/types';
34
+import type { InspectedElement } from 'src/devtools/views/elements/types';
35
36
function getInternalReactConstants(version) {
37
const ReactSymbols = {
@@ -459,6 +460,12 @@ export function attach(
460
const idToFiberMap: Map<number, Fiber> = new Map();
461
const primaryFibers: Set<Fiber> = new Set();
462
463
+ // When profiling is supported, we store the latest tree base durations for each Fiber.
464
+ // This is so that we can quickly capture a snapshot of those values if profiling starts.
465
+ // If we didn't store these values, we'd have to crawl the tree when profiling started,
466
+ // and use a slow path to find each of the current Fibers.
467
+ const idToTreeBaseDurationMap: Map<number, number> = new Map();
468
+
469
function getFiberID(primaryFiber: Fiber): number {
470
if (!fiberToIDMap.has(primaryFiber)) {
471
const id = getUID();
@@ -525,7 +532,12 @@ export function attach(
532
533
function flushPendingEvents(root: Object): void {
534
if (pendingOperations.length === 0) {
528
- return;
535
+ // If we're currently profiling, send an "operations" method even if there are no mutations to the tree.
536
+ // The frontend needs this no-op info to know how to reconstruct the tree for each commit,
537
+ // even if a particular commit didn't change the shape of the tree.
538
+ if (!isProfiling) {
539
+ return;
540
+ }
541
}
542
543
// Identify which renderer this update is coming from.
@@ -546,11 +558,17 @@ export function attach(
558
const isRoot = fiber.tag === HostRoot;
559
const id = getFiberID(getPrimaryFiber(fiber));
560
561
+ const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
562
+ if (isProfilingSupported) {
563
+ idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration);
564
+ }
565
+
566
if (isRoot) {
550
- const operation = new Uint32Array(3);
567
+ const operation = new Uint32Array(4);
568
operation[0] = TREE_OPERATION_ADD;
569
operation[1] = id;
570
operation[2] = ElementTypeRoot;
571
+ operation[3] = isProfilingSupported ? 1 : 0;
572
addOperation(operation);
573
} else {
574
const { displayName, key, type } = getDataForFiber(fiber);
@@ -616,8 +634,9 @@ export function attach(
634
operation[1] = id;
635
addOperation(operation, true);
636
}
619
- idToFiberMap.delete(id);
637
fiberToIDMap.delete(primaryFiber);
638
+ idToFiberMap.delete(id);
639
+ idToTreeBaseDurationMap.delete(primaryFiber);
640
primaryFibers.delete(primaryFiber);
641
}
642
@@ -645,6 +664,26 @@ export function attach(
664
) {
665
debug('enqueueUpdateIfNecessary()', fiber);
666
667
+ if (isProfiling) {
668
+ if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
669
+ const id = getFiberID(getPrimaryFiber(fiber));
670
+
671
+ idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration);
672
+
673
+ if (fiber.actualDuration > 0) {
674
+ // If profiling is active, store durations for elements that were rendered during the commit.
675
+ ((currentCommitProfilingMetadata: any): CommitProfilingData).committedFibers.push(
676
+ {
677
+ id,
678
+ actualDuration: fiber.actualDuration,
679
+ selfDuration: fiber.selfDuration,
680
+ treeBaseDuration: fiber.treeBaseDuration,
681
+ }
682
+ );
683
+ }
684
+ }
685
+ }
686
+
687
// The frontend only really cares about the displayName, key, and children.
688
// The first two don't really change, so we are only concerned with the order of children here.
689
// This is trickier than a simple comparison though, since certain types of fibers are filtered.
@@ -667,8 +706,6 @@ export function attach(
706
operation.set(nextChildren, 3);
707
addOperation(operation);
708
}
670
-
671
- // TODO (profiling) If we're profiling, also check to see if that data has changed.
709
}
710
711
function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
@@ -798,6 +835,21 @@ export function attach(
835
const current = root.current;
836
const alternate = current.alternate;
837
838
+ if (isProfiling) {
839
+ // If profiling is active, store commit time and duration, and the current interactions.
840
+ // The frontend may request this information after profiling has stopped.
841
+ currentCommitProfilingMetadata = {
842
+ committedFibers: [],
843
+ commitTime: performance.now() - profilingStartTime,
844
+ interactions: Array.from(root.memoizedInteractions).map(
845
+ ({ name, timestamp }: Interaction) => ({
846
+ name,
847
+ timestamp: timestamp - profilingStartTime,
848
+ })
849
+ ),
850
+ };
851
+ }
852
+
853
if (alternate) {
854
// TODO: relying on this seems a bit fishy.
855
const wasMounted =
@@ -819,6 +871,13 @@ export function attach(
871
// Mount a new root.
872
mountFiber(current, null);
873
}
874
+
875
+ if (isProfiling) {
876
+ ((commitProfilingMetadata: any): Array<CommitProfilingData>).push(
877
+ ((currentCommitProfilingMetadata: any): CommitProfilingData)
878
+ );
879
+ }
880
+
881
// We're done here.
882
flushPendingEvents(root);
883
}
@@ -1263,6 +1322,41 @@ export function attach(
1322
}
1323
}
1324
1325
+ type CommittedFiber = {|
1326
+ actualDuration: number,
1327
+ id: number,
1328
+ selfDuration: number,
1329
+ treeBaseDuration: number,
1330
+ |};
1331
+
1332
+ type CommitProfilingData = {|
1333
+ committedFibers: Array<CommittedFiber>,
1334
+ commitTime: number,
1335
+ interactions: Array<Interaction>,
1336
+ |};
1337
+
1338
+ let commitProfilingMetadata: Array<CommitProfilingData> | null = null;
1339
+ let currentCommitProfilingMetadata: CommitProfilingData | null = null;
1340
+ let initialTreeBaseDurations: Array<number> | null = null;
1341
+ let isProfiling: boolean = false;
1342
+ let profilingStartTime: number = 0;
1343
+
1344
+ function startProfiling() {
1345
+ commitProfilingMetadata = [];
1346
+ initialTreeBaseDurations = [];
1347
+ isProfiling = true;
1348
+ profilingStartTime = performance.now();
1349
+
1350
+ // Save initial treeBaseDurations; we'll need to send them later.
1351
+ idToTreeBaseDurationMap.forEach((treeBaseDuration: number, id: number) => {
1352
+ initialTreeBaseDurations = [id, treeBaseDuration];
1353
+ });
1354
+ }
1355
+
1356
+ function stopProfiling() {
1357
+ isProfiling = false;
1358
+ }
1359
+
1360
return {
1361
cleanup,
1362
getFiberIDFromNative,
@@ -1277,6 +1371,8 @@ export function attach(
1371
setInHook,
1372
setInProps,
1373
setInState,
1374
+ startProfiling,
1375
+ stopProfiling,
1376
walkTree,
1377
};
1378
}
src/backend/types.js
+4
-1
@@ -1,6 +1,7 @@
1
// @flow
2
3
-import type { ElementType, InspectedElement } from 'src/devtools/types';
3
+import type { ElementType } from 'src/devtools/types';
4
+import type { InspectedElement } from 'src/devtools/views/elements/types';
5
6
type BundleType =
7
| 0 // PROD
@@ -75,6 +76,8 @@ export type RendererInterface = {
76
) => void,
77
setInProps: (id: number, path: Array<string | number>, value: any) => void,
78
setInState: (id: number, path: Array<string | number>, value: any) => void,
79
+ startProfiling: () => void,
80
+ stopProfiling: () => void,
81
walkTree: () => void,
82
};
83
src/backend/utils.js
+1
-1
@@ -2,7 +2,7 @@
2
3
import { dehydrate } from '../hydration';
4
5
-import type { DehydratedData } from 'src/devtools/types';
5
+import type { DehydratedData } from 'src/devtools/views/elements/types';
6
7
export function cleanForBridge(data: Object | null): DehydratedData | null {
8
if (data !== null) {
src/devtools/store.js
+28
-1
@@ -10,7 +10,8 @@ import { ElementTypeRoot } from './types';
10
import { utfDecodeString } from '../utils';
11
import { __DEBUG__ } from '../constants';
12
13
-import type { Element, ElementType } from './types';
13
+import type { ElementType } from './types';
14
+import type { Element } from './views/elements/types';
15
import type { Bridge } from '../types';
16
17
const debug = (methodName, ...args) => {
@@ -24,6 +25,10 @@ const debug = (methodName, ...args) => {
25
}
26
};
27
28
+export type Capabilities = {|
29
+ supportsProfiling: boolean,
30
+|};
31
+
32
/**
33
* The store is the single source of truth for updates from the backend.
34
* ContextProviders can subscribe to the Store for specific things they want to provide.
@@ -47,9 +52,13 @@ export default class Store extends EventEmitter {
52
// Passive effects will check it for changes between render and mount.
53
_roots: $ReadOnlyArray<number> = [];
54
55
+ _rootIDToCapabilities: Map<number, Capabilities> = new Map();
56
+
57
// Renderer ID is needed to support inspection fiber props, state, and hooks.
58
_rootIDToRendererID: Map<number, number> = new Map();
59
60
+ _supportsProfiling: boolean = false;
61
+
62
constructor(bridge: Bridge) {
63
super();
64
@@ -72,6 +81,10 @@ export default class Store extends EventEmitter {
81
return this._roots;
82
}
83
84
+ get supportsProfiling(): boolean {
85
+ return this._supportsProfiling;
86
+ }
87
+
88
getElementAtIndex(index: number): Element | null {
89
if (index < 0 || index >= this.numElements) {
90
console.warn(
@@ -245,8 +258,12 @@ export default class Store extends EventEmitter {
258
// For now, we avoid adding it to the tree twice by checking if it's already been mounted.
259
// Maybe in the future we'll revisit this.
260
} else {
261
+ const supportsProfiling = operations[i] > 0;
262
+ i++;
263
+
264
this._roots = this._roots.concat(id);
265
this._rootIDToRendererID.set(id, rendererID);
266
+ this._rootIDToCapabilities.set(id, { supportsProfiling });
267
268
this._idToElement.set(id, {
269
children: [],
@@ -341,6 +358,9 @@ export default class Store extends EventEmitter {
358
359
this._roots = this._roots.filter(rootID => rootID !== id);
360
this._rootIDToRendererID.delete(id);
361
+ this._rootIDToCapabilities.delete(id);
362
+
363
+ haveRootsChanged = true;
364
} else {
365
debug('Remove', `fiber ${id} from parent ${parentID}`);
366
@@ -399,6 +419,13 @@ export default class Store extends EventEmitter {
419
this._revision++;
420
421
if (haveRootsChanged) {
422
+ this._supportsProfiling = false;
423
+ this._rootIDToCapabilities.forEach(({ supportsProfiling }) => {
424
+ if (supportsProfiling) {
425
+ this._supportsProfiling = true;
426
+ }
427
+ });
428
+
429
this.emit('roots');
430
}
431
src/devtools/types.js
-62
@@ -14,65 +14,3 @@ export const ElementTypeSuspense = 9;
14
// These types may be used to visually distinguish types,
15
// or to enable/disable certain functionality.
16
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
17
-
18
-// Each element on the frontend corresponds to a Fiber on the backend.
19
-// Some of its information (e.g. id, type, displayName) come from the backend.
20
-// Other bits (e.g. weight and depth) are computed on the frontend for windowing and display purposes.
21
-// Elements are udpated on a push basis– meaning the backend pushes updates to the frontend when needed.
22
-export type Element = {|
23
- id: number,
24
- parentID: number,
25
- children: Array<number>,
26
- type: ElementType,
27
- displayName: string | null,
28
- key: number | string | null,
29
-
30
- // Owner (if available)
31
- ownerID: number,
32
-
33
- // How many levels deep within the tree is this element?
34
- // This determines how much indentation (left padding) should be used in the Elements tree.
35
- depth: number,
36
-
37
- // How many nodes (including itself) are below this Element within the tree.
38
- // This property is used to quickly determine the total number of Elements,
39
- // and the Element at any given index (for windowing purposes).
40
- weight: number,
41
-|};
42
-
43
-export type Owner = {|
44
- displayName: string,
45
- id: number,
46
-|};
47
-
48
-export type InspectedElement = {|
49
- id: number,
50
-
51
- // Does the current renderer support editable hooks?
52
- canEditHooks: boolean,
53
-
54
- // Does the current renderer support editable function props?
55
- canEditFunctionProps: boolean,
56
-
57
- // Can view component source location.
58
- canViewSource: boolean,
59
-
60
- // Inspectable properties.
61
- context: Object | null,
62
- hooks: Object | null,
63
- props: Object | null,
64
- state: Object | null,
65
-
66
- // List of owners
67
- owners: Array<Owner> | null,
68
-
69
- // Location of component in source coude.
70
- source: Object | null,
71
-|};
72
-
73
-// TODO: Add profiling type
74
-
75
-export type DehydratedData = {|
76
- cleaned: Array<Array<string>>,
77
- data: Object,
78
-|};
src/devtools/views/ButtonIcon.js
+35
-11
@@ -3,18 +3,23 @@
3
import React from 'react';
4
import styles from './ButtonIcon.css';
5
6
+export type IconType =
7
+ | 'back'
8
+ | 'close'
9
+ | 'copy'
10
+ | 'down'
11
+ | 'filter'
12
+ | 'more'
13
+ | 'record'
14
+ | 'reload'
15
+ | 'search'
16
+ | 'undo'
17
+ | 'up'
18
+ | 'view-dom'
19
+ | 'view-source';
20
+
21
type Props = {|
7
- type:
8
- | 'back'
9
- | 'close'
10
- | 'copy'
11
- | 'down'
12
- | 'more'
13
- | 'search'
14
- | 'undo'
15
- | 'up'
16
- | 'view-dom'
17
- | 'view-source',
22
+ type: IconType,
23
|};
24
25
export default function ButtonIcon({ type }: Props) {
@@ -32,9 +37,18 @@ export default function ButtonIcon({ type }: Props) {
37
case 'down':
38
pathData = PATH_DOWN;
39
break;
40
+ case 'filter':
41
+ pathData = PATH_FILTER;
42
+ break;
43
case 'more':
44
pathData = PATH_MORE;
45
break;
46
+ case 'record':
47
+ pathData = PATH_RECORD;
48
+ break;
49
+ case 'reload':
50
+ pathData = PATH_RELOAD;
51
+ break;
52
case 'search':
53
pathData = PATH_SEARCH;
54
break;
@@ -85,6 +99,8 @@ const PATH_COPY = `
99
100
const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
101
102
+const PATH_FILTER = 'M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z';
103
+
104
const PATH_MORE = `
105
M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.97.89 1.66.89H22c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9
106
13.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm5 0c-.83
@@ -92,6 +108,14 @@ const PATH_MORE = `
108
1.5.67 1.5 1.5-.67 1.5-1.5 1.5z
109
`;
110
111
+const PATH_RECORD = 'M4,12a8,8 0 1,0 16,0a8,8 0 1,0 -16,0';
112
+
113
+const PATH_RELOAD = `
114
+ M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0
115
+ 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0
116
+ 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z
117
+`;
118
+
119
const PATH_SEARCH = `
120
M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46
121
4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87
src/devtools/views/DevTools.css
+21
@@ -9,9 +9,30 @@
9
10
.TabBar {
11
flex: 0 0 auto;
12
+ display: flex;
13
+ align-items: center;
14
+ padding: 0 0.5rem;
15
+ background-color: var(--color-background);
16
+ border-top: 1px solid var(--color-border);
17
+ font-family: var(--font-family-sans);
18
+ font-size: var(--font-size-sans-large);
19
+}
20
+
21
+.Spacer {
22
+ flex: 1;
23
}
24
25
.TabContent {
26
flex: 1 1 100%;
27
overflow: auto;
28
}
29
+
30
+.DevToolsVersion:before {
31
+ content: 'DevTools ';
32
+}
33
+
34
+@media screen and (max-width: 350px) {
35
+ .DevToolsVersion:before {
36
+ content: '';
37
+ }
38
+}
src/devtools/views/DevTools.js
+59
-2
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { useState } from 'react';
3
+import React, { useEffect, useState } from 'react';
4
import Store from '../store';
5
import { BridgeContext, StoreContext } from './context';
6
import Elements from './Elements/Elements';
@@ -9,6 +9,7 @@ import Settings from './Settings/Settings';
9
import TabBar from './TabBar';
10
import { SettingsContextController } from './Settings/SettingsContext';
11
import { TreeContextController } from './Elements/TreeContext';
12
+import ReactLogo from './ReactLogo';
13
14
import styles from './DevTools.css';
15
@@ -30,6 +31,28 @@ export type Props = {|
31
viewElementSource?: ?Function,
32
|};
33
34
+const elementTab = {
35
+ id: ('elements': TabID),
36
+ icon: 'elements',
37
+ label: 'Elements',
38
+ title: 'React Elements',
39
+};
40
+const profilerTab = {
41
+ id: ('profiler': TabID),
42
+ icon: 'profiler',
43
+ label: 'Profiler',
44
+ title: 'React Profiler',
45
+};
46
+const settingsTab = {
47
+ id: ('settings': TabID),
48
+ icon: 'settings',
49
+ label: 'Settings',
50
+ title: 'React Settings',
51
+};
52
+
53
+const tabsWithProfiler = [elementTab, profilerTab, settingsTab];
54
+const tabsWithoutProfiler = [elementTab, settingsTab];
55
+
56
export default function DevTools({
57
bridge,
58
browserName,
@@ -40,6 +63,27 @@ export default function DevTools({
63
viewElementSource = null,
64
}: Props) {
65
const [tab, setTab] = useState(defaultTab);
66
+ const [supportsProfiling, setSupportsProfiling] = useState(
67
+ store.supportsProfiling
68
+ );
69
+
70
+ // Show/hide the "Profiler" button depending on if profiling is supported.
71
+ useEffect(() => {
72
+ if (supportsProfiling !== store.supportsProfiling) {
73
+ setSupportsProfiling(store.supportsProfiling);
74
+ }
75
+
76
+ const handleRoots = () => {
77
+ if (supportsProfiling !== store.supportsProfiling) {
78
+ setSupportsProfiling(store.supportsProfiling);
79
+ }
80
+ };
81
+
82
+ store.addListener('roots', handleRoots);
83
+ return () => {
84
+ store.removeListener('roots', handleRoots);
85
+ };
86
+ }, [store, supportsProfiling]);
87
88
let tabElement;
89
switch (tab) {
@@ -63,7 +107,20 @@ export default function DevTools({
107
<div className={styles.DevTools}>
108
{showTabBar && (
109
<div className={styles.TabBar}>
66
- <TabBar currentTab={tab} selectTab={setTab} />
110
+ <ReactLogo />
111
+ <span className={styles.DevToolsVersion}>
112
+ {process.env.DEVTOOLS_VERSION}
113
+ </span>
114
+ <div className={styles.Spacer} />
115
+ <TabBar
116
+ currentTab={tab}
117
+ id="DevTools"
118
+ selectTab={setTab}
119
+ size="large"
120
+ tabs={
121
+ supportsProfiling ? tabsWithProfiler : tabsWithoutProfiler
122
+ }
123
+ />
124
</div>
125
)}
126
<div className={styles.TabContent}>{tabElement}</div>
src/devtools/views/Elements/Element.js
+1
-1
@@ -12,7 +12,7 @@ import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
12
import { createRegExp } from '../utils';
13
import { TreeContext } from './TreeContext';
14
15
-import type { Element } from '../../types';
15
+import type { Element } from './types';
16
17
import styles from './Element.css';
18
src/devtools/views/Elements/OwnersStack.js
+1
-1
@@ -6,7 +6,7 @@ import ButtonIcon from '../ButtonIcon';
6
import { TreeContext } from './TreeContext';
7
import { StoreContext } from '../context';
8
9
-import type { Element } from '../../types';
9
+import type { Element } from './types';
10
11
import styles from './OwnersStack.css';
12
src/devtools/views/Elements/SelectedElement.js
+2
-2
@@ -17,8 +17,8 @@ import { hydrate } from 'src/hydration';
17
import styles from './SelectedElement.css';
18
import { ElementTypeClass, ElementTypeFunction } from '../../types';
19
20
-import type { InspectedElement } from '../../types';
21
-import type { DehydratedData, Element } from 'src/devtools/types';
20
+import type { InspectedElement } from './types';
21
+import type { DehydratedData, Element } from './types';
22
23
export type Props = {||};
24
src/devtools/views/Elements/TreeContext.js
+1
-1
@@ -29,7 +29,7 @@ import { createRegExp } from '../utils';
29
import { BridgeContext, StoreContext } from '../context';
30
import Store from '../../store';
31
32
-import type { Element } from 'src/devtools/types';
32
+import type { Element } from './types';
33
34
type Context = {|
35
// Tree
src/devtools/views/Elements/types.js
new
+65
@@ -0,0 +1,65 @@
1
+// @flow
2
+
3
+import type { ElementType } from '../../types';
4
+
5
+// Each element on the frontend corresponds to a Fiber on the backend.
6
+// Some of its information (e.g. id, type, displayName) come from the backend.
7
+// Other bits (e.g. weight and depth) are computed on the frontend for windowing and display purposes.
8
+// Elements are udpated on a push basis– meaning the backend pushes updates to the frontend when needed.
9
+export type Element = {|
10
+ id: number,
11
+ parentID: number,
12
+ children: Array<number>,
13
+ type: ElementType,
14
+ displayName: string | null,
15
+ key: number | string | null,
16
+
17
+ // Owner (if available)
18
+ ownerID: number,
19
+
20
+ // How many levels deep within the tree is this element?
21
+ // This determines how much indentation (left padding) should be used in the Elements tree.
22
+ depth: number,
23
+
24
+ // How many nodes (including itself) are below this Element within the tree.
25
+ // This property is used to quickly determine the total number of Elements,
26
+ // and the Element at any given index (for windowing purposes).
27
+ weight: number,
28
+|};
29
+
30
+export type Owner = {|
31
+ displayName: string,
32
+ id: number,
33
+|};
34
+
35
+export type InspectedElement = {|
36
+ id: number,
37
+
38
+ // Does the current renderer support editable hooks?
39
+ canEditHooks: boolean,
40
+
41
+ // Does the current renderer support editable function props?
42
+ canEditFunctionProps: boolean,
43
+
44
+ // Can view component source location.
45
+ canViewSource: boolean,
46
+
47
+ // Inspectable properties.
48
+ context: Object | null,
49
+ hooks: Object | null,
50
+ props: Object | null,
51
+ state: Object | null,
52
+
53
+ // List of owners
54
+ owners: Array<Owner> | null,
55
+
56
+ // Location of component in source coude.
57
+ source: Object | null,
58
+|};
59
+
60
+// TODO: Add profiling type
61
+
62
+export type DehydratedData = {|
63
+ cleaned: Array<Array<string>>,
64
+ data: Object,
65
+|};
src/devtools/views/Icon.js
+47
-1
@@ -3,9 +3,19 @@
3
import React from 'react';
4
import styles from './Icon.css';
5
6
+export type IconType =
7
+ | 'arrow'
8
+ | 'elements'
9
+ | 'flame-chart'
10
+ | 'interactions'
11
+ | 'profiler'
12
+ | 'ranked-chart'
13
+ | 'search'
14
+ | 'settings';
15
+
16
type Props = {|
17
className?: string,
8
- type: 'arrow' | 'elements' | 'profiler' | 'search' | 'settings',
18
+ type: IconType,
19
|};
20
21
export default function Icon({ className = '', type }: Props) {
@@ -17,9 +27,18 @@ export default function Icon({ className = '', type }: Props) {
27
case 'elements':
28
pathData = PATH_ELEMENTS;
29
break;
30
+ case 'flame-chart':
31
+ pathData = PATH_FLAME_CHART;
32
+ break;
33
+ case 'interactions':
34
+ pathData = PATH_INTERACTIONS;
35
+ break;
36
case 'profiler':
37
pathData = PATH_PROFILER;
38
break;
39
+ case 'ranked-chart':
40
+ pathData = PATH_RANKED_CHART;
41
+ break;
42
case 'search':
43
pathData = PATH_SEARCH;
44
break;
@@ -46,14 +65,41 @@ export default function Icon({ className = '', type }: Props) {
65
}
66
67
const PATH_ARROW = 'M8 5v14l11-7z';
68
+
69
const PATH_ELEMENTS =
70
'M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z';
71
+
72
+const PATH_FLAME_CHART = `
73
+ M10.0650893,21.5040462 C7.14020814,20.6850349 5,18.0558698 5,14.9390244 C5,14.017627
74
+ 5,9.81707317 7.83333333,7.37804878 C7.83333333,7.37804878 7.58333333,11.199187 10,
75
+ 10.6300813 C11.125,10.326087 13.0062497,7.63043487 8.91666667,2.5 C14.1666667,3.06910569
76
+ 19,9.32926829 19,14.9390244 C19,18.0558698 16.8597919,20.6850349 13.9349107,21.5040462
77
+ C14.454014,21.0118505 14.7765152,20.3233394 14.7765152,19.5613412 C14.7765152,17.2826087
78
+ 12,15.0875871 12,15.0875871 C12,15.0875871 9.22348485,17.2826087 9.22348485,19.5613412
79
+ C9.22348485,20.3233394 9.54598603,21.0118505 10.0650893,21.5040462 Z M12.0833333,20.6514763
80
+ C11.3814715,20.6514763 10.8125,20.1226027 10.8125,19.4702042 C10.8125,18.6069669
81
+ 12.0833333,16.9347829 12.0833333,16.9347829 C12.0833333,16.9347829 13.3541667,18.6069669
82
+ 13.3541667,19.4702042 C13.3541667,20.1226027 12.7851952,20.6514763 12.0833333,20.6514763 Z
83
+`;
84
+
85
+const PATH_INTERACTIONS = `
86
+ M23 8c0 1.1-.9 2-2 2-.18 0-.35-.02-.51-.07l-3.56 3.55c.05.16.07.34.07.52 0 1.1-.9 2-2
87
+ 2s-2-.9-2-2c0-.18.02-.36.07-.52l-2.55-2.55c-.16.05-.34.07-.52.07s-.36-.02-.52-.07l-4.55
88
+ 4.56c.05.16.07.33.07.51 0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2c.18 0 .35.02.51.07l4.56-4.55C8.02
89
+ 9.36 8 9.18 8 9c0-1.1.9-2 2-2s2 .9 2 2c0 .18-.02.36-.07.52l2.55
90
+ 2.55c.16-.05.34-.07.52-.07s.36.02.52.07l3.55-3.56C19.02 8.35 19 8.18 19 8c0-1.1.9-2 2-2s2 .9 2 2z
91
+`;
92
+
93
const PATH_PROFILER = 'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z';
94
+
95
const PATH_SEARCH = `
96
M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91
97
16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99
98
5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z
99
`;
100
+
101
+const PATH_RANKED_CHART = 'M3 5h18v3H3zM3 10.5h13v3H3zM3 16h8v3H3z';
102
+
103
const PATH_SETTINGS = `
104
M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12
105
2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39
src/devtools/views/Profiler/Profiler.css
+54
-3
@@ -2,11 +2,62 @@
2
width: 100%;
3
height: 100%;
4
display: flex;
5
- align-items: center;
6
- justify-content: center;
7
- border-top: 1px solid var(--color-border);
5
+ flex-direction: column;
6
font-family: var(--font-family-sans);
7
font-size: var(--font-size-sans-normal);
8
background-color: var(--color-background);
9
color: var(--color-text-color);
10
}
11
+
12
+.Content {
13
+ flex: 1 1 auto;
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ justify-content: center;
18
+}
19
+
20
+.Column {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: center;
25
+}
26
+
27
+.Row {
28
+ display: flex;
29
+ flex-direction: row;
30
+ align-items: center;
31
+ justify-content: center;
32
+}
33
+
34
+.Header {
35
+ font-size: var(--font-size-sans-large);
36
+ margin-bottom: 0.5rem;
37
+}
38
+
39
+.Toolbar {
40
+ padding: 0 0.5rem;
41
+ flex: 0 0 auto;
42
+ display: flex;
43
+ align-items: center;
44
+ border-bottom: 1px solid var(--color-border);
45
+ border-top: 1px solid var(--color-border);
46
+ color: var(--color-button);
47
+}
48
+
49
+.VRule {
50
+ height: 20px;
51
+ width: 1px;
52
+ background-color: var(--color-border);
53
+ margin: 0 0.25rem;
54
+}
55
+
56
+.Spacer {
57
+ flex: 1;
58
+}
59
+
60
+.StopRecordingButton {
61
+ font-size: var(--font-size-sans-large);
62
+ padding: 0.5rem;
63
+}
src/devtools/views/Profiler/Profiler.js
+81
-3
@@ -1,11 +1,89 @@
1
// @flow
2
3
-import React from 'react';
3
+import React, { useContext, useState } from 'react';
4
+import { ProfilerContext, ProfilerContextController } from './ProfilerContext';
5
+import Button from '../Button';
6
+import ButtonIcon from '../ButtonIcon';
7
+import TabBar from '../TabBar';
8
+import RecordToggle from './RecordToggle';
9
10
import styles from './Profiler.css';
11
12
export type Props = {||};
13
9
-export default function Profiler(_: Props) {
10
- return <div className={styles.Profiler}>Under development...</div>;
14
+export default function ProfilerOuter(_: Props) {
15
+ return (
16
+ <ProfilerContextController>
17
+ <ProfilerInner />
18
+ </ProfilerContextController>
19
+ );
20
}
21
+
22
+function ProfilerInner(_: Props) {
23
+ const { hasProfilingData, isProfiling } = useContext(ProfilerContext);
24
+ const [tab, setTab] = useState('flame-chart');
25
+
26
+ let view = null;
27
+ if (isProfiling) {
28
+ view = <RecortdingInProgress />;
29
+ } else if (!hasProfilingData) {
30
+ view = <NoProfilingData />;
31
+ } else {
32
+ // TODO (profiling) Differentiate between no data and no data for the current root
33
+ // TODO (profiling) Show selected "tab" view
34
+ view = <div>Coming soon...</div>;
35
+ }
36
+
37
+ return (
38
+ <div className={styles.Profiler}>
39
+ <div className={styles.Toolbar}>
40
+ <RecordToggle />
41
+ <Button disabled title="Reload and start profiling">
42
+ {/* TODO (profiling) Wire up reload button */}
43
+ <ButtonIcon type="reload" />
44
+ </Button>
45
+ <div className={styles.VRule} />
46
+ <TabBar
47
+ currentTab={tab}
48
+ disabled={isProfiling || !hasProfilingData}
49
+ id="Profiler"
50
+ selectTab={setTab}
51
+ size="small"
52
+ tabs={tabs}
53
+ />
54
+ <div className={styles.Spacer} />
55
+ <Button disabled title="Filter commits by duration">
56
+ {/* TODO (profiling) Wire up filter button */}
57
+ <ButtonIcon type="filter" />
58
+ </Button>
59
+ </div>
60
+ <div className={styles.Content}>{view}</div>
61
+ </div>
62
+ );
63
+}
64
+
65
+const tabs = [
66
+ { id: 'flame-chart', icon: 'flame-chart', label: 'Flamegraph' },
67
+ { id: 'ranked-chart', icon: 'ranked-chart', label: 'Ranked' },
68
+ { id: 'interactions', icon: 'interactions', label: 'Interactions' },
69
+];
70
+
71
+const NoProfilingData = () => (
72
+ <div className={styles.Column}>
73
+ <div className={styles.Header}>No profiling data has been recorded.</div>
74
+ <div className={styles.Row}>
75
+ Click the record button <RecordToggle /> to start recording.
76
+ </div>
77
+ </div>
78
+);
79
+
80
+const RecortdingInProgress = () => {
81
+ return (
82
+ <div className={styles.Column}>
83
+ <div className={styles.Header}>Profiling is in progress...</div>
84
+ <div className={styles.Row}>
85
+ Click the record button <RecordToggle /> to stop recording.
86
+ </div>
87
+ </div>
88
+ );
89
+};
src/devtools/views/Profiler/ProfilerContext.js
new
+105
@@ -0,0 +1,105 @@
1
+// @flow
2
+
3
+import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useReducer,
10
+} from 'react';
11
+import { BridgeContext, StoreContext } from '../context';
12
+import reducer from './reducer';
13
+
14
+import type {
15
+ HANDLE_OPERATIONS_ACTION,
16
+ HANDLE_PROFILING_STATUS_CHANGE_ACTION,
17
+ SEND_START_PROFILING_ACTION,
18
+ SEND_STOP_PROFILING_ACTION,
19
+} from './reducer';
20
+
21
+type Context = {|
22
+ hasProfilingData: boolean,
23
+ isProfiling: boolean,
24
+ startProfiling(value: boolean): void,
25
+ stopProfiling(value: boolean): void,
26
+|};
27
+
28
+const ProfilerContext = createContext<Context>(((null: any): Context));
29
+ProfilerContext.displayName = 'ProfilerContext';
30
+
31
+type Props = {|
32
+ children: React$Node,
33
+|};
34
+
35
+function ProfilerContextController({ children }: Props) {
36
+ const bridge = useContext(BridgeContext);
37
+ const store = useContext(StoreContext);
38
+
39
+ // Some of this reducer's actions require access to the store.
40
+ // The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
41
+ // so we don't need to re-init the reducer in any special way.
42
+ const [state, dispatch] = useReducer(reducer, {
43
+ hasProfilingData: false,
44
+ isProfiling: false,
45
+ _operations: [],
46
+ _snapshot: new Map(),
47
+ });
48
+
49
+ const startProfiling = useCallback(() => {
50
+ bridge.send('startProfiling');
51
+ dispatch(({ type: 'SEND_START_PROFILING' }: SEND_START_PROFILING_ACTION));
52
+ }, [bridge, dispatch]);
53
+ const stopProfiling = useCallback(() => {
54
+ bridge.send('stopProfiling');
55
+ dispatch(({ type: 'SEND_STOP_PROFILING' }: SEND_STOP_PROFILING_ACTION));
56
+ }, [bridge, dispatch]);
57
+
58
+ const value = useMemo(
59
+ () => ({
60
+ hasProfilingData: state.hasProfilingData,
61
+ isProfiling: state.isProfiling,
62
+ startProfiling,
63
+ stopProfiling,
64
+ }),
65
+ [state, startProfiling, stopProfiling]
66
+ );
67
+
68
+ useLayoutEffect(() => {
69
+ const handleOperations = (operations: Uint32Array) =>
70
+ dispatch(
71
+ ({
72
+ type: 'HANDLE_OPERATIONS',
73
+ payload: operations,
74
+ }: HANDLE_OPERATIONS_ACTION)
75
+ );
76
+
77
+ const handleProfilingStatus = (isProfiling: boolean) =>
78
+ dispatch(
79
+ ({
80
+ type: 'HANDLE_PROFILING_STATUS_CHANGE',
81
+ payload: { isProfiling, store },
82
+ }: HANDLE_PROFILING_STATUS_CHANGE_ACTION)
83
+ );
84
+
85
+ bridge.addListener('operations', handleOperations);
86
+ bridge.addListener('profilingStatus', handleProfilingStatus);
87
+
88
+ // It's possible that profiling has already started (e.g. "reload and start profiling")
89
+ // so the frontend needs to ask the backend for its status after mounting.
90
+ bridge.send('getProfilingStatus');
91
+
92
+ return () => {
93
+ bridge.removeListener('operations', handleOperations);
94
+ bridge.removeListener('profilingStatus', handleProfilingStatus);
95
+ };
96
+ }, [bridge, dispatch, store]);
97
+
98
+ return (
99
+ <ProfilerContext.Provider value={value}>
100
+ {children}
101
+ </ProfilerContext.Provider>
102
+ );
103
+}
104
+
105
+export { ProfilerContext, ProfilerContextController };
src/devtools/views/Profiler/RecordToggle.css
new
+21
@@ -0,0 +1,21 @@
1
+.ActiveRecordToggle {
2
+ color: var(--color-record-active);
3
+}
4
+.ActiveRecordToggle:hover,
5
+.ActiveRecordToggle:focus {
6
+ color: var(--color-record-active);
7
+}
8
+.ActiveRecordToggle:active {
9
+ color: var(--color-record-hover);
10
+}
11
+
12
+.InactiveRecordToggle {
13
+ color: var(--color-record-inactive);
14
+}
15
+.InactiveRecordToggle:hover,
16
+.InactiveRecordToggle:focus {
17
+ color: var(--color-record-inactive);
18
+}
19
+.InactiveRecordToggle:active {
20
+ color: var(--color-record-hover);
21
+}
src/devtools/views/Profiler/RecordToggle.js
new
+28
@@ -0,0 +1,28 @@
1
+// @flow
2
+
3
+import React, { useContext } from 'react';
4
+import Button from '../Button';
5
+import ButtonIcon from '../ButtonIcon';
6
+import { ProfilerContext } from './ProfilerContext';
7
+
8
+import styles from './RecordToggle.css';
9
+
10
+export type Props = {||};
11
+
12
+export default function RecordToggle(_: Props) {
13
+ const { isProfiling, startProfiling, stopProfiling } = useContext(
14
+ ProfilerContext
15
+ );
16
+
17
+ return (
18
+ <Button
19
+ className={
20
+ isProfiling ? styles.ActiveRecordToggle : styles.InactiveRecordToggle
21
+ }
22
+ onClick={isProfiling ? stopProfiling : startProfiling}
23
+ title={isProfiling ? 'Stop profiling' : 'Start profiling'}
24
+ >
25
+ <ButtonIcon type="record" />
26
+ </Button>
27
+ );
28
+}
src/devtools/views/Profiler/reducer.js
new
+112
@@ -0,0 +1,112 @@
1
+// @flow
2
+
3
+import Store from '../../store';
4
+
5
+type Node = {|
6
+ id: number,
7
+ children: Array<number>,
8
+ displayName: string | null,
9
+ key: number | string | null,
10
+|};
11
+
12
+export type State = {|
13
+ hasProfilingData: boolean,
14
+ isProfiling: boolean,
15
+
16
+ // List of tree mutation that occur during profiling.
17
+ // Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
18
+ // to reconstruct the state of each root for each commit.
19
+ _operations: Array<Uint32Array>,
20
+
21
+ // Snapshot of the state of the main Store (including all roots) when profiling started.
22
+ // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
23
+ // to reconstruct the state of each root for each commit.
24
+ // It's okay to use a single root to store this information because node IDs are unique across all roots.
25
+ _snapshot: Map<number, Node>,
26
+|};
27
+
28
+export type HANDLE_OPERATIONS_ACTION = {|
29
+ type: 'HANDLE_OPERATIONS',
30
+ payload: Uint32Array,
31
+|};
32
+
33
+export type HANDLE_PROFILING_STATUS_CHANGE_ACTION = {|
34
+ type: 'HANDLE_PROFILING_STATUS_CHANGE',
35
+ payload: {
36
+ isProfiling: boolean,
37
+ store: Store,
38
+ },
39
+|};
40
+
41
+export type SEND_START_PROFILING_ACTION = {|
42
+ type: 'SEND_START_PROFILING',
43
+|};
44
+
45
+export type SEND_STOP_PROFILING_ACTION = {|
46
+ type: 'SEND_STOP_PROFILING',
47
+|};
48
+
49
+type Action =
50
+ | HANDLE_OPERATIONS_ACTION
51
+ | HANDLE_PROFILING_STATUS_CHANGE_ACTION
52
+ | SEND_START_PROFILING_ACTION
53
+ | SEND_STOP_PROFILING_ACTION;
54
+
55
+export default function reducer(state: State, action: Action): State {
56
+ const { type } = action;
57
+ switch (type) {
58
+ case 'HANDLE_OPERATIONS':
59
+ if (state.isProfiling) {
60
+ const operations = ((action: any): HANDLE_OPERATIONS_ACTION).payload;
61
+ return {
62
+ ...state,
63
+ hasProfilingData: true,
64
+ _operations: state._operations.concat(operations),
65
+ };
66
+ } else {
67
+ return state;
68
+ }
69
+ case 'HANDLE_PROFILING_STATUS_CHANGE':
70
+ const {
71
+ isProfiling,
72
+ store,
73
+ } = ((action: any): HANDLE_PROFILING_STATUS_CHANGE_ACTION).payload;
74
+
75
+ if (isProfiling) {
76
+ const snapshot = new Map();
77
+
78
+ const recursiveSnapshot = id => {
79
+ const element = store.getElementByID(id);
80
+ if (element !== null) {
81
+ snapshot.set(id, {
82
+ id,
83
+ children: element.children.slice(0),
84
+ displayName: element.displayName,
85
+ key: element.key,
86
+ });
87
+ element.children.forEach(id => recursiveSnapshot(id));
88
+ }
89
+ };
90
+
91
+ store.roots.forEach(rootID => recursiveSnapshot(rootID));
92
+
93
+ return {
94
+ ...state,
95
+ isProfiling,
96
+ _operations: [],
97
+ _snapshot: snapshot,
98
+ };
99
+ } else {
100
+ return {
101
+ ...state,
102
+ isProfiling,
103
+ };
104
+ }
105
+ case 'SEND_START_PROFILING':
106
+ return { ...state, hasProfilingData: false, isProfiling: true };
107
+ case 'SEND_STOP_PROFILING':
108
+ return { ...state, isProfiling: false };
109
+ default:
110
+ throw new Error(`Unrecognized action "${type}"`);
111
+ }
112
+}
src/devtools/views/Profiler/types.js
new
+29
@@ -0,0 +1,29 @@
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
+
15
+export type Node = {|
16
+ actualDuration: number,
17
+ baseDuration: number,
18
+ displayName: string,
19
+ id: number,
20
+ parentID: number,
21
+ selfDuration: number,
22
+|};
23
+
24
+export type CommitDetails = {|
25
+ duration: number,
26
+ interactions: Array<Interaction>,
27
+ root: Node,
28
+ timestamp: number,
29
+|};
src/devtools/views/ReactLogo.css
+1
@@ -2,4 +2,5 @@
2
width: 1.75rem;
3
height: 1.75rem;
4
margin: 0 0.75rem 0 0.25rem;
5
+ color: var(--color-button);
6
}
src/devtools/views/Settings/SettingsContext.js
+3
@@ -141,6 +141,9 @@ function updateThemeVariables(theme: Theme): void {
141
updateStyleHelper(theme, 'color-dimmest');
142
updateStyleHelper(theme, 'color-jsx-arrow-brackets');
143
updateStyleHelper(theme, 'color-jsx-arrow-brackets-inverted');
144
+ updateStyleHelper(theme, 'color-record-active');
145
+ updateStyleHelper(theme, 'color-record-hover');
146
+ updateStyleHelper(theme, 'color-record-inactive');
147
updateStyleHelper(theme, 'color-tree-node-selected');
148
updateStyleHelper(theme, 'color-tree-node-hover');
149
updateStyleHelper(theme, 'color-search-match');
src/devtools/views/TabBar.css
+34
-36
@@ -1,40 +1,17 @@
1
-.TabBar {
2
- display: flex;
3
- align-items: center;
4
- padding: 0 0.5rem;
5
- background-color: var(--color-background);
6
- color: var(--color-button);
7
- border-top: 1px solid var(--color-border);
8
- font-family: var(--font-family-sans);
9
- font-size: var(--font-size-sans-large);
10
-}
11
-
12
-.DevToolsVersion:before {
13
- content: 'DevTools ';
14
-}
15
-
16
-.Tabs {
17
- flex: 1 1 auto;
18
- display: flex;
19
- flex-direction: row;
20
- align-items: center;
21
- justify-content: flex-end;
22
- overflow-x: auto;
23
-}
24
-
1
+.Tab,
2
.TabCurrent,
26
-.Tab {
3
+.TabDisabled {
4
display: flex;
5
align-items: center;
29
- padding: 0.5rem 1rem;
6
cursor: pointer;
7
border-top: 3px solid transparent;
8
border-bottom: 3px solid transparent;
9
cursor: pointer;
10
user-select: none;
11
+ color: var(--color-text-color);
12
}
36
-.TabCurrent:hover,
37
-.Tab:hover {
13
+.Tab:hover,
14
+.TabCurrent:hover {
15
background-color: var(--color-tree-node-hover);
16
}
17
@@ -42,23 +19,50 @@
19
border-bottom: 3px solid var(--color-tree-node-selected);
20
}
21
22
+.TabDisabled {
23
+ color: var(--color-dim);
24
+ cursor: default;
25
+}
26
+
27
+.TabSizeLarge {
28
+ padding: 0.5rem 1rem;
29
+}
30
+.TabSizeSmall {
31
+ padding: 0.25rem 0.5rem;
32
+}
33
+
34
.Input {
35
width: 0;
36
margin: 0;
37
opacity: 0;
38
}
39
51
-.Icon {
40
+.IconSizeLarge,
41
+.IconSizeSmall {
42
margin-right: 0.5rem;
43
+ color: var(--color-button);
44
+}
45
+
46
+.IconDisabled {
47
+ color: var(--color-dim);
48
+}
49
+
50
+.IconSizeLarge {
51
width: 1.5rem;
52
height: 1.5rem;
53
}
54
55
+.IconSizeSmall {
56
+ width: 1rem;
57
+ height: 1rem;
58
+}
59
+
60
.TabLabel {
61
}
62
63
@media screen and (max-width: 600px) {
61
- .Icon {
64
+ .IconSizeLarge,
65
+ .IconSizeSmall {
66
margin-right: 0;
67
}
68
@@ -66,9 +70,3 @@
70
display: none;
71
}
72
}
69
-
70
-@media screen and (max-width: 350px) {
71
- .DevToolsVersion:before {
72
- content: '';
73
- }
74
-}
src/devtools/views/TabBar.js
+63
-50
@@ -1,77 +1,90 @@
1
// @flow
2
3
-import React, { useCallback } from 'react';
3
+import React, { Fragment, useCallback } from 'react';
4
import Icon from './Icon';
5
-import ReactLogo from './ReactLogo';
5
6
import styles from './TabBar.css';
7
9
-import type { TabID } from './DevTools';
8
+import type { IconType } from './Icon';
9
+
10
+type TabInfo = {|
11
+ icon: IconType,
12
+ id: string,
13
+ label: string,
14
+ title?: string,
15
+|};
16
17
export type Props = {|
12
- currentTab: TabID,
13
- selectTab: (tabID: TabID) => void,
18
+ currentTab: any,
19
+ disabled?: boolean,
20
+ id: string,
21
+ selectTab: (tabID: any) => void,
22
+ size: 'large' | 'small',
23
+ tabs: Array<TabInfo>,
24
|};
25
16
-export default function TabBar({ currentTab, selectTab }: Props) {
26
+export default function TabBar({
27
+ currentTab,
28
+ disabled = false,
29
+ id: groupName,
30
+ selectTab,
31
+ size,
32
+ tabs,
33
+}: Props) {
34
+ if (!tabs.some(tab => tab.id === currentTab)) {
35
+ selectTab(tabs[0].id);
36
+ }
37
+
38
const onChange = useCallback(
39
({ currentTarget }) => selectTab(currentTarget.value),
40
[selectTab]
41
);
42
43
+ const handleKeyDown = useCallback(event => {
44
+ switch (event.key) {
45
+ case 'ArrowDown':
46
+ case 'ArrowLeft':
47
+ case 'ArrowRight':
48
+ case 'ArrowUp':
49
+ event.stopPropagation();
50
+ break;
51
+ default:
52
+ break;
53
+ }
54
+ }, []);
55
+
56
+ const tabClassName =
57
+ size === 'large' ? styles.TabSizeLarge : styles.TabSizeSmall;
58
+
59
return (
23
- <div className={styles.TabBar}>
24
- <ReactLogo />
25
- <span className={styles.DevToolsVersion}>
26
- {process.env.DEVTOOLS_VERSION}
27
- </span>
28
- <div className={styles.Tabs}>
29
- <label
30
- className={currentTab === 'elements' ? styles.TabCurrent : styles.Tab}
31
- title="React Elements"
32
- >
33
- <input
34
- type="radio"
35
- name="TabBar-tab"
36
- className={styles.Input}
37
- checked={currentTab === 'elements'}
38
- value="elements"
39
- onChange={onChange}
40
- />
41
- <Icon className={styles.Icon} type="elements" />
42
- <span className={styles.TabLabel}>Elements</span>
43
- </label>
60
+ <Fragment>
61
+ {tabs.map(({ icon, id, label, title }) => (
62
<label
45
- className={currentTab === 'profiler' ? styles.TabCurrent : styles.Tab}
46
- title="React Profiler"
63
+ className={`${tabClassName} ${
64
+ disabled ? styles.TabDisabled : styles.Tab
65
+ } ${!disabled && currentTab === id ? styles.TabCurrent : ''}`}
66
+ key={id}
67
+ onKeyDown={handleKeyDown}
68
+ title={title || label}
69
>
70
<input
71
type="radio"
50
- name="TabBar-tab"
72
className={styles.Input}
52
- checked={currentTab === 'profiler'}
53
- value="profiler"
73
+ checked={currentTab === id}
74
+ disabled={disabled}
75
+ name={groupName}
76
+ value={id}
77
onChange={onChange}
78
/>
56
- <Icon className={styles.Icon} type="profiler" />
57
- <span className={styles.TabLabel}>Profiler</span>
58
- </label>
59
- <label
60
- className={currentTab === 'settings' ? styles.TabCurrent : styles.Tab}
61
- title="React Settings"
62
- >
63
- <input
64
- type="radio"
65
- name="TabBar-tab"
66
- className={styles.Input}
67
- checked={currentTab === 'settings'}
68
- value="settings"
69
- onChange={onChange}
79
+ <Icon
80
+ className={`${disabled ? styles.IconDisabled : ''} ${
81
+ size === 'large' ? styles.IconSizeLarge : styles.IconSizeSmall
82
+ }`}
83
+ type={icon}
84
/>
71
- <Icon className={styles.Icon} type="settings" />
72
- <span className={styles.TabLabel}>Settings</span>
85
+ <span className={styles.TabLabel}>{label}</span>
86
</label>
74
- </div>
75
- </div>
87
+ ))}
88
+ </Fragment>
89
);
90
}
src/devtools/views/root.css
+6
@@ -23,6 +23,9 @@
23
--light-color-dimmest: #eff0f1;
24
--light-color-jsx-arrow-brackets: #333333;
25
--light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
26
+ --light-color-record-active: #fc3a4b;
27
+ --light-color-record-hover: #000000;
28
+ --light-color-record-inactive: #cfd1d5;
29
--light-color-search-match: yellow;
30
--light-color-search-match-current: #f7923b;
31
--light-color-text-color: #000000;
@@ -49,6 +52,9 @@
52
--dark-color-dimmest: #4f5766;
53
--dark-color-jsx-arrow-brackets: #777d88;
54
--dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
55
+ --dark-color-record-active: #fc3a4b;
56
+ --dark-color-record-hover: #ffffff;
57
+ --dark-color-record-inactive: #777d88;
58
--dark-color-search-match: yellow;
59
--dark-color-search-match-current: #f7923b;
60
--dark-color-text-color: #ffffff;