diff --git a/frontend/Icons.js b/frontend/Icons.js index 14d1174a4a..e4f332c5ec 100644 --- a/frontend/Icons.js +++ b/frontend/Icons.js @@ -19,7 +19,7 @@ const Icons = { CHECK: ` M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z `, - CLEAR: ` + CLOSE: ` M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z `, diff --git a/frontend/Node.js b/frontend/Node.js index d34acb1ed7..aaaff88bb7 100644 --- a/frontend/Node.js +++ b/frontend/Node.js @@ -336,11 +336,14 @@ class Node extends React.Component { const jsxOpenTagStyle = jsxTagStyle(inverted && (!isBottomTagSelected || collapsed), nodeType, theme); const head = (
this._head = h} style={sharedHeadStyle} {...headEvents}> - + {collapsed ? '▶' : '▼'} < diff --git a/frontend/SvgIcon.js b/frontend/SvgIcon.js index a55d6a644e..47ad6ff3d8 100644 --- a/frontend/SvgIcon.js +++ b/frontend/SvgIcon.js @@ -17,22 +17,23 @@ type Props = { style?: Object, }; -const SvgIcon = ({ path, style = styles.svgIcon }: Props) => ( +const SvgIcon = ({ path, style }: Props) => ( ); -const styles = { - svgIcon: { - flex: '0 0 1rem', - width: '1rem', - height: '1rem', - fill: 'currentColor', - }, +const DEFAULT_STYLE = { + flex: '0 0 1rem', + width: '1rem', + height: '1rem', + fill: 'currentColor', }; module.exports = SvgIcon; diff --git a/plugins/Profiler/ProfilerStore.js b/plugins/Profiler/ProfilerStore.js index 80b3a9d8ef..8035c9aaf6 100644 --- a/plugins/Profiler/ProfilerStore.js +++ b/plugins/Profiler/ProfilerStore.js @@ -18,6 +18,8 @@ const {EventEmitter} = require('events'); const {get, set} = require('../../utils/storage'); const LOCAL_STORAGE_CHART_TYPE_KEY = 'profiler:selectedChartType'; +const LOCAL_STORAGE_COMMIT_THRESHOLD = 'profiler:commitThreshold'; +const LOCAL_STORAGE_HIDE_COMMITS_BELOW_THRESHOLD = 'profiler:hideCommitsBelowThreshold'; const LOCAL_STORAGE_SHOW_NATIVE_NODES_KEY = 'profiler:showNativeNodes'; class ProfilerStore extends EventEmitter { @@ -25,7 +27,10 @@ class ProfilerStore extends EventEmitter { _mainStore: Object; cachedData = {}; + commitThreshold: number = ((get(LOCAL_STORAGE_COMMIT_THRESHOLD, 0): any): number); + hideCommitsBelowThreshold: boolean = ((get(LOCAL_STORAGE_HIDE_COMMITS_BELOW_THRESHOLD, false): any): boolean); isRecording: boolean = false; + isSettingsPanelActive: boolean = false; processedInteractions: {[id: string]: Interaction} = {}; rootsToProfilerData: Map = new Map(); roots: List = new List(); @@ -86,6 +91,29 @@ class ProfilerStore extends EventEmitter { this.emit('roots', this._mainStore.roots); }; + setCommitThrehsold = (commitThreshold: number) => { + this.commitThreshold = commitThreshold; + this.emit('commitThreshold', commitThreshold); + set(LOCAL_STORAGE_COMMIT_THRESHOLD, commitThreshold); + }; + + setHideCommitsBelowThreshold(hideCommitsBelowThreshold: boolean): void { + this.hideCommitsBelowThreshold = hideCommitsBelowThreshold; + this.emit('hideCommitsBelowThreshold', hideCommitsBelowThreshold); + set(LOCAL_STORAGE_HIDE_COMMITS_BELOW_THRESHOLD, hideCommitsBelowThreshold); + } + + setIsRecording(isRecording: boolean): void { + this.isRecording = isRecording; + this.emit('isRecording', isRecording); + this._mainStore.setIsRecording(isRecording); + } + + setIsSettingsPanelActive(isSettingsPanelActive: boolean): void { + this.isSettingsPanelActive = isSettingsPanelActive; + this.emit('isSettingsPanelActive', isSettingsPanelActive); + } + setSelectedChartType(selectedChartType: ChartType) { this.selectedChartType = selectedChartType; this.emit('selectedChartType', selectedChartType); @@ -98,12 +126,6 @@ class ProfilerStore extends EventEmitter { set(LOCAL_STORAGE_SHOW_NATIVE_NODES_KEY, showNativeNodes); } - setIsRecording(isRecording: boolean): void { - this.isRecording = isRecording; - this.emit('isRecording', isRecording); - this._mainStore.setIsRecording(isRecording); - } - storeSnapshot = () => { this._mainStore.snapshotQueue.forEach((snapshot: Snapshot) => { const { root } = snapshot; diff --git a/plugins/Profiler/views/ChartNode.js b/plugins/Profiler/views/ChartNode.js index 242e5101a1..7c73528a72 100644 --- a/plugins/Profiler/views/ChartNode.js +++ b/plugins/Profiler/views/ChartNode.js @@ -24,6 +24,7 @@ type Props = {| onDoubleClick?: Function, placeLabelAboveNode?: boolean, theme: Theme, + title: string, width: number, x: number, y: number, @@ -31,12 +32,24 @@ type Props = {| const minWidthToDisplay = 35; -const ChartNode = ({ color, height, isDimmed = false, label, onClick, onDoubleClick, theme, width, x, y }: Props) => ( +const ChartNode = ({ + color, + height, + isDimmed = false, + label, + onClick, + onDoubleClick, + theme, + title, + width, + x, + y, +}: Props) => ( - {label} + {title} void; type Props = {| + commitThreshold: number, + hideCommitsBelowThreshold: boolean, selectedFiberID: string, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, + snapshotIndex: number, snapshots: Array, stopInspecting: Function, theme: Theme, |}; export default ({ + commitThreshold, + hideCommitsBelowThreshold, selectedFiberID, selectedSnapshot, selectSnapshot, + snapshotIndex, snapshots, stopInspecting, theme, -}: Props) => ( - - {({ height, width }) => ( - - )} - -); +}: Props) => { + const filteredData = getFilteredSnapshotData( + commitThreshold, + hideCommitsBelowThreshold, + true, // If we're viewing this component + selectedFiberID, + selectedSnapshot, + snapshotIndex, + snapshots, + ); + + return ( + + {({ height, width }) => ( + + )} + + ); +}; type RenderDurationsProps = {| + commitThreshold: number, height: number, + hideCommitsBelowThreshold: boolean, selectedFiberID: string, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, @@ -90,7 +112,9 @@ type RenderDurationsProps = {| |}; const RenderDurations = ({ + commitThreshold, height, + hideCommitsBelowThreshold, selectedFiberID, selectedSnapshot, selectSnapshot, @@ -111,7 +135,9 @@ const RenderDurations = ({ if (maxValue === 0) { return ( @@ -171,6 +197,7 @@ class ListItem extends PureComponent { onClick={() => selectSnapshot(node.parentSnapshot)} onDoubleClick={stopInspecting} theme={theme} + title={`${node.value}ms`} width={width} x={left} y={height - safeHeight} diff --git a/plugins/Profiler/views/IconButton.js b/plugins/Profiler/views/IconButton.js index 5f46c55c14..102c510614 100644 --- a/plugins/Profiler/views/IconButton.js +++ b/plugins/Profiler/views/IconButton.js @@ -22,18 +22,29 @@ const IconButton = Hoverable( onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{ + position: 'relative', + width: '1.5rem', + height: '1.5rem', background: isTransparent ? 'none' : theme.base00, border: 'none', + borderRadius: '0.125rem', outline: 'none', cursor: disabled ? 'default' : 'pointer', color: isActive ? theme.state00 : (isHovered ? theme.state06 : theme.base05), opacity: disabled ? 0.5 : 1, - padding: '4px', ...style, }} title={title} > - + ) ); diff --git a/plugins/Profiler/views/NoRenderTimesMessage.js b/plugins/Profiler/views/NoRenderTimesMessage.js index 497022599b..77c391966e 100644 --- a/plugins/Profiler/views/NoRenderTimesMessage.js +++ b/plugins/Profiler/views/NoRenderTimesMessage.js @@ -14,12 +14,20 @@ import React from 'react'; import {sansSerif} from '../../../frontend/Themes/Fonts'; type Props = {| + commitThreshold: number, height: number, + hideCommitsBelowThreshold: boolean, stopInspecting: Function, width: number, |}; -export default ({ height, stopInspecting, width }: Props) => ( +export default ({ + commitThreshold, + height, + hideCommitsBelowThreshold, + stopInspecting, + width, +}: Props) => (
( height, width, }}> -

- No render times were recorded for the selected element -

+ {!hideCommitsBelowThreshold && ( +

+ No render times were recorded for the selected element. +

+ )} + {hideCommitsBelowThreshold && ( +

+ No render times were recorded for the selected element based on the current commit threshold. +

+ )}

diff --git a/plugins/Profiler/views/ProfilerFiberDetailPane.js b/plugins/Profiler/views/ProfilerFiberDetailPane.js index 3a8bb8db72..eda088cb5b 100644 --- a/plugins/Profiler/views/ProfilerFiberDetailPane.js +++ b/plugins/Profiler/views/ProfilerFiberDetailPane.js @@ -11,7 +11,7 @@ 'use strict'; import type {Theme} from '../../../frontend/types'; -import type {ChartType, Snapshot} from '../ProfilerTypes'; +import type {Snapshot} from '../ProfilerTypes'; import React, {Fragment} from 'react'; import {monospace} from '../../../frontend/Themes/Fonts'; @@ -24,9 +24,9 @@ import IconButton from './IconButton'; const emptyFunction = () => {}; type Props = {| + deselectFiber: Function, isInspectingSelectedFiber: boolean, name?: string, - selectedChartType: ChartType, snapshot: Snapshot, snapshotFiber: any, theme: Theme, @@ -34,9 +34,9 @@ type Props = {| |}; const ProfilerFiberDetailPane = ({ + deselectFiber, isInspectingSelectedFiber, name = 'Unknown', - selectedChartType, snapshot, snapshotFiber, theme, @@ -66,18 +66,30 @@ const ProfilerFiberDetailPane = ({ > {name}
- +
+ + +
{snapshotFiber !== null && (
void, + showNativeNodes: boolean, + toggleHideCommitsBelowThreshold: Function, + toggleIsSettingsPanelActive: Function, + toggleShowNativeNodes: Function, +|}; + +class ProfilerSettings extends PureComponent { + static contextTypes = { + theme: PropTypes.object.isRequired, + }; + + handleCommitThresholdChange = event => { + const { hideCommitsBelowThreshold, setCommitThrehsold, toggleHideCommitsBelowThreshold } = this.props; + + const commitThreshold = parseFloat(event.currentTarget.value); + if (!Number.isNaN(commitThreshold)) { + setCommitThrehsold(commitThreshold); + + // For convenience, enable the hide-commits feature if the threshold is being changed. + // This seems likely to be what the user wants. + if (!hideCommitsBelowThreshold) { + toggleHideCommitsBelowThreshold(); + } + } + }; + + stopClickEventFromBubbling = event => event.stopPropagation(); + + render() { + const { theme } = this.context; + const { + commitThreshold, + hideCommitsBelowThreshold, + isSettingsPanelActive, + toggleHideCommitsBelowThreshold, + toggleIsSettingsPanelActive, + toggleShowNativeNodes, + showNativeNodes, + } = this.props; + + if (!isSettingsPanelActive) { + return null; + } + + return ( +
+
+

+ Profiler settings +

+ + + + +
+
+ ); + } +} + +export default decorate({ + store: 'profilerStore', + listeners: () => [ + 'commitThreshold', + 'hideCommitsBelowThreshold', + 'isSettingsPanelActive', + 'showNativeNodes', + ], + props(store) { + return { + commitThreshold: store.commitThreshold, + hideCommitsBelowThreshold: store.hideCommitsBelowThreshold, + isSettingsPanelActive: store.isSettingsPanelActive, + showNativeNodes: store.showNativeNodes, + setCommitThrehsold: store.setCommitThrehsold, + toggleHideCommitsBelowThreshold: () => store.setHideCommitsBelowThreshold(!store.hideCommitsBelowThreshold), + toggleIsSettingsPanelActive: () => store.setIsSettingsPanelActive(!store.isSettingsPanelActive), + toggleShowNativeNodes: () => store.setShowNativeNodes(!store.showNativeNodes), + }; + }, +}, ProfilerSettings); diff --git a/plugins/Profiler/views/ProfilerSnapshotDetailPane.js b/plugins/Profiler/views/ProfilerSnapshotDetailPane.js index 5336378c9e..4e2c6c5a41 100644 --- a/plugins/Profiler/views/ProfilerSnapshotDetailPane.js +++ b/plugins/Profiler/views/ProfilerSnapshotDetailPane.js @@ -50,7 +50,7 @@ const ProfilerSnapshotDetailPane = ({
-
Commit time: {formatTime(snapshot.commitTime)}s
+
Committed at: {formatTime(snapshot.commitTime)}s
Render duration: {formatDuration(snapshot.duration)}ms
Interactions:
    >, isRecording: boolean, profilerData: RootProfilerData, @@ -50,8 +53,8 @@ type Props = {| showNativeNodes: boolean, snapshots: Array, timestampsToInteractions: Map>, - toggleIsRecording: () => void, - toggleShowNativeNodes: () => void, + toggleIsRecording: Function, + toggleIsSettingsPanelActive: Function, |}; type State = {| @@ -128,12 +131,6 @@ class ProfilerTab extends React.Component { selectedFiberName: name, }); - selectNextSnapshotIndex = (event: SyntheticEvent) => - this.setState(prevState => ({ snapshotIndex: prevState.snapshotIndex + 1 })); - - selectPreviousSnapshotIndex = (event: SyntheticEvent) => - this.setState(prevState => ({ snapshotIndex: prevState.snapshotIndex - 1 })); - // We store the ID and name separately, // Because a Fiber may not exist in all snapshots. // In that case, it's still important to show the selected fiber (name) in the details pane. @@ -177,9 +174,11 @@ class ProfilerTab extends React.Component { const { cacheDataForSnapshot, cacheInteractionData, + commitThreshold, getCachedDataForSnapshot, getCachedInteractionData, hasMultipleRoots, + hideCommitsBelowThreshold, interactionsToSnapshots, isRecording, profilerData, @@ -189,6 +188,7 @@ class ProfilerTab extends React.Component { snapshots, timestampsToInteractions, toggleIsRecording, + toggleIsSettingsPanelActive, } = this.props; const { isInspectingSelectedFiber, @@ -220,9 +220,12 @@ class ProfilerTab extends React.Component { if (isInspectingSelectedFiber && selectedFiberID !== null) { content = ( { content = ( { } else if (selectedChartType !== 'interactions' && selectedFiberName !== null) { details = ( { borderBottom: `1px solid ${theme.base03}`, }}>
{content} + +
( export default decorate({ store: 'profilerStore', listeners: () => [ + 'commitThreshold', + 'hideCommitsBelowThreshold', 'isRecording', 'profilerData', 'selectedChartType', @@ -445,7 +454,9 @@ export default decorate({ cacheInteractionData: (...args) => store.cacheInteractionData(...args), getCachedDataForSnapshot: (...args) => store.getCachedDataForSnapshot(...args), getCachedInteractionData: (...args) => store.getCachedInteractionData(...args), + commitThreshold: store.commitThreshold, hasMultipleRoots: store.roots.size > 1, + hideCommitsBelowThreshold: store.hideCommitsBelowThreshold, interactionsToSnapshots: profilerData !== null ? profilerData.interactionsToSnapshots : new Map(), @@ -461,7 +472,7 @@ export default decorate({ ? profilerData.timestampsToInteractions : new Map(), toggleIsRecording: () => store.setIsRecording(!store.isRecording), - toggleShowNativeNodes: () => store.setShowNativeNodes(!store.showNativeNodes), + toggleIsSettingsPanelActive: () => store.setIsSettingsPanelActive(!store.isSettingsPanelActive), }; }, }, ProfilerTab); diff --git a/plugins/Profiler/views/ProfilerTabToolbar.js b/plugins/Profiler/views/ProfilerTabToolbar.js index b72fde3658..b6e9409d87 100644 --- a/plugins/Profiler/views/ProfilerTabToolbar.js +++ b/plugins/Profiler/views/ProfilerTabToolbar.js @@ -22,26 +22,25 @@ import IconButton from './IconButton'; import SnapshotSelector from './SnapshotSelector'; const CHART_RADIO_LABEL_WIDTH_THRESHOLD = 650; -const CHART_NATIVE_NODES_TOGGLE_LABEL_WIDTH_THRESHOLD = 800; type SelectSnapshot = (snapshot: Snapshot) => void; type Props = {| + commitThreshold: number, + hideCommitsBelowThreshold: boolean, interactionsCount: number, isInspectingSelectedFiber: boolean, isRecording: boolean, selectChart: (chart: ChartType) => void, - selectNextSnapshotIndex: Function, - selectPreviousSnapshotIndex: Function, selectedChartType: ChartType, + selectedFiberID: string | null, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, - showNativeNodes: boolean, snapshotIndex: number, snapshots: Array, theme: Theme, toggleIsRecording: Function, - toggleShowNativeNodes: Function, + toggleIsSettingsPanelActive: Function, |}; export default (props: Props) => ( @@ -53,40 +52,40 @@ export default (props: Props) => ( ); type ProfilerTabToolbarProps = { + commitThreshold: number, + hideCommitsBelowThreshold: boolean, interactionsCount: number, isInspectingSelectedFiber: boolean, isRecording: boolean, selectChart: (chart: ChartType) => void, - selectNextSnapshotIndex: Function, - selectPreviousSnapshotIndex: Function, selectedChartType: ChartType, + selectedFiberID: string | null, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, - showNativeNodes: boolean, snapshotIndex: number, snapshots: Array, theme: Theme, toggleIsRecording: Function, - toggleShowNativeNodes: Function, + toggleIsSettingsPanelActive: Function, width: number, }; const ProfilerTabToolbar = ({ + commitThreshold, + hideCommitsBelowThreshold, interactionsCount, isInspectingSelectedFiber, isRecording, selectChart, - selectNextSnapshotIndex, - selectPreviousSnapshotIndex, selectedChartType, + selectedFiberID, selectedSnapshot, selectSnapshot, - showNativeNodes, snapshotIndex, snapshots, theme, toggleIsRecording, - toggleShowNativeNodes, + toggleIsSettingsPanelActive, width, }: ProfilerTabToolbarProps) => (
+ {isRecording || snapshots.length === 0 && ( + +
+ + + + )} + {!isRecording && snapshots.length > 0 && ( - - - {snapshotIndex + 1} / {snapshots.length} - - - )}
@@ -291,59 +285,3 @@ const RecordButton = Hoverable( ) ); - -type ShowNativeNodesButtonProps = {| - isActive: boolean, - isDisabled: boolean, - isHovered: boolean, - onClick: Function, - onMouseEnter: Function, - onMouseLeave: Function, - theme: Theme, - width: number, -|}; - -const ShowNativeNodesButton = Hoverable(({ - isActive, - isDisabled, - isHovered, - onClick, - onMouseEnter, - onMouseLeave, - theme, - width, -}: ShowNativeNodesButtonProps) => ( - -)); diff --git a/plugins/Profiler/views/SnapshotFlamegraph.js b/plugins/Profiler/views/SnapshotFlamegraph.js index 7b99b66005..85f65e9599 100644 --- a/plugins/Profiler/views/SnapshotFlamegraph.js +++ b/plugins/Profiler/views/SnapshotFlamegraph.js @@ -72,6 +72,7 @@ type ItemData = {| type Props = {| cacheDataForSnapshot: CacheDataForSnapshot, + deselectFiber: Function, getCachedDataForSnapshot: GetCachedDataForSnapshot, inspectFiber: SelectOrInspectFiber, selectedFiberID: string | null, @@ -84,6 +85,7 @@ type Props = {| const SnapshotFlamegraph = ({ cacheDataForSnapshot, + deselectFiber, getCachedDataForSnapshot, inspectFiber, selectedFiberID, @@ -105,6 +107,7 @@ const SnapshotFlamegraph = ({ {({ height, width }) => ( - {ListItem} - + + {ListItem} + +
); }; class ListItem extends PureComponent { + handleClick = (id, name, event) => { + event.stopPropagation(); + const itemData: ItemData = ((this.props.data: any): ItemData); + itemData.selectFiber(id, name); + }; + + handleDoubleClick = (id, name, event) => { + event.stopPropagation(); + const itemData: ItemData = ((this.props.data: any): ItemData); + itemData.inspectFiber(id, name); + }; + render() { const { index, style } = this.props; const itemData: ItemData = ((this.props.data: any): ItemData); @@ -245,9 +267,10 @@ class ListItem extends PureComponent { isDimmed={index < focusedNodeIndex} key={id} label={didRender ? `${name} (${actualDuration.toFixed(2)}ms)` : name} - onClick={() => itemData.selectFiber(id, name)} - onDoubleClick={() => itemData.inspectFiber(id, name)} + onClick={this.handleClick.bind(this, id, name)} + onDoubleClick={this.handleDoubleClick.bind(this, id, name)} theme={itemData.theme} + title={didRender ? `${name} (${actualDuration}ms)` : name} width={nodeWidth} x={nodeX - focusedNodeX} y={top} diff --git a/plugins/Profiler/views/SnapshotRanked.js b/plugins/Profiler/views/SnapshotRanked.js index 863fad5bc4..87755b4691 100644 --- a/plugins/Profiler/views/SnapshotRanked.js +++ b/plugins/Profiler/views/SnapshotRanked.js @@ -25,6 +25,7 @@ type Node = {| id: any, label: string, name: string, + title: string, value: number, |}; @@ -44,6 +45,7 @@ type ItemData = {| type Props = {| cacheDataForSnapshot: CacheDataForSnapshot, + deselectFiber: Function, getCachedDataForSnapshot: GetCachedDataForSnapshot, inspectFiber: SelectOrInspectFiber, selectedFiberID: string | null, @@ -56,6 +58,7 @@ type Props = {| const SnapshotRanked = ({ cacheDataForSnapshot, + deselectFiber, getCachedDataForSnapshot, inspectFiber, selectedFiberID, @@ -77,6 +80,7 @@ const SnapshotRanked = ({ {({ height, width }) => ( - {ListItem} - + + {ListItem} + +
); }; class ListItem extends PureComponent { + handleClick = event => { + event.stopPropagation(); + const { data, index } = this.props; + const node = data.nodes[index]; + data.selectFiber(node.id, node.name, data.snapshot.root); + }; + + handleDoubleClick = event => { + event.stopPropagation(); + const { data, index } = this.props; + const node = data.nodes[index]; + data.inspectFiber(node.id, node.name, data.snapshot.root); + }; + render() { const { data, index, style } = this.props; @@ -172,9 +197,10 @@ class ListItem extends PureComponent { isDimmed={index < data.focusedNodeIndex} key={node.id} label={node.label} - onClick={() => data.selectFiber(node.id, node.name, data.snapshot.root)} - onDoubleClick={() => data.inspectFiber(node.id, node.name, data.snapshot.root)} + onClick={this.handleClick} + onDoubleClick={this.handleDoubleClick} theme={data.theme} + title={node.title} width={Math.max(minBarWidth, scaleX(node.value))} x={0} y={top} @@ -234,6 +260,7 @@ const convertSnapshotToChartData = (snapshot: Snapshot, showNativeNodes: boolean id: node.id, label: `${name} (${node.actualDuration.toFixed(2)}ms)`, name, + title: `${name} (${node.actualDuration}ms)`, value: node.actualDuration, }; }) diff --git a/plugins/Profiler/views/SnapshotSelector.js b/plugins/Profiler/views/SnapshotSelector.js index 31e8ec9da0..38e7c534fc 100644 --- a/plugins/Profiler/views/SnapshotSelector.js +++ b/plugins/Profiler/views/SnapshotSelector.js @@ -17,7 +17,17 @@ import memoize from 'memoize-one'; import React, {PureComponent} from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; -import { didNotRender, formatDuration, formatTime, getGradientColor, minBarHeight, minBarWidth } from './constants'; +import { + didNotRender, + formatDuration, + formatTime, + getGradientColor, + getFilteredSnapshotData, + minBarHeight, + minBarWidth, +} from './constants'; +import Icons from '../../../frontend/Icons'; +import IconButton from './IconButton'; const HEIGHT = 20; @@ -29,62 +39,221 @@ type ListData = {| |}; type ItemData = {| - disabled: boolean, isMouseDown: boolean, maxDuration: number, selectSnapshot: SelectSnapshot, selectedSnapshot: Snapshot, snapshots: Array, + theme: Theme, |}; type Props = {| - disabled: boolean, + commitThreshold: number, + hideCommitsBelowThreshold: boolean, + isInspectingSelectedFiber: boolean, + selectedFiberID: string | null, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, + snapshotIndex: number, snapshots: Array, theme: Theme, |}; export default ({ - disabled, + commitThreshold, + hideCommitsBelowThreshold, + isInspectingSelectedFiber, + selectedFiberID, selectedSnapshot, selectSnapshot, + snapshotIndex, snapshots, theme, }: Props) => { + const filteredData = getFilteredSnapshotData( + commitThreshold, + hideCommitsBelowThreshold, + isInspectingSelectedFiber, + selectedFiberID, + selectedSnapshot, + snapshotIndex, + snapshots, + ); + return ( -
- - {({ height, width }) => ( - - )} - -
+ ); }; +class SnapshotSelectorWrapper extends PureComponent { + handleKeyDown = event => { + if (event.keyCode === LEFT_ARROW || event.keyCode === RIGHT_ARROW) { + event.preventDefault(); + + if (event.keyCode === LEFT_ARROW) { + this.selectPreviousSnapshotIndex(); + } else { + this.selectNextSnapshotIndex(); + } + } + }; + + selectNextSnapshotIndex = () => { + const { + selectSnapshot, + snapshotIndex, + snapshots, + } = this.props; + + if (snapshots.length > 0) { + const newIndex = snapshotIndex < snapshots.length - 1 + ? snapshotIndex + 1 + : 0; + + selectSnapshot(snapshots[newIndex]); + } + }; + + selectPreviousSnapshotIndex = () => { + const { + selectSnapshot, + snapshotIndex, + snapshots, + } = this.props; + + if (snapshots.length > 0) { + const newIndex = snapshotIndex > 0 + ? snapshotIndex - 1 + : snapshots.length - 1; + + selectSnapshot(snapshots[newIndex]); + } + }; + + render() { + const { + commitThreshold, + hideCommitsBelowThreshold, + isInspectingSelectedFiber, + selectedFiberID, + selectedSnapshot, + selectSnapshot, + snapshotIndex, + snapshots, + theme, + } = this.props; + + const numSnapshots = snapshots.length; + + return ( +
+ {numSnapshots === 0 && ( + + 0 / 0 + + )} + {numSnapshots > 0 && ( + + {`${snapshotIndex + 1}`.padStart(`${numSnapshots}`.length, '0')} / {numSnapshots} + + )} + + + +
+ ); + } +} + +const AutoSizedSnapshotSelector = ({ + isInspectingSelectedFiber, + selectedFiberID, + selectedSnapshot, + selectSnapshot, + snapshotIndex, + snapshots, + theme, +}: Props) => ( +
+ + {({ height, width }) => ( + + )} + +
+); + +const LEFT_ARROW = 37; +const RIGHT_ARROW = 39; + type SnapshotSelectorProps = {| - disabled: boolean, height: number, + selectedFiberID: string | null, selectedSnapshot: Snapshot, selectSnapshot: SelectSnapshot, + snapshotIndex: number, snapshots: Array, + theme: Theme, width: number, |}; @@ -93,21 +262,36 @@ type SnapshotSelectorState = {| |}; class SnapshotSelector extends PureComponent { + // $FlowFixMe createRef() + listRef = React.createRef(); + state: SnapshotSelectorState = { isMouseDown: false, }; - handleMouseDown = event => this.setState({ isMouseDown: true }); - handleMouseLeave = event => this.setState({ isMouseDown: false }); + componentDidUpdate(prevProps) { + // Make sure any newly selected snapshot is visible within the list. + if (this.props.snapshotIndex !== prevProps.snapshotIndex) { + this.listRef.current.scrollToItem(this.props.snapshotIndex); + } + } + + componentWillUnmount() { + window.removeEventListener('mouseup', this.handleMouseUp); + } + + handleMouseDown = event => this.setState({ isMouseDown: true }, () => { + window.addEventListener('mouseup', this.handleMouseUp); + }); handleMouseUp = event => this.setState({ isMouseDown: false }); render() { const { - disabled, height, selectedSnapshot, selectSnapshot, snapshots, + theme, width, } = this.props; const {isMouseDown} = this.state; @@ -117,31 +301,35 @@ class SnapshotSelector extends PureComponent - - {ListItem} - + {numSnapshots > 0 && ( + + {ListItem} + + )} ); } @@ -159,32 +347,44 @@ class ListItem extends PureComponent { const { index, style } = this.props; const itemData: ItemData = ((this.props.data: any): ItemData); - const { disabled, maxDuration, selectedSnapshot, selectSnapshot, snapshots } = itemData; + const { + maxDuration, + selectedSnapshot, + selectSnapshot, + snapshots, + theme, + } = itemData; const snapshot = snapshots[index]; // Guard against commits with duration 0 const percentage = Math.min(1, Math.max(0, snapshot.duration / maxDuration)) || 0; const isSelected = selectedSnapshot === snapshot; + const width = parseFloat(style.width) - 1; + return (
selectSnapshot(snapshot)} - onMouseEnter={disabled ? undefined : this.handleMouseEnter} + onClick={() => selectSnapshot(snapshot)} + onMouseEnter={this.handleMouseEnter} style={{ ...style, - opacity: isSelected || disabled ? 0.5 : 1, - cursor: disabled ? 'default' : 'pointer', + width, + backgroundColor: isSelected ? theme.base01 : 'transparent', userSelect: 'none', + cursor: 'pointer', }} title={`Duration ${formatDuration(snapshot.duration)}ms at ${formatTime(snapshot.commitTime)}s`} >
); @@ -200,17 +400,17 @@ const getListData = memoize(( })); const getItemData = memoize(( - disabled: boolean, isMouseDown: boolean, maxDuration: number, selectSnapshot: SelectSnapshot, selectedSnapshot: Snapshot, snapshots: Array, + theme: Theme, ): ItemData => ({ - disabled, isMouseDown, maxDuration, selectSnapshot, selectedSnapshot, snapshots, + theme, })); diff --git a/plugins/Profiler/views/constants.js b/plugins/Profiler/views/constants.js index cfc8e39c4f..fd02e8d83b 100644 --- a/plugins/Profiler/views/constants.js +++ b/plugins/Profiler/views/constants.js @@ -10,6 +10,10 @@ */ 'use strict'; +import type {Snapshot} from '../ProfilerTypes'; + +import memoize from 'memoize-one'; + // http://gka.github.io/palettes/#colors=#37AFA9,#FEBC38|steps=10|bez=0|coL=0 export const gradient = [ '#37afa9', '#63b19e', '#80b393', '#97b488', '#abb67d', '#beb771', '#cfb965', '#dfba57', '#efbb49', '#febc38', @@ -45,3 +49,37 @@ export const getGradientColor = (value: number) => { 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; + +type FilteredSnapshotData = {| + snapshotIndex: number, + snapshots: Array, +|}; + +/** + * Helper method to filter snapshots based on the current store state. + * This is a helper util so that its calculations are memoized and shared between multiple components. + */ +export const getFilteredSnapshotData = memoize(( + commitThreshold: number, + hideCommitsBelowThreshold: boolean, + isInspectingSelectedFiber: boolean, + selectedFiberID: string | null, + selectedSnapshot: Snapshot, + snapshotIndex: number, + snapshots: Array, +): FilteredSnapshotData => { + let filteredSnapshots = snapshots; + if (isInspectingSelectedFiber) { + filteredSnapshots = filteredSnapshots.filter(snapshot => snapshot.committedNodes.includes(selectedFiberID)); + } + if (hideCommitsBelowThreshold) { + filteredSnapshots = filteredSnapshots.filter(snapshot => snapshot.duration >= commitThreshold); + } + + const filteredSnapshotIndex = filteredSnapshots.indexOf(selectedSnapshot); + + return { + snapshotIndex: filteredSnapshotIndex >= 0 ? filteredSnapshotIndex : 0, + snapshots: filteredSnapshots, + }; +});