Plugged react-window into commit selector

This commit is contained in:
Brian Vaughn
2019-03-13 14:57:52 -07:00
parent b8a52078c6
commit 0edf4e9dc1
19 changed files with 366 additions and 32 deletions
+9 -7
View File
@@ -203,16 +203,18 @@ Here is an example profile summary:
rootID: 1,
interactionCount: 2,
// Tuples of commit time (relative to when profiling started) and duration
commits: [
// Commit durations
commitDurations: [
10, // first commit took 10ms
13, // second commit took 13ms
5, // third commit took 5ms
]
// Commit times (relative to when profiling started)
commitTimes: [
210, // first commit started 210ms after profiling began
10, // and took 10ms
284, // second commit started 284ms after profiling began
13, // and took 13ms
303, // third commit started 303ms after profiling began
5, // and took 5ms
],
// Tuples of fiber id and initial tree base duration
+1 -1
View File
@@ -77,7 +77,7 @@
"fbjs": "0.5.1",
"fbjs-scripts": "0.7.0",
"firefox-profile": "^1.0.2",
"flow-bin": "^0.93.0",
"flow-bin": "^0.94.0",
"fs-extra": "^3.0.1",
"gh-pages": "^1.0.0",
"immutable": "3.7.6",
+6 -3
View File
@@ -1381,14 +1381,16 @@ export function attach(
function getProfilingSummary(rootID: number): ProfilingSummary {
const interactions = new Set();
const commits = [];
const commitDurations = [];
const commitTimes = [];
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
rootID
);
if (commitProfilingMetadata != null) {
commitProfilingMetadata.forEach(metadata => {
commits.push(metadata.commitTime, metadata.maxActualDuration);
commitDurations.push(metadata.maxActualDuration);
commitTimes.push(metadata.commitTime);
metadata.interactions.forEach(({ name, timestamp }) => {
interactions.add(`${timestamp}:${name}`);
});
@@ -1405,7 +1407,8 @@ export function attach(
);
return {
commits,
commitDurations,
commitTimes,
initialTreeBaseDurations,
interactionCount: interactions.size,
rootID,
+2 -1
View File
@@ -55,7 +55,8 @@ export type ReactRenderer = {
};
export type ProfilingSummary = {|
commits: Array<number>,
commitDurations: Array<number>,
commitTimes: Array<number>,
initialTreeBaseDurations: Array<number>,
interactionCount: number,
rootID: number,
+10 -9
View File
@@ -8,7 +8,7 @@ import type { Bridge } from '../types';
import type { ProfilingSummary as ProfilingSummaryBackend } from 'src/backend/types';
import type { ProfilingSummary as ProfilingSummaryFrontend } from 'src/devtools/views/Profiler/types';
type AAA = {|
type RendererAndRootID = {|
rootID: number,
rendererID: number,
|};
@@ -19,18 +19,17 @@ export default class ProfilingCache {
(profilingSummary: ProfilingSummaryFrontend) => void
> = new Map();
ProfilingSummary: Resource<AAA, ProfilingSummaryFrontend>;
// TODO (profiling) renderer + root
ProfilingSummary: Resource<RendererAndRootID, ProfilingSummaryFrontend>;
constructor(bridge: Bridge, store: Store) {
this.ProfilingSummary = createResource(
({ rendererID, rootID }: AAA) => {
({ rendererID, rootID }: RendererAndRootID) => {
return new Promise(resolve => {
if (!store._profilingOperations.has(rootID)) {
// If no profiling data was recorded for this root, skip the round trip.
resolve({
commits: [],
commitDurations: [],
commitTimes: [],
initialTreeBaseDurations: new Map(),
interactionCount: 0,
});
@@ -40,7 +39,7 @@ export default class ProfilingCache {
}
});
},
({ rendererID, rootID }: AAA) => rootID
({ rendererID, rootID }: RendererAndRootID) => rootID
);
bridge.addListener('profilingSummary', this.onProfileSummary);
@@ -52,7 +51,8 @@ export default class ProfilingCache {
}
onProfileSummary = ({
commits,
commitDurations,
commitTimes,
initialTreeBaseDurations,
interactionCount,
rootID,
@@ -68,7 +68,8 @@ export default class ProfilingCache {
}
resolve({
commits,
commitDurations,
commitTimes,
initialTreeBaseDurations: initialTreeBaseDurationsMap,
interactionCount,
});
+3
View File
@@ -275,6 +275,9 @@ export default class Store extends EventEmitter {
}
startProfiling(): void {
// Invalidate suspense cache if profiling data is being (re-)recorded.
this._profilingCache.invalidate();
this._bridge.send('startProfiling');
this._isProfiling = false;
this.emit('isProfiling');
+1 -1
View File
@@ -69,7 +69,7 @@ function ProfilerInner(_: Props) {
</div>
<div className={styles.Content}>
{view}
{isFilterModalShowing && ( // TODO (profiler) Position when snapshot graph is open
{isFilterModalShowing && (
<FilterModal dismissModal={dismissFilterModal} />
)}
</div>
@@ -88,6 +88,12 @@ function ProfilerContextController({ children }: Props) {
setCommitIndex(0);
}
const [prevIsProfiling, setPrevIsProfiling] = useState(isProfiling);
if (prevIsProfiling !== isProfiling) {
setPrevIsProfiling(isProfiling);
setCommitIndex(0);
}
const value = useMemo(
() => ({
commitIndex,
@@ -0,0 +1,145 @@
// @flow
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import SnapshotCommitListItem from './SnapshotCommitListItem';
import { minBarWidth } from './constants';
import type { ProfilingSummary } from './types';
export type ItemData = {|
commitDurations: Array<number>,
commitTimes: Array<number>,
isMouseDown: boolean,
maxDuration: number,
selectedCommitIndex: number,
setCommitIndex: (index: number) => void,
|};
type Props = {|
profilingSummary: ProfilingSummary,
selectedCommitIndex: number,
setCommitIndex: (index: number) => void,
viewNextCommit: () => void,
viewPrevCommit: () => void,
|};
export default function SnapshotCommitList(props: Props) {
return (
<AutoSizer>
{({ height, width }) => <List height={height} width={width} {...props} />}
</AutoSizer>
);
}
type ListProps = {|
height: number,
profilingSummary: ProfilingSummary,
selectedCommitIndex: number,
setCommitIndex: (index: number) => void,
viewNextCommit: () => void,
viewPrevCommit: () => void,
width: number,
|};
function List({
height,
profilingSummary,
selectedCommitIndex,
setCommitIndex,
viewNextCommit,
viewPrevCommit,
width,
}: ListProps) {
const listRef = useRef<FixedSizeList<ItemData> | null>(null);
const [isMouseDown, setIsMouseDown] = useState(false);
const prevSelectedCommitIndexRef = useRef<number>(-1);
// Make sure any newly selected snapshot is visible within the list.
useEffect(() => {
if (selectedCommitIndex !== prevSelectedCommitIndexRef.current) {
prevSelectedCommitIndexRef.current = selectedCommitIndex;
if (listRef.current !== null) {
listRef.current.scrollToItem(selectedCommitIndex);
}
}
}, [listRef, selectedCommitIndex]);
const handleMouseDown = useCallback(() => {
setIsMouseDown(true);
}, []);
const handleMouseUp = useCallback(() => {
setIsMouseDown(false);
}, []);
useEffect(() => {
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mouseup', handleMouseUp);
};
}, [handleMouseUp]);
const { commitDurations, commitTimes } = profilingSummary;
const itemSize = useMemo(
() => Math.max(minBarWidth, width / commitDurations.length),
[commitDurations, width]
);
const maxDuration = useMemo(
() =>
commitDurations.reduce(
(maxDuration, duration) => Math.max(maxDuration, duration),
0
),
[commitDurations]
);
// Pass required contextual data down to the ListItem renderer.
const itemData = useMemo<ItemData>(
() => ({
commitDurations,
commitTimes,
isMouseDown,
maxDuration,
selectedCommitIndex,
setCommitIndex,
}),
[
commitDurations,
commitTimes,
isMouseDown,
maxDuration,
selectedCommitIndex,
setCommitIndex,
]
);
return (
<div
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
style={{ height, width }}
>
{commitDurations.length > 0 && (
<FixedSizeList
direction="horizontal"
height={height}
itemCount={commitDurations.length}
itemData={itemData}
itemSize={itemSize}
ref={(listRef: any) /* Flow bug? */}
width={width}
>
{SnapshotCommitListItem}
</FixedSizeList>
)}
</div>
);
}
@@ -0,0 +1,77 @@
// @flow
import React, { memo, useCallback } from 'react';
import { areEqual } from 'react-window';
import { getGradientColor, formatDuration, formatTime } from './utils';
import type { ItemData } from './SnapshotCommitList';
type Props = {
data: ItemData,
index: number,
style: Object,
};
function SnapshotCommitListItem({ data: itemData, index, style }: Props) {
const {
commitDurations,
commitTimes,
isMouseDown,
maxDuration,
selectedCommitIndex,
setCommitIndex,
} = itemData;
const commitDuration = commitDurations[index];
const commitTime = commitTimes[index];
const handleClick = useCallback(() => setCommitIndex(index), [
index,
setCommitIndex,
]);
// Guard against commits with duration 0
const percentage =
Math.min(1, Math.max(0, commitDuration / maxDuration)) || 0;
const isSelected = selectedCommitIndex === index;
// Leave a 1px gap between snapshots
const width = parseFloat(style.width) - 1;
return (
<div
onClick={handleClick}
onMouseEnter={isMouseDown ? handleClick : null}
style={{
...style,
width,
userSelect: 'none',
cursor: 'pointer',
borderBottom: isSelected
? '3px solid var(--color-tree-node-selected)'
: '3px solid transparent',
paddingTop: 4,
paddingBottom: 1,
display: 'flex',
alignItems: 'flex-end',
}}
title={`Duration ${formatDuration(commitDuration)}ms at ${formatTime(
commitTime
)}s`}
>
<div
style={{
width,
height: `${Math.round(percentage * 100)}%`,
minHeight: 5,
backgroundColor:
percentage === 0
? 'var(--color-commit-did-not-render)'
: getGradientColor(percentage),
}}
/>
</div>
);
}
export default memo<Props>(SnapshotCommitListItem, areEqual);
@@ -1,5 +1,7 @@
.SnapshotSelector {
flex: 1 1 auto;
display: flex;
flex-direction: row;
align-items: center;
color: var(--color-text-color);
margin-left: 0.5rem;
@@ -10,6 +12,8 @@
}
.Commits {
flex: 1 1 150px;
height: 2.25rem;
margin-left: 0.25rem;
}
@@ -19,3 +23,7 @@
background-color: var(--color-border);
margin: 0 0.25rem;
}
.Number {
font-family: var(--font-family-monospace);
}
@@ -5,6 +5,7 @@ import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import { StoreContext } from '../context';
import { ProfilerContext } from './ProfilerContext';
import SnapshotCommitList from './SnapshotCommitList';
import styles from './SnapshotSelector.css';
@@ -34,7 +35,7 @@ function SnapshotSelector(_: Props) {
rootID: ((rootID: any): number),
});
const numCommits = profilingSummary.commits.length / 2;
const numCommits = profilingSummary.commitDurations.length;
if (numCommits === 0) {
return null;
@@ -51,7 +52,10 @@ function SnapshotSelector(_: Props) {
<Fragment>
<div className={styles.VRule} />
<div className={styles.SnapshotSelector}>
{commitIndex + 1} / {numCommits}
<span className={styles.Number}>
{`${commitIndex + 1}`.padStart(`${numCommits}`.length, '0')} /{' '}
{numCommits}
</span>
<Button
className={styles.Button}
disabled={commitIndex <= 0}
@@ -60,7 +64,13 @@ function SnapshotSelector(_: Props) {
<ButtonIcon type="previous" />
</Button>
<div className={styles.Commits}>
[] {/* TODO (profiling) Add FixedSizeList selector */}
<SnapshotCommitList
profilingSummary={profilingSummary}
selectedCommitIndex={commitIndex}
setCommitIndex={setCommitIndex}
viewNextCommit={viewNextCommit}
viewPrevCommit={viewPrevCommit}
/>
</div>
<Button
className={styles.Button}
+8
View File
@@ -0,0 +1,8 @@
// @flow
export const barHeight = 20;
export const barWidth = 100;
export const barWidthThreshold = 2;
export const minBarHeight = 5;
export const minBarWidth = 5;
export const textHeight = 18;
+5 -2
View File
@@ -29,8 +29,11 @@ export type CommitDetails = {|
|};
export type ProfilingSummary = {|
// Tuples of commit time (relative to when profiling started) and duration
commits: Array<number>,
// Commit durations
commitDurations: Array<number>,
// Commit times (relative to when profiling started)
commitTimes: Array<number>,
// Map of fiber id to (initial) tree base duration
initialTreeBaseDurations: Map<number, number>,
+34
View File
@@ -0,0 +1,34 @@
// @flow
const commitGradient = [
'var(--color-commit-gradient-0)',
'var(--color-commit-gradient-1)',
'var(--color-commit-gradient-2)',
'var(--color-commit-gradient-3)',
'var(--color-commit-gradient-4)',
'var(--color-commit-gradient-5)',
'var(--color-commit-gradient-6)',
'var(--color-commit-gradient-7)',
'var(--color-commit-gradient-8)',
'var(--color-commit-gradient-9)',
];
export const getGradientColor = (value: number) => {
const maxIndex = commitGradient.length - 1;
let index;
if (Number.isNaN(value)) {
index = 0;
} else if (!Number.isFinite(value)) {
index = maxIndex;
} else {
index = Math.max(0, Math.min(maxIndex, value)) * maxIndex;
}
return commitGradient[Math.round(index)];
};
export const formatDuration = (duration: number) =>
Math.round(duration * 10) / 10;
export const formatPercentage = (percentage: number) =>
Math.round(percentage * 100);
export const formatTime = (timestamp: number) =>
Math.round(Math.round(timestamp) / 100) / 10;
@@ -134,6 +134,17 @@ function updateThemeVariables(theme: Theme): void {
updateStyleHelper(theme, 'color-button-disabled');
updateStyleHelper(theme, 'color-button-focus');
updateStyleHelper(theme, 'color-button-hover');
updateStyleHelper(theme, 'color-commit-did-not-render');
updateStyleHelper(theme, 'color-commit-gradient-0');
updateStyleHelper(theme, 'color-commit-gradient-1');
updateStyleHelper(theme, 'color-commit-gradient-2');
updateStyleHelper(theme, 'color-commit-gradient-3');
updateStyleHelper(theme, 'color-commit-gradient-4');
updateStyleHelper(theme, 'color-commit-gradient-5');
updateStyleHelper(theme, 'color-commit-gradient-6');
updateStyleHelper(theme, 'color-commit-gradient-7');
updateStyleHelper(theme, 'color-commit-gradient-8');
updateStyleHelper(theme, 'color-commit-gradient-9');
updateStyleHelper(theme, 'color-component-name');
updateStyleHelper(theme, 'color-component-name-inverted');
updateStyleHelper(theme, 'color-dim');
+1 -1
View File
@@ -54,7 +54,7 @@ export function useLocalStorage<T>(
}
export function useModalDismissSignal(
modalRef: React$Ref<any>,
modalRef: { current: HTMLDivElement | null },
dismissCallback: Function
): void {
useEffect(() => {
+22
View File
@@ -16,6 +16,17 @@
--light-color-button-focus: #3578e5;
--light-color-button-hover: #3578e5;
--light-color-border: #eeeeee;
--light-color-commit-did-not-render: #777d88;
--light-color-commit-gradient-0: #37afa9;
--light-color-commit-gradient-1: #63b19e;
--light-color-commit-gradient-2: #80b393;
--light-color-commit-gradient-3: #97b488;
--light-color-commit-gradient-4: #abb67d;
--light-color-commit-gradient-5: #beb771;
--light-color-commit-gradient-6: #cfb965;
--light-color-commit-gradient-7: #dfba57;
--light-color-commit-gradient-8: #efbb49;
--light-color-commit-gradient-9: #febc38;
--light-color-component-name: #8155cb;
--light-color-component-name-inverted: #ffffff;
--light-color-dim: #777d88;
@@ -46,6 +57,17 @@
--dark-color-button-focus: #a2e9fc;
--dark-color-button-hover: #a2e9fc;
--dark-color-border: #3d424a;
--dark-color-commit-did-not-render: #8f949d;
--dark-color-commit-gradient-0: #37afa9;
--dark-color-commit-gradient-1: #63b19e;
--dark-color-commit-gradient-2: #80b393;
--dark-color-commit-gradient-3: #97b488;
--dark-color-commit-gradient-4: #abb67d;
--dark-color-commit-gradient-5: #beb771;
--dark-color-commit-gradient-6: #cfb965;
--dark-color-commit-gradient-7: #dfba57;
--dark-color-commit-gradient-8: #efbb49;
--dark-color-commit-gradient-9: #febc38;
--dark-color-component-name: #61dafb;
--dark-color-component-name-inverted: ##282828;
--dark-color-dim: #8f949d;
+4 -4
View File
@@ -4671,10 +4671,10 @@ flatstr@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3"
flow-bin@^0.93.0:
version "0.93.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.93.0.tgz#9192a08d88db2a8da0ff55e42420f44539791430"
integrity sha512-p8yq4ocOlpyJgOEBEj0v0GzCP25c9WP0ilFQ8hXSbrTR7RPKuR+Whr+OitlVyp8ocdX0j1MrIwQ8x28dacy1pg==
flow-bin@^0.94.0:
version "0.94.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.94.0.tgz#b5d58fe7559705b73a18229f97edfc3ab6ffffcb"
integrity sha512-DYF7r9CJ/AksfmmB4+q+TyLMoeQPRnqtF1Pk7KY3zgfkB/nVuA3nXyzqgsIPIvnMSiFEXQcFK4z+iPxSLckZhQ==
flush-write-stream@^1.0.0:
version "1.1.1"