diff --git a/res/css/style.css b/res/css/style.css index 99f734ca54..00c1121586 100644 --- a/res/css/style.css +++ b/res/css/style.css @@ -277,7 +277,7 @@ body, #root, .profileViewer { height: 0; width: 0; color: #888; - border-top: 5px solid transparent; + border-top: 5px solid transparent; border-right: 4px solid transparent; border-bottom: 5px solid transparent; border-left: 8px solid; @@ -329,313 +329,6 @@ body, #root, .profileViewer { flex: 1; } -.profileViewerHeader { - position: relative; - margin-left: 149px; - border-left: 1px solid var(--grey-30); - -moz-user-focus: ignore; -} - -.profileViewerHeaderTimeRuler { - height: 20px; - overflow: hidden; -} - -.profileViewerHeaderTimeRuler::after { - content: ''; - position: absolute; - top: 20px; - left: -150px; - right: 0; - height: 1px; - background: var(--grey-30); - z-index: 3; -} - -.timeRulerContainer { - overflow: hidden; - list-style: none; - margin: 0; - padding: 0; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - -moz-user-select: none; - user-select: none; - line-height: 20px; - font-size: 9px; - color: #888; - cursor: default; - z-index: 1; /* between .profileThreadHeaderBar background and .profileThreadHeaderBarThreadDetails */ -} - -.timeRulerNotch { - display: block; - position: absolute; - top: 0; - bottom: 0; - width: 1px; - margin-left: -1px; - white-space: nowrap; - text-align: right; - background: linear-gradient(transparent, var(--grey-30) 19px, var(--grey-30) 20px, #d7d7db66 0); -} - -.timeRulerNotchText { - position: absolute; - right: 0; - padding-right: 5px; -} - -.profileViewerHeaderOverflowEdgeIndicatorScrollbox { - margin: 0 0 0 -150px; - padding-left: 150px; - max-height: 250px; - overflow: auto; -} - -.profileThreadHeaderBarIntervalMarkerOverviewContainerJank { - border-bottom: 1px solid var(--grey-30); -} - -.profileThreadHeaderBarIntervalMarkerOverview { - list-style: none; - display: block; - margin: 0; - height: 6px; - position: relative; - overflow: hidden; - opacity: 0.75; -} - -.profileThreadHeaderBarIntervalMarkerOverview.selected { - opacity: 1; -} - -.profileThreadHeaderBarIntervalMarkerOverviewThreadGeckoMain { - height: 18px; - /*border-bottom: 1px solid var(--grey-30);*/ -} - -.intervalMarkerTimelineCanvas { - display: block; - width: 100%; - height: 100%; -} - -.profileViewerHeaderThreadList { - list-style: none; - margin: 0 0 0 -150px; - padding: 0; - box-shadow: inset 0 1px var(--grey-30); -} - -.profileThreadHeaderBarHidden { - height: 0; - pointer-events: none; -} - -.profileThreadHeaderBar { - margin: 0; - padding: 0; - display: flex; - flex-flow: row nowrap; - border-top: 1px solid var(--grey-30); - box-shadow: 0 1px var(--grey-30); -} - -.profileThreadHeaderBar.selected { - background-color: #edf6ff; -} - -.profileThreadHeaderBarThreadLabel { - box-sizing: border-box; - width: 150px; - border-right: 1px solid var(--grey-30); - padding-left: 14px; - cursor: default; - display: flex; - flex-flow: row nowrap; - align-items: center; -} - -.profileThreadHeaderBarThreadName { - font-weight: normal; - font: message-box; - font-size: 100%; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.profileThreadHeaderBarThreadDetails { - flex: 1; - position: relative; - z-index: 1; - display: flex; - flex-flow: column nowrap; -} - -.threadStackGraph { - height: 25px; -} - -.threadStackGraphCanvas { - display: block; - height: 25px; - width: 100%; -} - -.timeSelectionScrubberHoverIndicator { - position: absolute; - pointer-events: none; - visibility: hidden; - top: 0; - bottom: 0; - width: 1px; - background: rgba(0,0,0,0.4); - z-index: 1; -} - -.profileViewerHeader:hover > .timeSelectionScrubberHoverIndicator { - visibility: visible; -} - -.overlay { - position: absolute; - z-index: 2; - display: flex; - flex-flow: row nowrap; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - margin-left: -5px; - padding-left: 5px; - overflow: hidden; -} - -.dimmerBefore, -.dimmerAfter { - background: rgba(12, 12, 13, .1); - flex-shrink: 0; -} - -.dimmerAfter { - flex: 1; -} - -.selectionScrubberGrippy { - height: 20px; - pointer-events: auto; - display: flex; - flex-flow: row nowrap; -} - -.grippyRangeStart, -.grippyRangeEnd { - width: 0px; - padding: 3px; - background: #AAA; - border: 1px solid white; - margin: 0 -4px; - cursor: ew-resize; - border-radius: 5px; - position: relative; - z-index: 3; -} - -.grippyRangeStart:hover, -.grippyRangeStart.dragging, -.grippyRangeEnd:hover, -.grippyRangeEnd.dragging { - background: #888; -} - -.grippyMoveRange { - flex: 1; - cursor: -webkit-grab; - cursor: grab; -} - -.grippyMoveRange.dragging { - cursor: -webkit-grabbing; - cursor: grabbing; -} - -.selectionScrubberWrapper { - display: flex; - flex-flow: column nowrap; -} - -.selectionScrubberInner { - flex: 1; - justify-content: center; - align-items: center; - display: flex; - min-width: 0; - min-height: 0; -} - -.selectionScrubberRange { - top: 20px; - position: absolute; - padding: 4px 8px; - color: #fff; - background-color: var(--blue-50); - border-radius: 0 0 4px 4px; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - pointer-events: none; - opacity: 1; - transition: opacity 200ms; -} - -.selectionScrubberRange.hidden { - opacity: 0; -} - -.selectionScrubberZoomButton { - width: 30px; - height: 30px; - pointer-events: auto; - box-sizing: border-box; - border-radius: 100%; - margin: -15px; - position: relative; - border: 1px solid rgba(0, 0, 0, 0.2); - background: url(../img/svg/zoom-icon.svg) center center no-repeat rgba(255, 255, 255, 0.6); - transition: opacity 200ms ease-in-out; - will-change: opacity; - opacity: 0.5; -} - -.selectionScrubberZoomButton.hidden { - opacity: 0.0 !important; - pointer-events: none; - transition: unset; -} - -.profileViewerHeader:hover .selectionScrubberZoomButton, -.selectionScrubberZoomButton:active { - opacity: 1.0; -} - -.selectionScrubberZoomButton:hover { - background-color: rgba(255, 255, 255, 0.9); -} - -.selectionScrubberZoomButton:active:hover { - background-color: rgba(160, 160, 160, 0.6); -} - .tabBarContainer { display: flex; flex-flow: row nowrap; diff --git a/src/components/app/Details.js b/src/components/app/Details.js index 02c17cef49..47952bc1c3 100644 --- a/src/components/app/Details.js +++ b/src/components/app/Details.js @@ -26,7 +26,7 @@ import { getSelectedTab } from '../../reducers/url-state'; import { getIsSidebarOpen } from '../../reducers/app'; import CallNodeContextMenu from '../shared/CallNodeContextMenu'; import MarkerTableContextMenu from '../marker-table/ContextMenu'; -import ProfileThreadHeaderContextMenu from '../header/ProfileThreadHeaderContextMenu'; +import TimelineThreadContextMenu from '../timeline/ThreadContextMenu'; import { toValidTabSlug } from '../../utils/flow'; import { tabsWithTitleArray } from '../../app-logic/tabs-handling'; @@ -110,7 +110,7 @@ class ProfileViewer extends PureComponent { } - + ); } diff --git a/src/components/app/ProfileViewer.js b/src/components/app/ProfileViewer.js index 7b20cae310..21bcdf6add 100644 --- a/src/components/app/ProfileViewer.js +++ b/src/components/app/ProfileViewer.js @@ -12,7 +12,7 @@ import MenuButtons from './MenuButtons'; import SymbolicationStatusOverlay from './SymbolicationStatusOverlay'; import { returnToZipFileList } from '../../actions/zipped-profiles'; import { getProfileName } from '../../reducers/url-state'; -import ProfileViewerHeader from '../header/ProfileViewerHeader'; +import Timeline from '../timeline'; import { getHasZipFile } from '../../reducers/zipped-profiles'; import type { @@ -53,7 +53,7 @@ class ProfileViewer extends PureComponent { - + diff --git a/src/components/header/ProfileThreadJankOverview.js b/src/components/header/ProfileThreadJankOverview.js deleted file mode 100644 index a39e675764..0000000000 --- a/src/components/header/ProfileThreadJankOverview.js +++ /dev/null @@ -1,34 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// @flow - -import explicitConnect from '../../utils/connect'; -import IntervalMarkerOverview from './IntervalMarkerOverview'; -import { selectorsForThread } from '../../reducers/profile-view'; -import { - styles, - overlayFills, -} from '../../profile-logic/interval-marker-styles'; -import { getSelectedThreadIndex } from '../../reducers/url-state'; - -import type { ExplicitConnectOptions } from '../../utils/connect'; -import type { StateProps, OwnProps } from './IntervalMarkerOverview'; - -const options: ExplicitConnectOptions = { - mapStateToProps: (state, props) => { - const { threadIndex } = props; - const selectors = selectorsForThread(threadIndex); - const selectedThread = getSelectedThreadIndex(state); - - return { - intervalMarkers: selectors.getJankInstances(state), - isSelected: threadIndex === selectedThread, - styles, - overlayFills, - }; - }, - component: IntervalMarkerOverview, -}; -export default explicitConnect(options); diff --git a/src/components/header/ProfileThreadTracingMarkerOverview.js b/src/components/header/ProfileThreadTracingMarkerOverview.js deleted file mode 100644 index b66682b75e..0000000000 --- a/src/components/header/ProfileThreadTracingMarkerOverview.js +++ /dev/null @@ -1,36 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// @flow - -import explicitConnect from '../../utils/connect'; -import IntervalMarkerOverview from './IntervalMarkerOverview'; -import { selectorsForThread } from '../../reducers/profile-view'; -import { - styles, - overlayFills, -} from '../../profile-logic/interval-marker-styles'; -import { getSelectedThreadIndex } from '../../reducers/url-state'; - -import type { ExplicitConnectOptions } from '../../utils/connect'; -import type { StateProps, OwnProps } from './IntervalMarkerOverview'; - -const options: ExplicitConnectOptions = { - mapStateToProps: (state, props) => { - const { threadIndex } = props; - const selectors = selectorsForThread(threadIndex); - const selectedThread = getSelectedThreadIndex(state); - const intervalMarkers = selectors.getRangeSelectionFilteredTracingMarkersForHeader( - state - ); - return { - intervalMarkers, - isSelected: threadIndex === selectedThread, - styles, - overlayFills, - }; - }, - component: IntervalMarkerOverview, -}; -export default explicitConnect(options); diff --git a/src/components/header/SelectionScrubberOverlay.js b/src/components/header/SelectionScrubberOverlay.js deleted file mode 100644 index d5d44d0923..0000000000 --- a/src/components/header/SelectionScrubberOverlay.js +++ /dev/null @@ -1,144 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// @flow - -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; -import clamp from 'clamp'; -import Draggable from '../shared/Draggable'; -import { getFormattedTimeLength } from '../../profile-logic/range-filters'; -import type { ProfileSelection } from '../../types/actions'; -import type { Action } from '../../types/store'; -import type { Milliseconds } from '../../types/units'; -import type { OnMove } from '../shared/Draggable'; - -type Props = { - rangeStart: Milliseconds, - rangeEnd: Milliseconds, - selectionStart: Milliseconds, - selectionEnd: Milliseconds, - isModifying: boolean, - width: number, - onSelectionChange: (selection: ProfileSelection) => Action, - onZoomButtonClick: (start: Milliseconds, end: Milliseconds) => *, -}; - -export default class SelectionScrubberOverlay extends PureComponent { - _rangeStartOnMove: OnMove; - _moveRangeOnMove: OnMove; - _rangeEndOnMove: OnMove; - - constructor(props: Props) { - super(props); - - const makeOnMove = fun => (originalValue, dx, dy, isModifying) => { - const { rangeStart, rangeEnd, width } = this.props; - const delta = dx / width * (rangeEnd - rangeStart); - const selectionDeltas = fun(delta); - const selectionStart = Math.max( - rangeStart, - originalValue.selectionStart + selectionDeltas.startDelta - ); - const selectionEnd = clamp( - originalValue.selectionEnd + selectionDeltas.endDelta, - selectionStart, - rangeEnd - ); - this.props.onSelectionChange({ - hasSelection: true, - isModifying, - selectionStart, - selectionEnd, - }); - }; - - this._rangeStartOnMove = makeOnMove(delta => ({ - startDelta: delta, - endDelta: 0, - })); - this._moveRangeOnMove = makeOnMove(delta => ({ - startDelta: delta, - endDelta: delta, - })); - this._rangeEndOnMove = makeOnMove(delta => ({ - startDelta: 0, - endDelta: delta, - })); - - (this: any)._zoomButtonOnMouseDown = this._zoomButtonOnMouseDown.bind(this); - (this: any)._zoomButtonOnClick = this._zoomButtonOnClick.bind(this); - } - - _zoomButtonOnMouseDown(e: SyntheticMouseEvent<>) { - e.stopPropagation(); - } - - _zoomButtonOnClick(e: SyntheticMouseEvent<>) { - e.stopPropagation(); - const { selectionStart, selectionEnd } = this.props; - this.props.onZoomButtonClick(selectionStart, selectionEnd); - } - - render() { - const { - rangeStart, - rangeEnd, - selectionStart, - selectionEnd, - isModifying, - width, - } = this.props; - const selection = { selectionStart, selectionEnd }; - const beforeWidth = - (selectionStart - rangeStart) / (rangeEnd - rangeStart) * width; - const selectionWidth = - (selectionEnd - selectionStart) / (rangeEnd - rangeStart) * width; - return ( -
-
-
-
- - - -
-
- - {getFormattedTimeLength(selectionEnd - selectionStart)} - -
-
-
-
- ); - } -} diff --git a/src/components/header/TimeSelectionScrubber.js b/src/components/header/TimeSelectionScrubber.js deleted file mode 100644 index b3ebf5ad51..0000000000 --- a/src/components/header/TimeSelectionScrubber.js +++ /dev/null @@ -1,252 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// @flow - -import * as React from 'react'; -import TimeRuler from './TimeRuler'; -import SelectionScrubberOverlay from './SelectionScrubberOverlay'; -import clamp from 'clamp'; -import { getContentRect } from '../../utils/css-geometry-tools'; -import { withSize } from '../shared/WithSize'; - -import type { Milliseconds, CssPixels } from '../../types/units'; -import type { ProfileSelection } from '../../types/actions'; -import type { SizeProps } from '../shared/WithSize'; - -type MouseHandler = (event: MouseEvent) => void; - -type Props = SizeProps & {| - +className: string, - +zeroAt: Milliseconds, - +rangeStart: Milliseconds, - +rangeEnd: Milliseconds, - +minSelectionStartWidth: Milliseconds, - +selection: ProfileSelection, - +onSelectionChange: ProfileSelection => *, - +onZoomButtonClick: ( - selectionStart: Milliseconds, - selectionEnd: Milliseconds - ) => *, - +children: React.Node, -|}; - -type State = {| - hoverLocation: null | CssPixels, -|}; - -class TimeSelectionScrubberImpl extends React.PureComponent { - _handlers: ?{ - mouseMoveHandler: MouseHandler, - mouseUpHandler: MouseHandler, - }; - - _container: ?HTMLElement; - - state = { - hoverLocation: null, - }; - - _containerCreated = (element: HTMLElement | null) => { - this._container = element; - }; - - _onMouseDown = (event: SyntheticMouseEvent<>) => { - if (!this._container || event.button !== 0) { - return; - } - - const rect = getContentRect(this._container); - if ( - event.pageX < rect.left || - event.pageX >= rect.right || - event.pageY < rect.top || - event.pageY >= rect.bottom - ) { - return; - } - - // Don't steal focus. The -moz-user-focus: ignore declaration achieves - // this more reliably in Gecko, so this preventDefault is mostly for other - // browsers. - event.preventDefault(); - - const { rangeStart, rangeEnd, minSelectionStartWidth } = this.props; - const mouseDownTime = - (event.pageX - rect.left) / rect.width * (rangeEnd - rangeStart) + - rangeStart; - - let isRangeSelecting = false; - - const mouseMoveHandler = event => { - const mouseMoveTime = - (event.pageX - rect.left) / rect.width * (rangeEnd - rangeStart) + - rangeStart; - const selectionStart = clamp( - Math.min(mouseDownTime, mouseMoveTime), - rangeStart, - rangeEnd - ); - const selectionEnd = clamp( - Math.max(mouseDownTime, mouseMoveTime), - rangeStart, - rangeEnd - ); - if ( - isRangeSelecting || - selectionEnd - selectionStart >= minSelectionStartWidth - ) { - isRangeSelecting = true; - this.props.onSelectionChange({ - hasSelection: true, - selectionStart, - selectionEnd, - isModifying: true, - }); - } - }; - - const mouseUpHandler = event => { - if (isRangeSelecting) { - const mouseMoveTime = - (event.pageX - rect.left) / rect.width * (rangeEnd - rangeStart) + - rangeStart; - const selectionStart = clamp( - Math.min(mouseDownTime, mouseMoveTime), - rangeStart, - rangeEnd - ); - const selectionEnd = clamp( - Math.max(mouseDownTime, mouseMoveTime), - rangeStart, - rangeEnd - ); - this.props.onSelectionChange({ - hasSelection: true, - selectionStart, - selectionEnd, - isModifying: false, - }); - event.stopPropagation(); - this._uninstallMoveAndUpHandlers(); - return; - } - - const { selection } = this.props; - if (selection.hasSelection) { - const mouseUpTime = - (event.pageX - rect.left) / rect.width * (rangeEnd - rangeStart) + - rangeStart; - const { selectionStart, selectionEnd } = selection; - if (mouseUpTime < selectionStart || mouseUpTime >= selectionEnd) { - // Unset selection. - this.props.onSelectionChange({ - hasSelection: false, - isModifying: false, - }); - } - } - - // Do not stopPropagation(), so that graph gets mouseup event. - this._uninstallMoveAndUpHandlers(); - }; - - this._installMoveAndUpHandlers(mouseMoveHandler, mouseUpHandler); - }; - - _installMoveAndUpHandlers( - mouseMoveHandler: MouseHandler, - mouseUpHandler: MouseHandler - ) { - this._handlers = { mouseMoveHandler, mouseUpHandler }; - window.addEventListener('mousemove', mouseMoveHandler, true); - window.addEventListener('mouseup', mouseUpHandler, true); - } - - _uninstallMoveAndUpHandlers() { - if (this._handlers) { - const { mouseMoveHandler, mouseUpHandler } = this._handlers; - window.removeEventListener('mousemove', mouseMoveHandler, true); - window.removeEventListener('mouseup', mouseUpHandler, true); - } - } - - _onMouseMove = (event: SyntheticMouseEvent<>) => { - if (!this._container) { - return; - } - - const rect = getContentRect(this._container); - if ( - event.pageX < rect.left || - event.pageX >= rect.right || - event.pageY < rect.top || - event.pageY >= rect.bottom - ) { - this.setState({ hoverLocation: null }); - } else { - this.setState({ hoverLocation: event.pageX - rect.left }); - } - }; - - render() { - const { - className, - zeroAt, - rangeStart, - rangeEnd, - children, - selection, - width, - onSelectionChange, - onZoomButtonClick, - } = this.props; - - const { hoverLocation } = this.state; - - return ( -
- - {children} - {selection.hasSelection ? ( - - ) : null} -
-
- ); - } -} - -const TimeSelectionScrubber = withSize(TimeSelectionScrubberImpl); - -export default TimeSelectionScrubber; diff --git a/src/components/shared/Draggable.js b/src/components/shared/Draggable.js index 6e3d4e8bc9..a8af6958ad 100644 --- a/src/components/shared/Draggable.js +++ b/src/components/shared/Draggable.js @@ -8,14 +8,14 @@ import * as React from 'react'; import type { Milliseconds } from '../../types/units'; export type OnMove = ( - originalValue: { selectionEnd: Milliseconds, selectionStart: Milliseconds }, + originalValue: { +selectionEnd: Milliseconds, +selectionStart: Milliseconds }, dx: number, dy: number, isModifying: boolean ) => *; type Props = { - value: { selectionStart: Milliseconds, selectionEnd: Milliseconds }, + value: { +selectionStart: Milliseconds, +selectionEnd: Milliseconds }, onMove: OnMove, className: string, children?: React.Node, diff --git a/src/components/header/EmptyThreadIndicator.css b/src/components/timeline/EmptyThreadIndicator.css similarity index 82% rename from src/components/header/EmptyThreadIndicator.css rename to src/components/timeline/EmptyThreadIndicator.css index ae32fd830e..6bfea8d2b9 100644 --- a/src/components/header/EmptyThreadIndicator.css +++ b/src/components/timeline/EmptyThreadIndicator.css @@ -2,14 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.headerEmptyThreadIndicator { +.timelineEmptyThreadIndicator { --empty-buffer-color1: rgba(69, 160, 255, 0.225); --empty-buffer-color2: rgba(69, 160, 255, 0.075); /* The following color is based off grey-20, but uses opacity instead. */ --shutdown-startup-color: rgba(0, 0, 50, 0.07); } -.headerEmptyThreadIndicatorBlock { +.timelineEmptyThreadIndicatorBlock { position: absolute; top: 0; bottom: 0; @@ -17,19 +17,19 @@ box-sizing: border-box; } -.headerEmptyThreadIndicatorStartup { +.timelineEmptyThreadIndicatorStartup { border-right: solid 1px var(--grey-30); background-color: var(--shutdown-startup-color); box-shadow: inset -4px 0 4px rgba(0, 0, 0, 0.05); } -.headerEmptyThreadIndicatorShutdown { +.timelineEmptyThreadIndicatorShutdown { border-left: solid 1px var(--grey-30); background-color: var(--shutdown-startup-color); box-shadow: inset 4px 0 4px rgba(0, 0, 0, 0.05); } -.headerEmptyThreadIndicatorEmptyBuffer { +.timelineEmptyThreadIndicatorEmptyBuffer { background: repeating-linear-gradient( 45deg, var(--empty-buffer-color1), @@ -39,7 +39,7 @@ ); } -.headerEmptyThreadIndicatorLongTooltip { +.timelineEmptyThreadIndicatorLongTooltip { display: inline-block; max-width: 500px; } diff --git a/src/components/header/EmptyThreadIndicator.js b/src/components/timeline/EmptyThreadIndicator.js similarity index 91% rename from src/components/header/EmptyThreadIndicator.js rename to src/components/timeline/EmptyThreadIndicator.js index df9313d29b..57a366f19f 100644 --- a/src/components/header/EmptyThreadIndicator.js +++ b/src/components/timeline/EmptyThreadIndicator.js @@ -40,27 +40,27 @@ class EmptyThreadIndicator extends PureComponent { render() { const style = getIndicatorPositions(this.props); return ( -
+
{style.startup ? ( ) : null} {style.shutdown ? ( ) : null} {style.emptyBufferStart ? ( + {oneLine` This buffer was empty. Either the profiler was still initializing for a new thread, or the profiling buffer was full. Increase your buffer diff --git a/src/components/header/OverflowEdgeIndicator.css b/src/components/timeline/OverflowEdgeIndicator.css similarity index 100% rename from src/components/header/OverflowEdgeIndicator.css rename to src/components/timeline/OverflowEdgeIndicator.css diff --git a/src/components/header/OverflowEdgeIndicator.js b/src/components/timeline/OverflowEdgeIndicator.js similarity index 73% rename from src/components/header/OverflowEdgeIndicator.js rename to src/components/timeline/OverflowEdgeIndicator.js index 3a4da07b59..b83c029d51 100644 --- a/src/components/header/OverflowEdgeIndicator.js +++ b/src/components/timeline/OverflowEdgeIndicator.js @@ -3,8 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow -/* eslint-disable react/no-unused-state */ -// (See https://github.com/yannickcr/eslint-plugin-react/issues/1572) import * as React from 'react'; import classNames from 'classnames'; @@ -24,31 +22,27 @@ type State = { }; class OverflowEdgeIndicator extends React.PureComponent { - _containerCreated: (elem: HTMLDivElement | null) => void; - _container: HTMLDivElement | null; - _contentsWrapperCreated: (elem: HTMLDivElement | null) => void; - _contentsWrapper: HTMLDivElement | null; + _container: HTMLDivElement | null = null; + _contentsWrapper: HTMLDivElement | null = null; - constructor(props: Props) { - super(props); - this.state = { - overflowsOnTop: false, - overflowsOnRight: false, - overflowsOnBottom: false, - overflowsOnLeft: false, - }; - (this: any)._onScroll = this._onScroll.bind(this); - this._containerCreated = elem => { - this._container = elem; - }; - this._contentsWrapperCreated = elem => { - this._contentsWrapper = elem; - }; - } + state = { + overflowsOnTop: false, + overflowsOnRight: false, + overflowsOnBottom: false, + overflowsOnLeft: false, + }; + + _takeContainerRef = (element: HTMLDivElement | null) => { + this._container = element; + }; - _onScroll() { + _takeContainerWrapperRef = (element: HTMLDivElement | null) => { + this._contentsWrapper = element; + }; + + _onScroll = () => { this._updateIndicatorStatus(); - } + }; componentDidMount() { this._updateIndicatorStatus(); @@ -93,11 +87,11 @@ class OverflowEdgeIndicator extends React.PureComponent { `${className}Scrollbox` )} onScroll={this._onScroll} - ref={this._containerCreated} + ref={this._takeContainerRef} >
{children}
diff --git a/src/components/timeline/Ruler.css b/src/components/timeline/Ruler.css new file mode 100644 index 0000000000..757d2ad162 --- /dev/null +++ b/src/components/timeline/Ruler.css @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.timelineRuler { + height: 20px; + overflow: hidden; +} + +.timelineRuler::after { + content: ''; + position: absolute; + top: 20px; + left: -150px; + right: 0; + height: 1px; + background: var(--grey-30); + z-index: 3; +} + +.timelineRulerContainer { + overflow: hidden; + list-style: none; + margin: 0; + padding: 0; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + -moz-user-select: none; + user-select: none; + line-height: 20px; + font-size: 9px; + color: #888; + cursor: default; + z-index: 1; /* between .timelineThread background and .timelineThreadDetails */ +} + +.timelineRulerNotch { + display: block; + position: absolute; + top: 0; + bottom: 0; + width: 1px; + margin-left: -1px; + white-space: nowrap; + text-align: right; + background: linear-gradient(transparent, var(--grey-30) 19px, var(--grey-30) 20px, #d7d7db66 0); +} + +.timelineRulerNotchText { + position: absolute; + right: 0; + padding-right: 5px; +} diff --git a/src/components/header/TimeRuler.js b/src/components/timeline/Ruler.js similarity index 84% rename from src/components/header/TimeRuler.js rename to src/components/timeline/Ruler.js index ab14e03e17..3f807880af 100644 --- a/src/components/header/TimeRuler.js +++ b/src/components/timeline/Ruler.js @@ -5,18 +5,18 @@ // @flow import React, { PureComponent } from 'react'; +import './Ruler.css'; import type { Milliseconds, CssPixels } from '../../types/units'; type Props = {| - +className: string, +zeroAt: Milliseconds, +rangeStart: Milliseconds, +rangeEnd: Milliseconds, +width: CssPixels, |}; -class TimeRuler extends PureComponent { +class TimelineRuler extends PureComponent { _findNiceNumberGreaterOrEqualTo(uglyNumber: number) { // Write uglyNumber as a * 10^b, with 1 <= a < 10. // Return the lowest of 2 * 10^b, 5 * 10^b, 10 * 10^b that is greater or equal to uglyNumber. @@ -57,14 +57,17 @@ class TimeRuler extends PureComponent { } render() { - const { className } = this.props; const { notches, decimalPlaces } = this._getNotches(); return ( -
-
    +
    +
      {notches.map(({ time, pos }, i) => ( -
    1. - {`${time.toFixed( +
    2. + {`${time.toFixed( decimalPlaces )}s`}
    3. @@ -75,4 +78,4 @@ class TimeRuler extends PureComponent { } } -export default TimeRuler; +export default TimelineRuler; diff --git a/src/components/timeline/Selection.css b/src/components/timeline/Selection.css new file mode 100644 index 0000000000..8c577cd501 --- /dev/null +++ b/src/components/timeline/Selection.css @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.timelineSelection { + position: relative; + margin-left: 149px; + border-left: 1px solid var(--grey-30); + -moz-user-focus: ignore; +} + +.timelineSelectionHoverLine { + position: absolute; + pointer-events: none; + visibility: hidden; + top: 0; + bottom: 0; + width: 1px; + background: rgba(0,0,0,0.4); + z-index: 1; +} + +.timelineSelection:hover > .timelineSelectionHoverLine { + visibility: visible; +} + +.timelineSelectionOverlay { + position: absolute; + z-index: 2; + display: flex; + flex-flow: row nowrap; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + margin-left: -5px; + padding-left: 5px; + overflow: hidden; +} + +.timelineSelectionDimmerBefore, +.timelineSelectionDimmerAfter { + background: rgba(12, 12, 13, .1); + flex-shrink: 0; +} + +.timelineSelectionDimmerAfter { + flex: 1; +} + +.timelineSelectionOverlayWrapper { + display: flex; + flex-flow: column nowrap; +} + +.timelineSelectionGrippy { + height: 20px; + pointer-events: auto; + display: flex; + flex-flow: row nowrap; +} + +.timelineSelectionGrippyRangeStart, +.timelineSelectionGrippyRangeEnd { + width: 0px; + padding: 3px; + background: #AAA; + border: 1px solid white; + margin: 0 -4px; + cursor: ew-resize; + border-radius: 5px; + position: relative; + z-index: 3; +} + +.timelineSelectionGrippyRangeStart:hover, +.timelineSelectionGrippyRangeStart.dragging, +.timelineSelectionGrippyRangeEnd:hover, +.timelineSelectionGrippyRangeEnd.dragging { + background: #888; +} + +.timelineSelectionGrippyMoveRange { + flex: 1; + cursor: -webkit-grab; + cursor: grab; +} + +.timelineSelectionGrippyMoveRange.dragging { + cursor: -webkit-grabbing; + cursor: grabbing; +} + + +.timelineSelectionOverlayInner { + flex: 1; + justify-content: center; + align-items: center; + display: flex; + min-width: 0; + min-height: 0; +} + +.timelineSelectionOverlayRange { + top: 20px; + position: absolute; + padding: 4px 8px; + color: #fff; + background-color: var(--blue-50); + border-radius: 0 0 4px 4px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + pointer-events: none; + opacity: 1; + transition: opacity 200ms; +} + +.timelineSelectionOverlayRange.hidden { + opacity: 0; +} + +.timelineSelectionOverlayZoomButton { + width: 30px; + height: 30px; + pointer-events: auto; + box-sizing: border-box; + border-radius: 100%; + margin: -15px; + position: relative; + border: 1px solid rgba(0, 0, 0, 0.2); + background: url(../../../res/img/svg/zoom-icon.svg) center center no-repeat rgba(255, 255, 255, 0.6); + transition: opacity 200ms ease-in-out; + will-change: opacity; + opacity: 0.5; +} + +.timelineSelectionOverlayZoomButton.hidden { + opacity: 0.0 !important; + pointer-events: none; + transition: unset; +} + +.timelineSelection:hover .timelineSelectionOverlayZoomButton, +.timelineSelectionOverlayZoomButton:active { + opacity: 1.0; +} + +.timelineSelectionOverlayZoomButton:hover { + background-color: rgba(255, 255, 255, 0.9); +} + +.timelineSelectionOverlayZoomButton:active:hover { + background-color: rgba(160, 160, 160, 0.6); +} diff --git a/src/components/timeline/Selection.js b/src/components/timeline/Selection.js new file mode 100644 index 0000000000..d211ec6a81 --- /dev/null +++ b/src/components/timeline/Selection.js @@ -0,0 +1,389 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +import * as React from 'react'; +import clamp from 'clamp'; +import { getContentRect } from '../../utils/css-geometry-tools'; +import { + getProfileInterval, + getProfileViewOptions, + getDisplayRange, + getZeroAt, +} from '../../reducers/profile-view'; +import { + updateProfileSelection, + addRangeFilterAndUnsetSelection, +} from '../../actions/profile-view'; +import explicitConnect from '../../utils/connect'; +import classNames from 'classnames'; +import Draggable from '../shared/Draggable'; +import { getFormattedTimeLength } from '../../profile-logic/range-filters'; +import './Selection.css'; + +import type { OnMove } from '../shared/Draggable'; +import type { Milliseconds, CssPixels, StartEndRange } from '../../types/units'; +import type { ProfileSelection } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + +type MouseHandler = (event: MouseEvent) => void; + +type OwnProps = {| + +width: number, + +children: React.Node, +|}; + +type StateProps = {| + +selection: ProfileSelection, + +displayRange: StartEndRange, + +zeroAt: Milliseconds, + +minSelectionStartWidth: Milliseconds, +|}; + +type DispatchProps = {| + +addRangeFilterAndUnsetSelection: typeof addRangeFilterAndUnsetSelection, + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type Props = ConnectedProps; + +type State = {| + hoverLocation: null | CssPixels, +|}; + +class TimelineRulerAndSelection extends React.PureComponent { + _handlers: ?{ + mouseMoveHandler: MouseHandler, + mouseUpHandler: MouseHandler, + }; + + _container: ?HTMLElement; + _rangeStartOnMove: OnMove; + _moveRangeOnMove: OnMove; + _rangeEndOnMove: OnMove; + + state = { + hoverLocation: null, + }; + + _containerCreated = (element: HTMLElement | null) => { + this._container = element; + }; + + _onMouseDown = (event: SyntheticMouseEvent<>) => { + if (!this._container || event.button !== 0) { + return; + } + + const rect = getContentRect(this._container); + if ( + event.pageX < rect.left || + event.pageX >= rect.right || + event.pageY < rect.top || + event.pageY >= rect.bottom + ) { + return; + } + + // Don't steal focus. The -moz-user-focus: ignore declaration achieves + // this more reliably in Gecko, so this preventDefault is mostly for other + // browsers. + event.preventDefault(); + + const { displayRange, minSelectionStartWidth } = this.props; + const mouseDownTime = + (event.pageX - rect.left) / + rect.width * + (displayRange.end - displayRange.start) + + displayRange.start; + + let isRangeSelecting = false; + + const mouseMoveHandler = event => { + const mouseMoveTime = + (event.pageX - rect.left) / + rect.width * + (displayRange.end - displayRange.start) + + displayRange.start; + const selectionStart = clamp( + Math.min(mouseDownTime, mouseMoveTime), + displayRange.start, + displayRange.end + ); + const selectionEnd = clamp( + Math.max(mouseDownTime, mouseMoveTime), + displayRange.start, + displayRange.end + ); + if ( + isRangeSelecting || + selectionEnd - selectionStart >= minSelectionStartWidth + ) { + isRangeSelecting = true; + this.props.updateProfileSelection({ + hasSelection: true, + selectionStart, + selectionEnd, + isModifying: true, + }); + } + }; + + const mouseUpHandler = event => { + if (isRangeSelecting) { + const mouseMoveTime = + (event.pageX - rect.left) / + rect.width * + (displayRange.end - displayRange.start) + + displayRange.start; + const selectionStart = clamp( + Math.min(mouseDownTime, mouseMoveTime), + displayRange.start, + displayRange.end + ); + const selectionEnd = clamp( + Math.max(mouseDownTime, mouseMoveTime), + displayRange.start, + displayRange.end + ); + this.props.updateProfileSelection({ + hasSelection: true, + selectionStart, + selectionEnd, + isModifying: false, + }); + event.stopPropagation(); + this._uninstallMoveAndUpHandlers(); + return; + } + + const { selection } = this.props; + if (selection.hasSelection) { + const mouseUpTime = + (event.pageX - rect.left) / + rect.width * + (displayRange.end - displayRange.start) + + displayRange.start; + const { selectionStart, selectionEnd } = selection; + if (mouseUpTime < selectionStart || mouseUpTime >= selectionEnd) { + // Unset selection. + this.props.updateProfileSelection({ + hasSelection: false, + isModifying: false, + }); + } + } + + // Do not stopPropagation(), so that graph gets mouseup event. + this._uninstallMoveAndUpHandlers(); + }; + + this._installMoveAndUpHandlers(mouseMoveHandler, mouseUpHandler); + }; + + _installMoveAndUpHandlers( + mouseMoveHandler: MouseHandler, + mouseUpHandler: MouseHandler + ) { + this._handlers = { mouseMoveHandler, mouseUpHandler }; + window.addEventListener('mousemove', mouseMoveHandler, true); + window.addEventListener('mouseup', mouseUpHandler, true); + } + + _uninstallMoveAndUpHandlers() { + if (this._handlers) { + const { mouseMoveHandler, mouseUpHandler } = this._handlers; + window.removeEventListener('mousemove', mouseMoveHandler, true); + window.removeEventListener('mouseup', mouseUpHandler, true); + } + } + + _onMouseMove = (event: SyntheticMouseEvent<>) => { + if (!this._container) { + return; + } + + const rect = getContentRect(this._container); + if ( + event.pageX < rect.left || + event.pageX >= rect.right || + event.pageY < rect.top || + event.pageY >= rect.bottom + ) { + this.setState({ hoverLocation: null }); + } else { + this.setState({ hoverLocation: event.pageX - rect.left }); + } + }; + + _makeOnMove = (fun: number => { startDelta: number, endDelta: number }) => ( + originalSelection: { +selectionStart: number, +selectionEnd: number }, + dx: number, + dy: number, + isModifying: boolean + ) => { + const { displayRange, width, updateProfileSelection } = this.props; + const delta = dx / width * (displayRange.end - displayRange.start); + const selectionDeltas = fun(delta); + const selectionStart = Math.max( + displayRange.start, + originalSelection.selectionStart + selectionDeltas.startDelta + ); + const selectionEnd = clamp( + originalSelection.selectionEnd + selectionDeltas.endDelta, + selectionStart, + displayRange.end + ); + updateProfileSelection({ + hasSelection: true, + isModifying, + selectionStart, + selectionEnd, + }); + }; + + _rangeStartOnMove = this._makeOnMove(delta => ({ + startDelta: delta, + endDelta: 0, + })); + + _moveRangeOnMove = this._makeOnMove(delta => ({ + startDelta: delta, + endDelta: delta, + })); + + _rangeEndOnMove = this._makeOnMove(delta => ({ + startDelta: 0, + endDelta: delta, + })); + + _zoomButtonOnMouseDown = (e: SyntheticMouseEvent<>) => { + e.stopPropagation(); + }; + + _zoomButtonOnClick = (e: SyntheticMouseEvent<>) => { + e.stopPropagation(); + const { selection, zeroAt, addRangeFilterAndUnsetSelection } = this.props; + if (selection.hasSelection) { + addRangeFilterAndUnsetSelection( + selection.selectionStart - zeroAt, + selection.selectionEnd - zeroAt + ); + } + }; + + renderSelectionOverlay(selection: { + +selectionStart: number, + +selectionEnd: number, + +isModifying: boolean, + }) { + const { displayRange, width } = this.props; + const { selectionStart, selectionEnd } = selection; + + const beforeWidth = + (selectionStart - displayRange.start) / + (displayRange.end - displayRange.start) * + width; + const selectionWidth = + (selectionEnd - selectionStart) / + (displayRange.end - displayRange.start) * + width; + + return ( +
      +
      +
      +
      + + + +
      +
      + + {getFormattedTimeLength(selectionEnd - selectionStart)} + +
      +
      +
      +
      + ); + } + + render() { + const { children, selection } = this.props; + const { hoverLocation } = this.state; + + return ( +
      + {children} + {selection.hasSelection ? this.renderSelectionOverlay(selection) : null} +
      +
      + ); + } +} + +const options: ExplicitConnectOptions = { + mapStateToProps: state => ({ + selection: getProfileViewOptions(state).selection, + displayRange: getDisplayRange(state), + zeroAt: getZeroAt(state), + minSelectionStartWidth: getProfileInterval(state), + }), + mapDispatchToProps: { + updateProfileSelection, + addRangeFilterAndUnsetSelection, + }, + component: TimelineRulerAndSelection, +}; + +export default explicitConnect(options); diff --git a/src/components/timeline/StackGraph.css b/src/components/timeline/StackGraph.css new file mode 100644 index 0000000000..20f1bfc223 --- /dev/null +++ b/src/components/timeline/StackGraph.css @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.timelineStackGraph { + height: 25px; +} + +.timelineStackGraphCanvas { + display: block; + height: 25px; + width: 100%; +} diff --git a/src/components/header/ThreadStackGraph.js b/src/components/timeline/StackGraph.js similarity index 91% rename from src/components/header/ThreadStackGraph.js rename to src/components/timeline/StackGraph.js index b5db23a2b6..fcbb22fff9 100644 --- a/src/components/header/ThreadStackGraph.js +++ b/src/components/timeline/StackGraph.js @@ -4,11 +4,11 @@ // @flow import React, { PureComponent } from 'react'; -import classNames from 'classnames'; import bisection from 'bisection'; import { timeCode } from '../../utils/time-code'; import { getSampleCallNodes } from '../../profile-logic/profile-data'; import { BLUE_70, BLUE_40 } from 'photon-colors'; +import './StackGraph.css'; import type { Thread } from '../../types/profile'; import type { Milliseconds } from '../../types/units'; @@ -24,23 +24,16 @@ type Props = {| +rangeEnd: Milliseconds, +callNodeInfo: CallNodeInfo, +selectedCallNodeIndex: IndexIntoCallNodeTable | null, - +className: string, +onStackClick: (time: Milliseconds) => void, |}; -class ThreadStackGraph extends PureComponent { - _canvas: null | HTMLCanvasElement; - _requestedAnimationFrame: boolean; +class StackGraph extends PureComponent { + _canvas: null | HTMLCanvasElement = null; + _requestedAnimationFrame: boolean = false; _resizeListener: () => void; _takeCanvasRef = (canvas: HTMLCanvasElement | null) => (this._canvas = canvas); - - constructor(props: Props) { - super(props); - this._resizeListener = () => this.forceUpdate(); - this._requestedAnimationFrame = false; - this._canvas = null; - } + _resizeListener = () => this.forceUpdate(); _scheduleDraw() { if (!this._requestedAnimationFrame) { @@ -49,7 +42,7 @@ class ThreadStackGraph extends PureComponent { this._requestedAnimationFrame = false; const canvas = this._canvas; if (canvas) { - timeCode('ThreadStackGraph render', () => { + timeCode('StackGraph render', () => { this.drawCanvas(canvas); }); } @@ -204,12 +197,9 @@ class ThreadStackGraph extends PureComponent { render() { this._scheduleDraw(); return ( -
      +
      @@ -218,4 +208,4 @@ class ThreadStackGraph extends PureComponent { } } -export default ThreadStackGraph; +export default StackGraph; diff --git a/src/components/timeline/Thread.css b/src/components/timeline/Thread.css new file mode 100644 index 0000000000..9ebdece28e --- /dev/null +++ b/src/components/timeline/Thread.css @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.timelineThread { + margin: 0; + padding: 0; + display: flex; + flex-flow: row nowrap; + border-top: 1px solid var(--grey-30); + box-shadow: 0 1px var(--grey-30); +} + +.timelineThread.selected { + background-color: #edf6ff; +} + +.timelineThreadHidden { + height: 0; + pointer-events: none; +} + +.timelineThreadLabel { + box-sizing: border-box; + width: 150px; + border-right: 1px solid var(--grey-30); + padding-left: 14px; + cursor: default; + display: flex; + flex-flow: row nowrap; + align-items: center; +} + +.timelineThreadName { + font-weight: normal; + font: message-box; + font-size: 100%; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.timelineThreadDetails { + flex: 1; + position: relative; + z-index: 1; + display: flex; + flex-flow: column nowrap; +} + +.timelineThreadIntervalMarkerOverview { + list-style: none; + display: block; + margin: 0; + height: 6px; + position: relative; + overflow: hidden; + opacity: 0.75; +} + +.timelineThreadIntervalMarkerOverviewContainerJank { + border-bottom: 1px solid var(--grey-30); +} + +.timelineThreadIntervalMarkerOverview.selected { + opacity: 1; +} + +.timelineThreadIntervalMarkerOverviewThreadGeckoMain { + height: 18px; +} diff --git a/src/components/header/ProfileThreadHeaderBar.js b/src/components/timeline/Thread.js similarity index 80% rename from src/components/header/ProfileThreadHeaderBar.js rename to src/components/timeline/Thread.js index 88f629dd92..6af2c94500 100644 --- a/src/components/header/ProfileThreadHeaderBar.js +++ b/src/components/timeline/Thread.js @@ -6,7 +6,7 @@ import React, { PureComponent } from 'react'; import explicitConnect from '../../utils/connect'; -import ThreadStackGraph from './ThreadStackGraph'; +import StackGraph from './StackGraph'; import { selectorsForThread } from '../../reducers/profile-view'; import { getSelectedThreadIndex } from '../../reducers/url-state'; import { @@ -14,8 +14,10 @@ import { getCallNodePathFromIndex, } from '../../profile-logic/profile-data'; import ContextMenuTrigger from '../shared/ContextMenuTrigger'; -import ProfileThreadJankOverview from './ProfileThreadJankOverview'; -import ProfileThreadTracingMarkerOverview from './ProfileThreadTracingMarkerOverview'; +import { + TimelineTracingMarkersJank, + TimelineTracingMarkersOverview, +} from './TracingMarkers'; import { changeSelectedThread, updateProfileSelection, @@ -24,6 +26,7 @@ import { focusCallTree, } from '../../actions/profile-view'; import EmptyThreadIndicator from './EmptyThreadIndicator'; +import './Thread.css'; import type { Thread, ThreadIndex } from '../../types/profile'; import type { Milliseconds, StartEndRange } from '../../types/units'; @@ -67,18 +70,8 @@ type DispatchProps = {| type Props = ConnectedProps; -class ProfileThreadHeaderBar extends PureComponent { - constructor(props) { - super(props); - (this: any)._onLabelMouseDown = this._onLabelMouseDown.bind(this); - (this: any)._onStackClick = this._onStackClick.bind(this); - (this: any)._onLineClick = this._onLineClick.bind(this); - (this: any)._onIntervalMarkerSelect = this._onIntervalMarkerSelect.bind( - this - ); - } - - _onLabelMouseDown(event: MouseEvent) { +class TimelineThread extends PureComponent { + _onLabelMouseDown = (event: MouseEvent) => { const { changeSelectedThread, changeRightClickedThread, @@ -94,14 +87,14 @@ class ProfileThreadHeaderBar extends PureComponent { // actually changing the current selection. changeRightClickedThread(threadIndex); } - } + }; - _onLineClick() { + _onLineClick = () => { const { threadIndex, changeSelectedThread } = this.props; changeSelectedThread(threadIndex); - } + }; - _onStackClick(time: number) { + _onStackClick = (time: number) => { const { threadIndex, interval } = this.props; const { thread, @@ -124,13 +117,13 @@ class ProfileThreadHeaderBar extends PureComponent { getCallNodePathFromIndex(newSelectedCallNode, callNodeInfo.callNodeTable) ); focusCallTree(); - } + }; - _onIntervalMarkerSelect( + _onIntervalMarkerSelect = ( threadIndex: ThreadIndex, start: Milliseconds, end: Milliseconds - ) { + ) => { const { rangeStart, rangeEnd, @@ -144,7 +137,7 @@ class ProfileThreadHeaderBar extends PureComponent { selectionEnd: Math.min(rangeEnd, end), }); changeSelectedThread(threadIndex); - } + }; render() { const { @@ -167,7 +160,7 @@ class ProfileThreadHeaderBar extends PureComponent { if (isHidden) { // If this thread is hidden, render out a stub element so that the Reorderable // Component still works across all the threads. - return
    4. ; + return
    5. ; } const processType = thread.processType; @@ -177,29 +170,28 @@ class ProfileThreadHeaderBar extends PureComponent { thread.name === 'Compositor' || thread.name === 'Renderer') && processType !== 'plugin'; - const className = 'profileThreadHeaderBar'; return (
    6. -

      {threadName}

      +

      {threadName}

      -
      +
      {displayJank ? ( - { /> ) : null} {displayTracingMarkers ? ( - { isModifyingSelection={isModifyingSelection} /> ) : null} - = { changeSelectedCallNode, focusCallTree, }, - component: ProfileThreadHeaderBar, + component: TimelineThread, }; export default explicitConnect(options); diff --git a/src/components/header/ProfileThreadHeaderContextMenu.js b/src/components/timeline/ThreadContextMenu.js similarity index 95% rename from src/components/header/ProfileThreadHeaderContextMenu.js rename to src/components/timeline/ThreadContextMenu.js index afe11c361d..cd0c0d2660 100644 --- a/src/components/header/ProfileThreadHeaderContextMenu.js +++ b/src/components/timeline/ThreadContextMenu.js @@ -41,7 +41,7 @@ type DispatchProps = {| type Props = ConnectedProps<{||}, StateProps, DispatchProps>; -class ProfileThreadHeaderContextMenu extends PureComponent { +class TimelineThreadContextMenu extends PureComponent { constructor(props: Props) { super(props); (this: any)._toggleThreadVisibility = this._toggleThreadVisibility.bind( @@ -84,7 +84,7 @@ class ProfileThreadHeaderContextMenu extends PureComponent { ); return ( - + {threads.length <= 1 ? null : (
      = { rightClickedThreadIndex: getRightClickedThreadIndex(state), }), mapDispatchToProps: { hideThread, showThread, isolateThread }, - component: ProfileThreadHeaderContextMenu, + component: TimelineThreadContextMenu, }; export default explicitConnect(options); diff --git a/src/components/timeline/TracingMarkers.css b/src/components/timeline/TracingMarkers.css new file mode 100644 index 0000000000..ce8ca291e7 --- /dev/null +++ b/src/components/timeline/TracingMarkers.css @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.timelineTracingMarkersCanvas { + display: block; + width: 100%; + height: 100%; +} diff --git a/src/components/header/IntervalMarkerOverview.js b/src/components/timeline/TracingMarkers.js similarity index 73% rename from src/components/header/IntervalMarkerOverview.js rename to src/components/timeline/TracingMarkers.js index ce3484f8bc..d456105a21 100644 --- a/src/components/header/IntervalMarkerOverview.js +++ b/src/components/timeline/TracingMarkers.js @@ -9,16 +9,48 @@ import { timeCode } from '../../utils/time-code'; import { withSize } from '../shared/WithSize'; import Tooltip from '../shared/Tooltip'; import MarkerTooltipContents from '../shared/MarkerTooltipContents'; +import { + styles, + overlayFills, +} from '../../profile-logic/interval-marker-styles'; +import explicitConnect from '../../utils/connect'; +import { selectorsForThread } from '../../reducers/profile-view'; +import { getSelectedThreadIndex } from '../../reducers/url-state'; +import './TracingMarkers.css'; import type { Milliseconds, CssPixels } from '../../types/units'; import type { TracingMarker } from '../../types/profile-derived'; import type { SizeProps } from '../shared/WithSize'; -import type { ConnectedProps } from '../../utils/connect'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; import type { ThreadIndex } from '../../types/profile'; type MarkerState = 'PRESSED' | 'HOVERED' | 'NONE'; -// Typically this component is wrapped in a connect function, but in other files. +/** + * The TimelineTracingMarkers component is built up of several nested components, + * and they are all collected in this file. In pseudo-code, they take + * the following forms: + * + * export const TimelineTracingMarkersJank = ( + * + * + * + * + * + * ); + * + * export const TimelineTracingMarkersOverview = ( + * + * + * + * + * + * ); + */ + export type OwnProps = {| +className: string, +rangeStart: Milliseconds, @@ -47,29 +79,22 @@ type State = { mouseY: CssPixels, }; -class IntervalMarkerOverview extends React.PureComponent { - _canvas: HTMLCanvasElement | null; - _requestedAnimationFrame: boolean | null; +class TimelineTracingMarkersImplementation extends React.PureComponent< + Props, + State +> { + _canvas: HTMLCanvasElement | null = null; + _requestedAnimationFrame: boolean = false; + state = { + hoveredItem: null, + mouseDownItem: null, + mouseX: 0, + mouseY: 0, + }; - constructor(props: Props) { - super(props); - this.state = { - hoveredItem: null, - mouseDownItem: null, - mouseX: 0, - mouseY: 0, - }; - (this: any)._onMouseDown = this._onMouseDown.bind(this); - (this: any)._onMouseMove = this._onMouseMove.bind(this); - (this: any)._onMouseUp = this._onMouseUp.bind(this); - (this: any)._onMouseOut = this._onMouseOut.bind(this); - (this: any)._takeCanvasRef = this._takeCanvasRef.bind(this); - this._canvas = null; - } - - _takeCanvasRef(c: HTMLCanvasElement | null) { + _takeCanvasRef = (c: HTMLCanvasElement | null) => { this._canvas = c; - } + }; _scheduleDraw() { window.requestAnimationFrame(() => { @@ -111,7 +136,7 @@ class IntervalMarkerOverview extends React.PureComponent { return null; } - _onMouseMove(event: SyntheticMouseEvent<>) { + _onMouseMove = (event: SyntheticMouseEvent<>) => { const hoveredItem = this._hitTest(event); if (hoveredItem !== null) { this.setState({ @@ -124,9 +149,9 @@ class IntervalMarkerOverview extends React.PureComponent { hoveredItem: null, }); } - } + }; - _onMouseDown(e) { + _onMouseDown = e => { const mouseDownItem = this._hitTest(e); this.setState({ mouseDownItem }); if (mouseDownItem !== null) { @@ -135,9 +160,9 @@ class IntervalMarkerOverview extends React.PureComponent { } e.stopPropagation(); } - } + }; - _onMouseUp(e) { + _onMouseUp = e => { const { mouseDownItem } = this.state; if (mouseDownItem !== null) { const mouseUpItem = this._hitTest(e); @@ -158,13 +183,13 @@ class IntervalMarkerOverview extends React.PureComponent { mouseDownItem: null, }); } - } + }; - _onMouseOut() { + _onMouseOut = () => { this.setState({ hoveredItem: null, }); - } + }; componentDidUpdate(prevProps: Props, prevState: State) { if ( @@ -185,18 +210,11 @@ class IntervalMarkerOverview extends React.PureComponent { const { mouseDownItem, hoveredItem, mouseX, mouseY } = this.state; const shouldShowTooltip = !isModifyingSelection && !mouseDownItem; - const canvasClassName = className - .split(' ') - .map(name => `${name}Canvas`) - .join(' '); return (
      { } } -export default withSize(IntervalMarkerOverview); +/** + * Combine the base implementation of the TimelineTracingMarkers with the + * WithSize component. + */ +export const TimelineTracingMarkers = withSize( + TimelineTracingMarkersImplementation +); + +/** + * Create a special connected component for Jank instances. + */ +const jankOptions: ExplicitConnectOptions = { + mapStateToProps: (state, props) => { + const { threadIndex } = props; + const selectors = selectorsForThread(threadIndex); + const selectedThread = getSelectedThreadIndex(state); + + return { + intervalMarkers: selectors.getJankInstances(state), + isSelected: threadIndex === selectedThread, + styles: styles, + overlayFills: overlayFills, + }; + }, + component: TimelineTracingMarkers, +}; + +export const TimelineTracingMarkersJank = explicitConnect(jankOptions); + +/** + * Create a connected component for all tracing markers. + */ +const tracingOptions: ExplicitConnectOptions = { + mapStateToProps: (state, props) => { + const { threadIndex } = props; + const selectors = selectorsForThread(threadIndex); + const selectedThread = getSelectedThreadIndex(state); + const intervalMarkers = selectors.getRangeSelectionFilteredTracingMarkersForHeader( + state + ); + return { + intervalMarkers, + isSelected: threadIndex === selectedThread, + styles, + overlayFills, + }; + }, + component: TimelineTracingMarkers, +}; + +export const TimelineTracingMarkersOverview = explicitConnect(tracingOptions); diff --git a/src/components/timeline/index.css b/src/components/timeline/index.css new file mode 100644 index 0000000000..b6de3d3a2c --- /dev/null +++ b/src/components/timeline/index.css @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +.timelineOverflowEdgeIndicatorScrollbox { + margin: 0 0 0 -150px; + padding-left: 150px; + max-height: 250px; + overflow: auto; +} + +.timelineThreadList { + list-style: none; + margin: 0 0 0 -150px; + padding: 0; + box-shadow: inset 0 1px var(--grey-30); +} diff --git a/src/components/header/ProfileViewerHeader.js b/src/components/timeline/index.js similarity index 67% rename from src/components/header/ProfileViewerHeader.js rename to src/components/timeline/index.js index 0fceb10bc4..a4e03303f4 100644 --- a/src/components/header/ProfileViewerHeader.js +++ b/src/components/timeline/index.js @@ -5,10 +5,12 @@ // @flow import React, { PureComponent } from 'react'; -import ProfileThreadHeaderBar from './ProfileThreadHeaderBar'; -import Reorderable from '../shared/Reorderable'; -import TimeSelectionScrubber from './TimeSelectionScrubber'; +import TimelineThread from './Thread'; +import TimelineRuler from './Ruler'; +import TimelineSelection from './Selection'; import OverflowEdgeIndicator from './OverflowEdgeIndicator'; +import Reorderable from '../shared/Reorderable'; +import { withSize } from '../shared/WithSize'; import explicitConnect from '../../utils/connect'; import { getProfile, @@ -17,6 +19,9 @@ import { getZeroAt, } from '../../reducers/profile-view'; import { getHiddenThreads, getThreadOrder } from '../../reducers/url-state'; +import './index.css'; + +import type { SizeProps } from '../shared/WithSize'; import { changeThreadOrder, @@ -32,8 +37,11 @@ import type { ConnectedProps, } from '../../utils/connect'; +type OwnProps = SizeProps; + type StateProps = {| +profile: Profile, + +displayRange: StartEndRange, +selection: ProfileSelection, +threadOrder: ThreadIndex[], +hiddenThreads: ThreadIndex[], @@ -47,19 +55,9 @@ type DispatchProps = {| +updateProfileSelection: typeof updateProfileSelection, |}; -type Props = ConnectedProps<{||}, StateProps, DispatchProps>; - -class ProfileViewerHeader extends PureComponent { - constructor(props: Props) { - super(props); - (this: any)._onZoomButtonClick = this._onZoomButtonClick.bind(this); - } - - _onZoomButtonClick(start: Milliseconds, end: Milliseconds) { - const { addRangeFilterAndUnsetSelection, zeroAt } = this.props; - addRangeFilterAndUnsetSelection(start - zeroAt, end - zeroAt); - } +type Props = ConnectedProps; +class Timeline extends PureComponent { render() { const { profile, @@ -67,34 +65,31 @@ class ProfileViewerHeader extends PureComponent { changeThreadOrder, selection, timeRange, - zeroAt, hiddenThreads, - updateProfileSelection, + displayRange, + zeroAt, + width, } = this.props; const threads = profile.threads; - return ( - - + + + { {threads.map((thread, threadIndex) => ( - { } - + ); } } -const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { +const options: ExplicitConnectOptions = { mapStateToProps: state => ({ profile: getProfile(state), selection: getProfileViewOptions(state).selection, threadOrder: getThreadOrder(state), hiddenThreads: getHiddenThreads(state), timeRange: getDisplayRange(state), + displayRange: getDisplayRange(state), zeroAt: getZeroAt(state), }), mapDispatchToProps: { @@ -126,7 +122,6 @@ const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { updateProfileSelection, addRangeFilterAndUnsetSelection, }, - component: ProfileViewerHeader, + component: Timeline, }; - -export default explicitConnect(options); +export default withSize(explicitConnect(options)); diff --git a/src/test/components/EmptyThreadIndicator.test.js b/src/test/components/EmptyThreadIndicator.test.js index fe1d97fa0a..892d176328 100644 --- a/src/test/components/EmptyThreadIndicator.test.js +++ b/src/test/components/EmptyThreadIndicator.test.js @@ -11,7 +11,7 @@ import renderer from 'react-test-renderer'; import EmptyThreadIndicator, { getIndicatorPositions, -} from '../../components/header/EmptyThreadIndicator'; +} from '../../components/timeline/EmptyThreadIndicator'; import { getProfileFromTextSamples } from '../fixtures/profiles/make-profile'; import { getBoundingBox } from '../fixtures/utils'; import mockRaf from '../fixtures/mocks/request-animation-frame'; diff --git a/src/test/components/ProfileViewerHeader.test.js b/src/test/components/Timeline.test.js similarity index 95% rename from src/test/components/ProfileViewerHeader.test.js rename to src/test/components/Timeline.test.js index 1bc423801e..7de5c48645 100644 --- a/src/test/components/ProfileViewerHeader.test.js +++ b/src/test/components/Timeline.test.js @@ -4,7 +4,7 @@ // @flow import * as React from 'react'; -import ProfileViewerHeader from '../../components/header/ProfileViewerHeader'; +import Timeline from '../../components/timeline'; import renderer from 'react-test-renderer'; import { Provider } from 'react-redux'; import { storeWithProfile } from '../fixtures/stores'; @@ -63,7 +63,7 @@ function _getProfileWithDroppedSamples(): Profile { return profile; } -describe('calltree/ProfileViewerHeader', function() { +describe('Timeline', function() { beforeEach(() => { jest.spyOn(ReactDOM, 'findDOMNode').mockImplementation(() => { // findDOMNode uses nominal typing instead of structural (null | Element | Text), so @@ -97,7 +97,7 @@ describe('calltree/ProfileViewerHeader', function() { const header = renderer.create( - + , { createNodeMock } ); diff --git a/src/test/components/ProfileThreadTracingMarkerOverview.test.js b/src/test/components/TimelineTracingMarkersOverview.test.js similarity index 90% rename from src/test/components/ProfileThreadTracingMarkerOverview.test.js rename to src/test/components/TimelineTracingMarkersOverview.test.js index 49b957096f..f7db2bb068 100644 --- a/src/test/components/ProfileThreadTracingMarkerOverview.test.js +++ b/src/test/components/TimelineTracingMarkersOverview.test.js @@ -4,7 +4,7 @@ // @flow import * as React from 'react'; -import ProfileThreadTrackingMarkerOverview from '../../components/header/ProfileThreadTracingMarkerOverview'; +import { TimelineTracingMarkersOverview } from '../../components/timeline/TracingMarkers'; import renderer from 'react-test-renderer'; import { Provider } from 'react-redux'; import mockCanvasContext from '../fixtures/mocks/canvas-context'; @@ -14,7 +14,7 @@ import ReactDOM from 'react-dom'; import { getBoundingBox } from '../fixtures/utils'; import mockRaf from '../fixtures/mocks/request-animation-frame'; -describe('ProfileThreadTracingMarkerOverview', function() { +describe('TimelineTracingMarkersOverview', function() { beforeEach(() => { jest.spyOn(ReactDOM, 'findDOMNode').mockImplementation(() => { // findDOMNode uses nominal typing instead of structural (null | Element | Text), so @@ -60,8 +60,8 @@ describe('ProfileThreadTracingMarkerOverview', function() { const overview = renderer.create( - - +
      `; @@ -106,7 +106,7 @@ exports[`app/Details renders an initial view with the right panel for tab calltr - +
      `; @@ -161,7 +161,7 @@ exports[`app/Details renders an initial view with the right panel for tab flame- - +
      `; @@ -216,7 +216,7 @@ exports[`app/Details renders an initial view with the right panel for tab marker - +
      `; @@ -271,7 +271,7 @@ exports[`app/Details renders an initial view with the right panel for tab marker - +
    7. `; @@ -326,7 +326,7 @@ exports[`app/Details renders an initial view with the right panel for tab networ - +
      `; @@ -381,7 +381,7 @@ exports[`app/Details renders an initial view with the right panel for tab stack- - +
      `; @@ -436,7 +436,7 @@ exports[`app/Details show the correct state for the sidebar open button 1`] = ` - +
      `; @@ -491,7 +491,7 @@ exports[`app/Details show the correct state for the sidebar open button 2`] = ` - +
      `; @@ -546,7 +546,7 @@ exports[`app/Details show the correct state for the sidebar open button 3`] = ` - +
    `; @@ -601,6 +601,6 @@ exports[`app/Details show the correct state for the sidebar open button 4`] = ` - +
`; diff --git a/src/test/components/__snapshots__/EmptyThreadIndicator.test.js.snap b/src/test/components/__snapshots__/EmptyThreadIndicator.test.js.snap index f9e3225667..e25c60f9d6 100644 --- a/src/test/components/__snapshots__/EmptyThreadIndicator.test.js.snap +++ b/src/test/components/__snapshots__/EmptyThreadIndicator.test.js.snap @@ -2,10 +2,10 @@ exports[`EmptyThreadIndicator rendering matches the snapshot when rendering all three types of indicators 1`] = `
`; diff --git a/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap b/src/test/components/__snapshots__/Timeline.test.js.snap similarity index 70% rename from src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap rename to src/test/components/__snapshots__/Timeline.test.js.snap index ff4c21c834..adcb6b2c8a 100644 --- a/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap +++ b/src/test/components/__snapshots__/Timeline.test.js.snap @@ -1,19 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`calltree/ProfileViewerHeader renders the header 1`] = ` +exports[`Timeline renders the header 1`] = `
  1. 0.000s
  2. 0.002s
  3. 0.004s
  4. 0.006s
  5. 0.008s @@ -85,7 +85,7 @@ exports[`calltree/ProfileViewerHeader renders the header 1`] = `
  1. Main Thread

  2. Thread with dropped samples

  3. Thread with dropped samples

    `; -exports[`calltree/ProfileViewerHeader renders the header 2`] = ` +exports[`Timeline renders the header 2`] = ` Array [ Array [ "set fillStyle", diff --git a/src/test/components/__snapshots__/ProfileThreadTracingMarkerOverview.test.js.snap b/src/test/components/__snapshots__/TimelineTracingMarkersOverview.test.js.snap similarity index 81% rename from src/test/components/__snapshots__/ProfileThreadTracingMarkerOverview.test.js.snap rename to src/test/components/__snapshots__/TimelineTracingMarkersOverview.test.js.snap index 0a40de17c8..43c58b3dcc 100644 --- a/src/test/components/__snapshots__/ProfileThreadTracingMarkerOverview.test.js.snap +++ b/src/test/components/__snapshots__/TimelineTracingMarkersOverview.test.js.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ProfileThreadTracingMarkerOverview renders correctly 1`] = ` +exports[`TimelineTracingMarkersOverview renders correctly 1`] = `
    `; -exports[`ProfileThreadTracingMarkerOverview renders correctly 2`] = ` +exports[`TimelineTracingMarkersOverview renders correctly 2`] = ` Array [ Array [ "clearRect",