@samitouri / QOS-React / commits / 0edf4e9dc1

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"