Plugged react-window into commit selector
Brian Vaughn committed
Mar 13, 2019 at 14:57 UTC
0edf4e9dc16e58881dc3c45180d2d384ca6fc35e
19 files changed
+366
-32
OVERVIEW.md
+9
-7
@@ -203,16 +203,18 @@ Here is an example profile summary:
203
rootID: 1,
204
interactionCount: 2,
205
206
- // Tuples of commit time (relative to when profiling started) and duration
207
- commits: [
208
- 210, // first commit started 210ms after profiling began
209
- 10, // and took 10ms
206
+ // Commit durations
207
+ commitDurations: [
208
+ 10, // first commit took 10ms
209
+ 13, // second commit took 13ms
210
+ 5, // third commit took 5ms
211
+ ]
212
213
+ // Commit times (relative to when profiling started)
214
+ commitTimes: [
215
+ 210, // first commit started 210ms after profiling began
216
284, // second commit started 284ms after profiling began
212
- 13, // and took 13ms
213
-
217
303, // third commit started 303ms after profiling began
215
- 5, // and took 5ms
218
],
219
220
// Tuples of fiber id and initial tree base duration
package.json
+1
-1
@@ -77,7 +77,7 @@
77
"fbjs": "0.5.1",
78
"fbjs-scripts": "0.7.0",
79
"firefox-profile": "^1.0.2",
80
- "flow-bin": "^0.93.0",
80
+ "flow-bin": "^0.94.0",
81
"fs-extra": "^3.0.1",
82
"gh-pages": "^1.0.0",
83
"immutable": "3.7.6",
src/backend/renderer.js
+6
-3
@@ -1381,14 +1381,16 @@ export function attach(
1381
1382
function getProfilingSummary(rootID: number): ProfilingSummary {
1383
const interactions = new Set();
1384
- const commits = [];
1384
+ const commitDurations = [];
1385
+ const commitTimes = [];
1386
1387
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
1388
rootID
1389
);
1390
if (commitProfilingMetadata != null) {
1391
commitProfilingMetadata.forEach(metadata => {
1391
- commits.push(metadata.commitTime, metadata.maxActualDuration);
1392
+ commitDurations.push(metadata.maxActualDuration);
1393
+ commitTimes.push(metadata.commitTime);
1394
metadata.interactions.forEach(({ name, timestamp }) => {
1395
interactions.add(`${timestamp}:${name}`);
1396
});
@@ -1405,7 +1407,8 @@ export function attach(
1407
);
1408
1409
return {
1408
- commits,
1410
+ commitDurations,
1411
+ commitTimes,
1412
initialTreeBaseDurations,
1413
interactionCount: interactions.size,
1414
rootID,
src/backend/types.js
+2
-1
@@ -55,7 +55,8 @@ export type ReactRenderer = {
55
};
56
57
export type ProfilingSummary = {|
58
- commits: Array<number>,
58
+ commitDurations: Array<number>,
59
+ commitTimes: Array<number>,
60
initialTreeBaseDurations: Array<number>,
61
interactionCount: number,
62
rootID: number,
src/devtools/ProfilingCache.js
+10
-9
@@ -8,7 +8,7 @@ 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';
10
11
-type AAA = {|
11
+type RendererAndRootID = {|
12
rootID: number,
13
rendererID: number,
14
|};
@@ -19,18 +19,17 @@ export default class ProfilingCache {
19
(profilingSummary: ProfilingSummaryFrontend) => void
20
> = new Map();
21
22
- ProfilingSummary: Resource<AAA, ProfilingSummaryFrontend>;
23
-
24
- // TODO (profiling) renderer + root
22
+ ProfilingSummary: Resource<RendererAndRootID, ProfilingSummaryFrontend>;
23
24
constructor(bridge: Bridge, store: Store) {
25
this.ProfilingSummary = createResource(
28
- ({ rendererID, rootID }: AAA) => {
26
+ ({ rendererID, rootID }: RendererAndRootID) => {
27
return new Promise(resolve => {
28
if (!store._profilingOperations.has(rootID)) {
29
// If no profiling data was recorded for this root, skip the round trip.
30
resolve({
33
- commits: [],
31
+ commitDurations: [],
32
+ commitTimes: [],
33
initialTreeBaseDurations: new Map(),
34
interactionCount: 0,
35
});
@@ -40,7 +39,7 @@ export default class ProfilingCache {
39
}
40
});
41
},
43
- ({ rendererID, rootID }: AAA) => rootID
42
+ ({ rendererID, rootID }: RendererAndRootID) => rootID
43
);
44
45
bridge.addListener('profilingSummary', this.onProfileSummary);
@@ -52,7 +51,8 @@ export default class ProfilingCache {
51
}
52
53
onProfileSummary = ({
55
- commits,
54
+ commitDurations,
55
+ commitTimes,
56
initialTreeBaseDurations,
57
interactionCount,
58
rootID,
@@ -68,7 +68,8 @@ export default class ProfilingCache {
68
}
69
70
resolve({
71
- commits,
71
+ commitDurations,
72
+ commitTimes,
73
initialTreeBaseDurations: initialTreeBaseDurationsMap,
74
interactionCount,
75
});
src/devtools/store.js
+3
@@ -275,6 +275,9 @@ export default class Store extends EventEmitter {
275
}
276
277
startProfiling(): void {
278
+ // Invalidate suspense cache if profiling data is being (re-)recorded.
279
+ this._profilingCache.invalidate();
280
+
281
this._bridge.send('startProfiling');
282
this._isProfiling = false;
283
this.emit('isProfiling');
src/devtools/views/Profiler/Profiler.js
+1
-1
@@ -69,7 +69,7 @@ function ProfilerInner(_: Props) {
69
</div>
70
<div className={styles.Content}>
71
{view}
72
- {isFilterModalShowing && ( // TODO (profiler) Position when snapshot graph is open
72
+ {isFilterModalShowing && (
73
<FilterModal dismissModal={dismissFilterModal} />
74
)}
75
</div>
src/devtools/views/Profiler/ProfilerContext.js
+6
@@ -88,6 +88,12 @@ function ProfilerContextController({ children }: Props) {
88
setCommitIndex(0);
89
}
90
91
+ const [prevIsProfiling, setPrevIsProfiling] = useState(isProfiling);
92
+ if (prevIsProfiling !== isProfiling) {
93
+ setPrevIsProfiling(isProfiling);
94
+ setCommitIndex(0);
95
+ }
96
+
97
const value = useMemo(
98
() => ({
99
commitIndex,
src/devtools/views/Profiler/SnapshotCommitList.js
new
+145
@@ -0,0 +1,145 @@
1
+// @flow
2
+
3
+import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+} from 'react';
10
+import AutoSizer from 'react-virtualized-auto-sizer';
11
+import { FixedSizeList } from 'react-window';
12
+import SnapshotCommitListItem from './SnapshotCommitListItem';
13
+import { minBarWidth } from './constants';
14
+
15
+import type { ProfilingSummary } from './types';
16
+
17
+export type ItemData = {|
18
+ commitDurations: Array<number>,
19
+ commitTimes: Array<number>,
20
+ isMouseDown: boolean,
21
+ maxDuration: number,
22
+ selectedCommitIndex: number,
23
+ setCommitIndex: (index: number) => void,
24
+|};
25
+
26
+type Props = {|
27
+ profilingSummary: ProfilingSummary,
28
+ selectedCommitIndex: number,
29
+ setCommitIndex: (index: number) => void,
30
+ viewNextCommit: () => void,
31
+ viewPrevCommit: () => void,
32
+|};
33
+
34
+export default function SnapshotCommitList(props: Props) {
35
+ return (
36
+ <AutoSizer>
37
+ {({ height, width }) => <List height={height} width={width} {...props} />}
38
+ </AutoSizer>
39
+ );
40
+}
41
+
42
+type ListProps = {|
43
+ height: number,
44
+ profilingSummary: ProfilingSummary,
45
+ selectedCommitIndex: number,
46
+ setCommitIndex: (index: number) => void,
47
+ viewNextCommit: () => void,
48
+ viewPrevCommit: () => void,
49
+ width: number,
50
+|};
51
+
52
+function List({
53
+ height,
54
+ profilingSummary,
55
+ selectedCommitIndex,
56
+ setCommitIndex,
57
+ viewNextCommit,
58
+ viewPrevCommit,
59
+ width,
60
+}: ListProps) {
61
+ const listRef = useRef<FixedSizeList<ItemData> | null>(null);
62
+ const [isMouseDown, setIsMouseDown] = useState(false);
63
+ const prevSelectedCommitIndexRef = useRef<number>(-1);
64
+
65
+ // Make sure any newly selected snapshot is visible within the list.
66
+ useEffect(() => {
67
+ if (selectedCommitIndex !== prevSelectedCommitIndexRef.current) {
68
+ prevSelectedCommitIndexRef.current = selectedCommitIndex;
69
+ if (listRef.current !== null) {
70
+ listRef.current.scrollToItem(selectedCommitIndex);
71
+ }
72
+ }
73
+ }, [listRef, selectedCommitIndex]);
74
+
75
+ const handleMouseDown = useCallback(() => {
76
+ setIsMouseDown(true);
77
+ }, []);
78
+ const handleMouseUp = useCallback(() => {
79
+ setIsMouseDown(false);
80
+ }, []);
81
+
82
+ useEffect(() => {
83
+ window.addEventListener('mouseup', handleMouseUp);
84
+ return () => {
85
+ window.removeEventListener('mouseup', handleMouseUp);
86
+ };
87
+ }, [handleMouseUp]);
88
+
89
+ const { commitDurations, commitTimes } = profilingSummary;
90
+
91
+ const itemSize = useMemo(
92
+ () => Math.max(minBarWidth, width / commitDurations.length),
93
+ [commitDurations, width]
94
+ );
95
+ const maxDuration = useMemo(
96
+ () =>
97
+ commitDurations.reduce(
98
+ (maxDuration, duration) => Math.max(maxDuration, duration),
99
+ 0
100
+ ),
101
+ [commitDurations]
102
+ );
103
+
104
+ // Pass required contextual data down to the ListItem renderer.
105
+ const itemData = useMemo<ItemData>(
106
+ () => ({
107
+ commitDurations,
108
+ commitTimes,
109
+ isMouseDown,
110
+ maxDuration,
111
+ selectedCommitIndex,
112
+ setCommitIndex,
113
+ }),
114
+ [
115
+ commitDurations,
116
+ commitTimes,
117
+ isMouseDown,
118
+ maxDuration,
119
+ selectedCommitIndex,
120
+ setCommitIndex,
121
+ ]
122
+ );
123
+
124
+ return (
125
+ <div
126
+ onMouseDown={handleMouseDown}
127
+ onMouseUp={handleMouseUp}
128
+ style={{ height, width }}
129
+ >
130
+ {commitDurations.length > 0 && (
131
+ <FixedSizeList
132
+ direction="horizontal"
133
+ height={height}
134
+ itemCount={commitDurations.length}
135
+ itemData={itemData}
136
+ itemSize={itemSize}
137
+ ref={(listRef: any) /* Flow bug? */}
138
+ width={width}
139
+ >
140
+ {SnapshotCommitListItem}
141
+ </FixedSizeList>
142
+ )}
143
+ </div>
144
+ );
145
+}
src/devtools/views/Profiler/SnapshotCommitListItem.js
new
+77
@@ -0,0 +1,77 @@
1
+// @flow
2
+
3
+import React, { memo, useCallback } from 'react';
4
+import { areEqual } from 'react-window';
5
+import { getGradientColor, formatDuration, formatTime } from './utils';
6
+
7
+import type { ItemData } from './SnapshotCommitList';
8
+
9
+type Props = {
10
+ data: ItemData,
11
+ index: number,
12
+ style: Object,
13
+};
14
+
15
+function SnapshotCommitListItem({ data: itemData, index, style }: Props) {
16
+ const {
17
+ commitDurations,
18
+ commitTimes,
19
+ isMouseDown,
20
+ maxDuration,
21
+ selectedCommitIndex,
22
+ setCommitIndex,
23
+ } = itemData;
24
+
25
+ const commitDuration = commitDurations[index];
26
+ const commitTime = commitTimes[index];
27
+
28
+ const handleClick = useCallback(() => setCommitIndex(index), [
29
+ index,
30
+ setCommitIndex,
31
+ ]);
32
+
33
+ // Guard against commits with duration 0
34
+ const percentage =
35
+ Math.min(1, Math.max(0, commitDuration / maxDuration)) || 0;
36
+ const isSelected = selectedCommitIndex === index;
37
+
38
+ // Leave a 1px gap between snapshots
39
+ const width = parseFloat(style.width) - 1;
40
+
41
+ return (
42
+ <div
43
+ onClick={handleClick}
44
+ onMouseEnter={isMouseDown ? handleClick : null}
45
+ style={{
46
+ ...style,
47
+ width,
48
+ userSelect: 'none',
49
+ cursor: 'pointer',
50
+ borderBottom: isSelected
51
+ ? '3px solid var(--color-tree-node-selected)'
52
+ : '3px solid transparent',
53
+ paddingTop: 4,
54
+ paddingBottom: 1,
55
+ display: 'flex',
56
+ alignItems: 'flex-end',
57
+ }}
58
+ title={`Duration ${formatDuration(commitDuration)}ms at ${formatTime(
59
+ commitTime
60
+ )}s`}
61
+ >
62
+ <div
63
+ style={{
64
+ width,
65
+ height: `${Math.round(percentage * 100)}%`,
66
+ minHeight: 5,
67
+ backgroundColor:
68
+ percentage === 0
69
+ ? 'var(--color-commit-did-not-render)'
70
+ : getGradientColor(percentage),
71
+ }}
72
+ />
73
+ </div>
74
+ );
75
+}
76
+
77
+export default memo<Props>(SnapshotCommitListItem, areEqual);
src/devtools/views/Profiler/SnapshotSelector.css
+8
@@ -1,5 +1,7 @@
1
.SnapshotSelector {
2
+ flex: 1 1 auto;
3
display: flex;
4
+ flex-direction: row;
5
align-items: center;
6
color: var(--color-text-color);
7
margin-left: 0.5rem;
@@ -10,6 +12,8 @@
12
}
13
14
.Commits {
15
+ flex: 1 1 150px;
16
+ height: 2.25rem;
17
margin-left: 0.25rem;
18
}
19
@@ -19,3 +23,7 @@
23
background-color: var(--color-border);
24
margin: 0 0.25rem;
25
}
26
+
27
+.Number {
28
+ font-family: var(--font-family-monospace);
29
+}
src/devtools/views/Profiler/SnapshotSelector.js
+13
-3
@@ -5,6 +5,7 @@ import Button from '../Button';
5
import ButtonIcon from '../ButtonIcon';
6
import { StoreContext } from '../context';
7
import { ProfilerContext } from './ProfilerContext';
8
+import SnapshotCommitList from './SnapshotCommitList';
9
10
import styles from './SnapshotSelector.css';
11
@@ -34,7 +35,7 @@ function SnapshotSelector(_: Props) {
35
rootID: ((rootID: any): number),
36
});
37
37
- const numCommits = profilingSummary.commits.length / 2;
38
+ const numCommits = profilingSummary.commitDurations.length;
39
40
if (numCommits === 0) {
41
return null;
@@ -51,7 +52,10 @@ function SnapshotSelector(_: Props) {
52
<Fragment>
53
<div className={styles.VRule} />
54
<div className={styles.SnapshotSelector}>
54
- {commitIndex + 1} / {numCommits}
55
+ <span className={styles.Number}>
56
+ {`${commitIndex + 1}`.padStart(`${numCommits}`.length, '0')} /{' '}
57
+ {numCommits}
58
+ </span>
59
<Button
60
className={styles.Button}
61
disabled={commitIndex <= 0}
@@ -60,7 +64,13 @@ function SnapshotSelector(_: Props) {
64
<ButtonIcon type="previous" />
65
</Button>
66
<div className={styles.Commits}>
63
- [] {/* TODO (profiling) Add FixedSizeList selector */}
67
+ <SnapshotCommitList
68
+ profilingSummary={profilingSummary}
69
+ selectedCommitIndex={commitIndex}
70
+ setCommitIndex={setCommitIndex}
71
+ viewNextCommit={viewNextCommit}
72
+ viewPrevCommit={viewPrevCommit}
73
+ />
74
</div>
75
<Button
76
className={styles.Button}
src/devtools/views/Profiler/constants.js
new
+8
@@ -0,0 +1,8 @@
1
+// @flow
2
+
3
+export const barHeight = 20;
4
+export const barWidth = 100;
5
+export const barWidthThreshold = 2;
6
+export const minBarHeight = 5;
7
+export const minBarWidth = 5;
8
+export const textHeight = 18;
src/devtools/views/Profiler/types.js
+5
-2
@@ -29,8 +29,11 @@ export type CommitDetails = {|
29
|};
30
31
export type ProfilingSummary = {|
32
- // Tuples of commit time (relative to when profiling started) and duration
33
- commits: Array<number>,
32
+ // Commit durations
33
+ commitDurations: Array<number>,
34
+
35
+ // Commit times (relative to when profiling started)
36
+ commitTimes: Array<number>,
37
38
// Map of fiber id to (initial) tree base duration
39
initialTreeBaseDurations: Map<number, number>,
src/devtools/views/Profiler/utils.js
new
+34
@@ -0,0 +1,34 @@
1
+// @flow
2
+
3
+const commitGradient = [
4
+ 'var(--color-commit-gradient-0)',
5
+ 'var(--color-commit-gradient-1)',
6
+ 'var(--color-commit-gradient-2)',
7
+ 'var(--color-commit-gradient-3)',
8
+ 'var(--color-commit-gradient-4)',
9
+ 'var(--color-commit-gradient-5)',
10
+ 'var(--color-commit-gradient-6)',
11
+ 'var(--color-commit-gradient-7)',
12
+ 'var(--color-commit-gradient-8)',
13
+ 'var(--color-commit-gradient-9)',
14
+];
15
+
16
+export const getGradientColor = (value: number) => {
17
+ const maxIndex = commitGradient.length - 1;
18
+ let index;
19
+ if (Number.isNaN(value)) {
20
+ index = 0;
21
+ } else if (!Number.isFinite(value)) {
22
+ index = maxIndex;
23
+ } else {
24
+ index = Math.max(0, Math.min(maxIndex, value)) * maxIndex;
25
+ }
26
+ return commitGradient[Math.round(index)];
27
+};
28
+
29
+export const formatDuration = (duration: number) =>
30
+ Math.round(duration * 10) / 10;
31
+export const formatPercentage = (percentage: number) =>
32
+ Math.round(percentage * 100);
33
+export const formatTime = (timestamp: number) =>
34
+ Math.round(Math.round(timestamp) / 100) / 10;
src/devtools/views/Settings/SettingsContext.js
+11
@@ -134,6 +134,17 @@ function updateThemeVariables(theme: Theme): void {
134
updateStyleHelper(theme, 'color-button-disabled');
135
updateStyleHelper(theme, 'color-button-focus');
136
updateStyleHelper(theme, 'color-button-hover');
137
+ updateStyleHelper(theme, 'color-commit-did-not-render');
138
+ updateStyleHelper(theme, 'color-commit-gradient-0');
139
+ updateStyleHelper(theme, 'color-commit-gradient-1');
140
+ updateStyleHelper(theme, 'color-commit-gradient-2');
141
+ updateStyleHelper(theme, 'color-commit-gradient-3');
142
+ updateStyleHelper(theme, 'color-commit-gradient-4');
143
+ updateStyleHelper(theme, 'color-commit-gradient-5');
144
+ updateStyleHelper(theme, 'color-commit-gradient-6');
145
+ updateStyleHelper(theme, 'color-commit-gradient-7');
146
+ updateStyleHelper(theme, 'color-commit-gradient-8');
147
+ updateStyleHelper(theme, 'color-commit-gradient-9');
148
updateStyleHelper(theme, 'color-component-name');
149
updateStyleHelper(theme, 'color-component-name-inverted');
150
updateStyleHelper(theme, 'color-dim');
src/devtools/views/hooks.js
+1
-1
@@ -54,7 +54,7 @@ export function useLocalStorage<T>(
54
}
55
56
export function useModalDismissSignal(
57
- modalRef: React$Ref<any>,
57
+ modalRef: { current: HTMLDivElement | null },
58
dismissCallback: Function
59
): void {
60
useEffect(() => {
src/devtools/views/root.css
+22
@@ -16,6 +16,17 @@
16
--light-color-button-focus: #3578e5;
17
--light-color-button-hover: #3578e5;
18
--light-color-border: #eeeeee;
19
+ --light-color-commit-did-not-render: #777d88;
20
+ --light-color-commit-gradient-0: #37afa9;
21
+ --light-color-commit-gradient-1: #63b19e;
22
+ --light-color-commit-gradient-2: #80b393;
23
+ --light-color-commit-gradient-3: #97b488;
24
+ --light-color-commit-gradient-4: #abb67d;
25
+ --light-color-commit-gradient-5: #beb771;
26
+ --light-color-commit-gradient-6: #cfb965;
27
+ --light-color-commit-gradient-7: #dfba57;
28
+ --light-color-commit-gradient-8: #efbb49;
29
+ --light-color-commit-gradient-9: #febc38;
30
--light-color-component-name: #8155cb;
31
--light-color-component-name-inverted: #ffffff;
32
--light-color-dim: #777d88;
@@ -46,6 +57,17 @@
57
--dark-color-button-focus: #a2e9fc;
58
--dark-color-button-hover: #a2e9fc;
59
--dark-color-border: #3d424a;
60
+ --dark-color-commit-did-not-render: #8f949d;
61
+ --dark-color-commit-gradient-0: #37afa9;
62
+ --dark-color-commit-gradient-1: #63b19e;
63
+ --dark-color-commit-gradient-2: #80b393;
64
+ --dark-color-commit-gradient-3: #97b488;
65
+ --dark-color-commit-gradient-4: #abb67d;
66
+ --dark-color-commit-gradient-5: #beb771;
67
+ --dark-color-commit-gradient-6: #cfb965;
68
+ --dark-color-commit-gradient-7: #dfba57;
69
+ --dark-color-commit-gradient-8: #efbb49;
70
+ --dark-color-commit-gradient-9: #febc38;
71
--dark-color-component-name: #61dafb;
72
--dark-color-component-name-inverted: ##282828;
73
--dark-color-dim: #8f949d;
yarn.lock
+4
-4
@@ -4671,10 +4671,10 @@ flatstr@^1.0.4:
4671
version "1.0.5"
4672
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3"
4673
4674
-flow-bin@^0.93.0:
4675
- version "0.93.0"
4676
- resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.93.0.tgz#9192a08d88db2a8da0ff55e42420f44539791430"
4677
- integrity sha512-p8yq4ocOlpyJgOEBEj0v0GzCP25c9WP0ilFQ8hXSbrTR7RPKuR+Whr+OitlVyp8ocdX0j1MrIwQ8x28dacy1pg==
4674
+flow-bin@^0.94.0:
4675
+ version "0.94.0"
4676
+ resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.94.0.tgz#b5d58fe7559705b73a18229f97edfc3ab6ffffcb"
4677
+ integrity sha512-DYF7r9CJ/AksfmmB4+q+TyLMoeQPRnqtF1Pk7KY3zgfkB/nVuA3nXyzqgsIPIvnMSiFEXQcFK4z+iPxSLckZhQ==
4678
4679
flush-write-stream@^1.0.0:
4680
version "1.1.1"