From 0edf4e9dc16e58881dc3c45180d2d384ca6fc35e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 13 Mar 2019 14:57:52 -0700 Subject: [PATCH] Plugged react-window into commit selector --- OVERVIEW.md | 16 +- package.json | 2 +- src/backend/renderer.js | 9 +- src/backend/types.js | 3 +- src/devtools/ProfilingCache.js | 19 +-- src/devtools/store.js | 3 + src/devtools/views/Profiler/Profiler.js | 2 +- .../views/Profiler/ProfilerContext.js | 6 + .../views/Profiler/SnapshotCommitList.js | 145 ++++++++++++++++++ .../views/Profiler/SnapshotCommitListItem.js | 77 ++++++++++ .../views/Profiler/SnapshotSelector.css | 8 + .../views/Profiler/SnapshotSelector.js | 16 +- src/devtools/views/Profiler/constants.js | 8 + src/devtools/views/Profiler/types.js | 7 +- src/devtools/views/Profiler/utils.js | 34 ++++ .../views/Settings/SettingsContext.js | 11 ++ src/devtools/views/hooks.js | 2 +- src/devtools/views/root.css | 22 +++ yarn.lock | 8 +- 19 files changed, 366 insertions(+), 32 deletions(-) create mode 100644 src/devtools/views/Profiler/SnapshotCommitList.js create mode 100644 src/devtools/views/Profiler/SnapshotCommitListItem.js create mode 100644 src/devtools/views/Profiler/constants.js create mode 100644 src/devtools/views/Profiler/utils.js diff --git a/OVERVIEW.md b/OVERVIEW.md index b6d68549a5..78ae10aff2 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -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 diff --git a/package.json b/package.json index c89882904a..938a3b6aca 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 398b4cfba3..1d45f6c3e9 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -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, diff --git a/src/backend/types.js b/src/backend/types.js index 11869ab179..47b0280860 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -55,7 +55,8 @@ export type ReactRenderer = { }; export type ProfilingSummary = {| - commits: Array, + commitDurations: Array, + commitTimes: Array, initialTreeBaseDurations: Array, interactionCount: number, rootID: number, diff --git a/src/devtools/ProfilingCache.js b/src/devtools/ProfilingCache.js index 68982b081c..b718f81f51 100644 --- a/src/devtools/ProfilingCache.js +++ b/src/devtools/ProfilingCache.js @@ -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; - - // TODO (profiling) renderer + root + ProfilingSummary: Resource; 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, }); diff --git a/src/devtools/store.js b/src/devtools/store.js index 4f733df1e6..c1fe80ebbe 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -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'); diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js index af11db8e10..e02630d699 100644 --- a/src/devtools/views/Profiler/Profiler.js +++ b/src/devtools/views/Profiler/Profiler.js @@ -69,7 +69,7 @@ function ProfilerInner(_: Props) {
{view} - {isFilterModalShowing && ( // TODO (profiler) Position when snapshot graph is open + {isFilterModalShowing && ( )}
diff --git a/src/devtools/views/Profiler/ProfilerContext.js b/src/devtools/views/Profiler/ProfilerContext.js index df4f2874d5..467d8247a2 100644 --- a/src/devtools/views/Profiler/ProfilerContext.js +++ b/src/devtools/views/Profiler/ProfilerContext.js @@ -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, diff --git a/src/devtools/views/Profiler/SnapshotCommitList.js b/src/devtools/views/Profiler/SnapshotCommitList.js new file mode 100644 index 0000000000..4f4fe92661 --- /dev/null +++ b/src/devtools/views/Profiler/SnapshotCommitList.js @@ -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, + commitTimes: Array, + 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 ( + + {({ height, width }) => } + + ); +} + +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 | null>(null); + const [isMouseDown, setIsMouseDown] = useState(false); + const prevSelectedCommitIndexRef = useRef(-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( + () => ({ + commitDurations, + commitTimes, + isMouseDown, + maxDuration, + selectedCommitIndex, + setCommitIndex, + }), + [ + commitDurations, + commitTimes, + isMouseDown, + maxDuration, + selectedCommitIndex, + setCommitIndex, + ] + ); + + return ( +
+ {commitDurations.length > 0 && ( + + {SnapshotCommitListItem} + + )} +
+ ); +} diff --git a/src/devtools/views/Profiler/SnapshotCommitListItem.js b/src/devtools/views/Profiler/SnapshotCommitListItem.js new file mode 100644 index 0000000000..6defc07346 --- /dev/null +++ b/src/devtools/views/Profiler/SnapshotCommitListItem.js @@ -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 ( +
+
+
+ ); +} + +export default memo(SnapshotCommitListItem, areEqual); diff --git a/src/devtools/views/Profiler/SnapshotSelector.css b/src/devtools/views/Profiler/SnapshotSelector.css index b404d6251d..d101b3b8d0 100644 --- a/src/devtools/views/Profiler/SnapshotSelector.css +++ b/src/devtools/views/Profiler/SnapshotSelector.css @@ -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); +} diff --git a/src/devtools/views/Profiler/SnapshotSelector.js b/src/devtools/views/Profiler/SnapshotSelector.js index 091ffd3441..e63da13173 100644 --- a/src/devtools/views/Profiler/SnapshotSelector.js +++ b/src/devtools/views/Profiler/SnapshotSelector.js @@ -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) {
- {commitIndex + 1} / {numCommits} + + {`${commitIndex + 1}`.padStart(`${numCommits}`.length, '0')} /{' '} + {numCommits} +
- [] {/* TODO (profiling) Add FixedSizeList selector */} +