From 1c8d0e5c3c8d956866c18a3eac606a8462760cbd Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Tue, 18 Apr 2017 10:49:03 -0500 Subject: [PATCH 1/7] Add a basic marker view implementation --- src/common/types/profile-derived.js | 11 + src/content/actions/timeline.js | 9 +- src/content/actions/types.js | 3 +- .../components/TimelineMarkerCanvas.css | 9 + .../components/TimelineMarkerCanvas.js | 331 ++++++++++++++++++ src/content/components/TimelineViewport.js | 17 +- src/content/containers/ProfileViewerHeader.js | 4 +- src/content/containers/TimelineFlameChart.js | 10 +- src/content/containers/TimelineMarkers.css | 53 +++ src/content/containers/TimelineMarkers.js | 172 +++++++++ src/content/containers/TimelineView.css | 10 +- src/content/containers/TimelineView.js | 20 +- src/content/marker-timing.js | 59 ++++ src/content/reducers/profile-view.js | 7 + src/content/reducers/timeline-view.js | 28 +- src/test/store/actions.js | 37 +- 16 files changed, 751 insertions(+), 29 deletions(-) create mode 100644 src/content/components/TimelineMarkerCanvas.css create mode 100644 src/content/components/TimelineMarkerCanvas.js create mode 100644 src/content/containers/TimelineMarkers.css create mode 100644 src/content/containers/TimelineMarkers.js create mode 100644 src/content/marker-timing.js diff --git a/src/common/types/profile-derived.js b/src/common/types/profile-derived.js index 074efb6f15..8b432b9684 100644 --- a/src/common/types/profile-derived.js +++ b/src/common/types/profile-derived.js @@ -31,3 +31,14 @@ export type Node = { dim: boolean, icon: string | null, }; + +export type MarkerTiming = { + // Start time in milliseconds. + start: number[], + // End time in milliseconds. + end: number[], + index: number[], + name: string, + length: number, +}; +export type MarkerTimingRows = Array diff --git a/src/content/actions/timeline.js b/src/content/actions/timeline.js index 17b322d50f..7aa2890552 100644 --- a/src/content/actions/timeline.js +++ b/src/content/actions/timeline.js @@ -18,8 +18,13 @@ export function changeFlameChartLabelingStrategy(getLabel: GetLabel): Action { }; } -export function changeTimelineExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action { - const type = 'CHANGE_TIMELINE_EXPANDED_THREAD'; +export function changeTimelineFlameChartExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action { + const type = 'CHANGE_TIMELINE_FLAME_CHART_EXPANDED_THREAD'; + return { type, threadIndex, isExpanded }; +} + +export function changeTimelineMarkersExpandedThread(threadIndex: ThreadIndex, isExpanded: boolean): Action { + const type = 'CHANGE_TIMELINE_MARKERS_EXPANDED_THREAD'; return { type, threadIndex, isExpanded }; } diff --git a/src/content/actions/types.js b/src/content/actions/types.js index 4337a8033a..c62542c2dc 100644 --- a/src/content/actions/types.js +++ b/src/content/actions/types.js @@ -78,7 +78,8 @@ type ReceiveProfileAction = type TimelineAction = { type: 'CHANGE_FLAME_CHART_COLOR_STRATEGY', getCategory: GetCategory } | { type: 'CHANGE_FLAME_CHART_LABELING_STRATEGY', getLabel: GetLabel } | - { type: 'CHANGE_TIMELINE_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean }; + { type: 'CHANGE_TIMELINE_FLAME_CHART_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean } | + { type: 'CHANGE_TIMELINE_MARKERS_EXPANDED_THREAD', threadIndex: ThreadIndex, isExpanded: boolean }; type URLEnhancerAction = { type: "@@urlenhancer/urlSetupDone" } | diff --git a/src/content/components/TimelineMarkerCanvas.css b/src/content/components/TimelineMarkerCanvas.css new file mode 100644 index 0000000000..35e496c41d --- /dev/null +++ b/src/content/components/TimelineMarkerCanvas.css @@ -0,0 +1,9 @@ +.timelineMarkerCanvas { + position: absolute; + top: 0; + left: 0; +} + +.timelineMarkerCanvas.hover { + cursor: default; +} diff --git a/src/content/components/TimelineMarkerCanvas.js b/src/content/components/TimelineMarkerCanvas.js new file mode 100644 index 0000000000..e7d155dfc2 --- /dev/null +++ b/src/content/components/TimelineMarkerCanvas.js @@ -0,0 +1,331 @@ +// @flow +import React, { Component } from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { timeCode } from '../../common/time-code'; +import withTimelineViewport from './TimelineViewport'; +import classNames from 'classnames'; + +import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange, DevicePixels } from '../../common/types/units'; +import type { TracingMarker, MarkerTimingRows } from '../../common/types/profile-derived'; +import type { Action, ProfileSelection } from '../actions/types'; + +type Props = { + interval: Milliseconds, + rangeStart: Milliseconds, + rangeEnd: Milliseconds, + containerWidth: CssPixels, + containerHeight: CssPixels, + viewportLeft: UnitIntervalOfProfileRange, + viewportRight: UnitIntervalOfProfileRange, + viewportTop: CssPixels, + viewportBottom: CssPixels, + markerTimingRows: MarkerTimingRows, + rowHeight: CssPixels, + markers: TracingMarker[], + updateProfileSelection: ProfileSelection => Action, +}; + +require('./TimelineMarkerCanvas.css'); + +const ROW_HEIGHT = 16; +const TEXT_OFFSET_START = 3; +const TEXT_OFFSET_TOP = 11; +const TWO_PI = Math.PI * 2; +const MARKER_DOT_RADIUS = 0.25; + +class TimelineMarkerCanvas extends Component { + + _requestedAnimationFrame: boolean + _devicePixelRatio: number + _ctx: null|CanvasRenderingContext2D + + props: Props + + state: { + hoveredItem: null | number; + } + + constructor(props: Props) { + super(props); + this._requestedAnimationFrame = false; + this._devicePixelRatio = 1; + this.state = { hoveredItem: null }; + + (this: any).onMouseMove = this.onMouseMove.bind(this); + (this: any).onMouseOut = this.onMouseOut.bind(this); + (this: any).onDoubleClick = this.onDoubleClick.bind(this); + } + + _scheduleDraw() { + if (!this._requestedAnimationFrame) { + this._requestedAnimationFrame = true; + window.requestAnimationFrame(() => { + this._requestedAnimationFrame = false; + if (this.refs.canvas) { + timeCode('TimelineMarkerCanvas render', () => { + this.drawCanvas(); + }); + } + }); + } + } + + shouldComponentUpdate(nextProps: Props) { + return shallowCompare(this, nextProps); + } + + _prepCanvas() { + const {canvas} = this.refs; + const {containerWidth, containerHeight} = this.props; + const {devicePixelRatio} = window; + const pixelWidth: DevicePixels = containerWidth * devicePixelRatio; + const pixelHeight: DevicePixels = containerHeight * devicePixelRatio; + if (!this._ctx) { + this._ctx = canvas.getContext('2d'); + } + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + canvas.style.width = containerWidth + 'px'; + canvas.style.height = containerHeight + 'px'; + this._ctx.scale(this._devicePixelRatio, this._devicePixelRatio); + } + if (this._devicePixelRatio !== devicePixelRatio) { + // Make sure and multiply by the inverse of the previous ratio, as the scaling + // operates off of the previous set scale. + const scale = (1 / this._devicePixelRatio) * devicePixelRatio; + this._ctx.scale(scale, scale); + this._devicePixelRatio = devicePixelRatio; + } + return this._ctx; + } + + drawCanvas() { + const ctx = this._prepCanvas(); + const { + viewportTop, viewportBottom, rowHeight, containerWidth, containerHeight, markerTimingRows, + } = this.props; + // Convert CssPixels to Stack Depth + const startRow = Math.floor(viewportTop / rowHeight); + const endRow = Math.min(Math.ceil(viewportBottom / rowHeight), markerTimingRows.length); + + ctx.clearRect(0, 0, containerWidth, containerHeight); + + this.drawMarkers(ctx, startRow, endRow); + this.drawSeparatorsAndLabels(ctx, startRow, endRow); + } + + drawMarkers(ctx, startRow, endRow) { + const { rangeStart, rangeEnd, containerWidth, markers, + containerHeight, markerTimingRows, rowHeight, + viewportLeft, viewportRight, viewportTop, viewportBottom } = this.props; + const { hoveredItem } = this.state; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; + + // Only draw the stack frames that are vertically within view. + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // Get the timing information for a row of stack frames. + const markerTiming = markerTimingRows[rowIndex]; + + if (!markerTiming) { + continue; + } + + // Decide which samples to actually draw + const timeAtViewportLeft: Milliseconds = rangeStart + rangeLength * viewportLeft; + const timeAtViewportRight: Milliseconds = rangeStart + rangeLength * viewportRight; + + ctx.lineWidth = 1; + for (let i = 0; i < markerTiming.length; i++) { + // Only draw samples that are in bounds. + if (markerTiming.end[i] > timeAtViewportLeft && markerTiming.start[i] < timeAtViewportRight) { + const startTime: UnitIntervalOfProfileRange = (markerTiming.start[i] - rangeStart) / rangeLength; + const endTime: UnitIntervalOfProfileRange = (markerTiming.end[i] - rangeStart) / rangeLength; + + const x: CssPixels = ((startTime - viewportLeft) * containerWidth / viewportLength); + const y: CssPixels = rowIndex * ROW_HEIGHT - viewportTop; + const w: CssPixels = Math.max(10, ((endTime - startTime) * containerWidth / viewportLength)); + const h: CssPixels = ROW_HEIGHT - 1; + + if (w < 2) { + // Skip sending draw calls for sufficiently small boxes. + continue; + } + + const markerIndex = markerTiming.index[i]; + const marker = markers[markerIndex]; + const text = marker.name; + + ctx.fillStyle = hoveredItem === markerIndex ? '#38445F' : '#8296cb'; + + if (w >= h) { + this.drawRoundedRect(ctx, x, y + 1, w, h - 1, 1); + } else { + ctx.beginPath(); + ctx.arc( + x + w / 2, // x + y + h / 2, // y + h * MARKER_DOT_RADIUS, // radius + 0, // arc start + TWO_PI // arc end + ); + ctx.fill(); + } + } + } + } + } + + drawSeparatorsAndLabels(ctx, startRow, endRow) { + const { markerTimingRows, rowHeight, viewportTop, containerWidth } = this.props; + + // Draw separators + ctx.fillStyle = '#eee'; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // Get the timing information for a row of stack frames. + const markerTiming = markerTimingRows[rowIndex]; + const y = (rowIndex + 1) * rowHeight - viewportTop; + ctx.fillRect(0, y, containerWidth, 1); + } + + // Fill in behind text + const gradient = ctx.createLinearGradient(0, 0, 150, 0); + gradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)'); + ctx.fillStyle = gradient; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // Get the timing information for a row of stack frames. + const { name } = markerTimingRows[rowIndex]; + const y = rowIndex * rowHeight - viewportTop; + const textWidth = ctx.measureText(name); + ctx.fillRect(0, y, 150, rowHeight); + } + + // Draw the text + ctx.fillStyle = '#000'; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // Get the timing information for a row of stack frames. + const { name } = markerTimingRows[rowIndex]; + const y = rowIndex * rowHeight - viewportTop; + ctx.fillText(name, 5, y + TEXT_OFFSET_TOP); + } + } + + hitTest(event): number|null { + const { canvas } = this.refs; + if (!canvas) { + return null; + } + + const rect = canvas.getBoundingClientRect(); + const { + rangeStart, rangeEnd, markerTimingRows, viewportLeft, viewportRight, + containerWidth, rowHeight, markers, + } = this.props; + const x: CssPixels = event.pageX - rect.left; + const y: CssPixels = event.pageY - rect.top; + + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; + const unitIntervalTime: UnitIntervalOfProfileRange = viewportLeft + viewportLength * (x / containerWidth); + const time: Milliseconds = rangeStart + unitIntervalTime * rangeLength; + const rowIndex = Math.floor(y / rowHeight); + const minDuration = rangeLength * viewportLength * (rowHeight * 2 * MARKER_DOT_RADIUS / containerWidth); + const markerTiming = markerTimingRows[rowIndex]; + + if (!markerTiming) { + return null; + } + + for (let i = 0; i < markerTiming.length; i++) { + const start = markerTiming.start[i]; + // Ensure that really small markers are hoverable with a minDuration. + const end = Math.max(start + minDuration, markerTiming.end[i]); + if (start < time && end > time) { + return markerTiming.index[i]; + } + } + return null; + } + + onMouseMove(event: SyntheticMouseEvent) { + const hoveredItem = this.hitTest(event); + if (this.state.hoveredItem !== hoveredItem) { + this.setState({ hoveredItem }); + } + } + + onMouseOut() { + if (this.state.hoveredItem !== null) { + this.setState({ hoveredItem: null }); + } + } + + onDoubleClick() { + const { hoveredItem } = this.state; + if (hoveredItem === null) { + return; + } + const { markers, updateProfileSelection } = this.props; + const marker = markers[hoveredItem]; + updateProfileSelection({ + hasSelection: true, + isModifying: false, + selectionStart: marker.start, + selectionEnd: marker.start + marker.dur, + }); + } + + drawRoundedRect(ctx: CanvasRenderingContext2D, + x: CssPixels, y: CssPixels, width: CssPixels, height: CssPixels, + cornerSize: CssPixels) { + // Cut out c x c -sized squares in the corners. + const c = Math.min(width / 2, Math.min(height / 2, cornerSize)); + const bottom = y + height; + ctx.fillRect(x + c, y, width - 2 * c, c); + ctx.fillRect(x, y + c, width, height - 2 * c); + ctx.fillRect(x + c, bottom - c, width - 2 * c, c); + } + + getHoveredMarkerInfo(): null | string { + const { hoveredItem } = this.state; + if (hoveredItem === null) { + return null; + } + + const { name, dur } = this.props.markers[hoveredItem]; + let duration; + if (dur >= 10) { + duration = dur.toFixed(0); + } else if (dur >= 1) { + duration = dur.toFixed(1); + } else if (dur >= 0.1) { + duration = dur.toFixed(2); + } else { + duration = dur.toFixed(3); + } + return `${name} - ${duration}ms`; + } + + render() { + const { hoveredItem } = this.state; + this._scheduleDraw(); + + const className = classNames({ + timelineMarkerCanvas: true, + hover: hoveredItem !== null, + }); + + return ; + } +} + +export default withTimelineViewport(TimelineMarkerCanvas); diff --git a/src/content/components/TimelineViewport.js b/src/content/components/TimelineViewport.js index bca5cf5172..15615a5d30 100644 --- a/src/content/components/TimelineViewport.js +++ b/src/content/components/TimelineViewport.js @@ -85,7 +85,7 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) constructor(props: Props) { super(props); - + console.log('!!! TimelineViewport constructor'); (this: any)._mouseWheelListener = this._mouseWheelListener.bind(this); (this: any)._mouseDownListener = this._mouseDownListener.bind(this); (this: any)._mouseMoveListener = this._mouseMoveListener.bind(this); @@ -163,6 +163,7 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) componentWillReceiveProps(newProps: Props) { if (this.props.isRowExpanded !== newProps.isRowExpanded) { this.setState(this.getDefaultState(newProps)); + this._setSizeNextFrame(); return; } if ( @@ -175,6 +176,7 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) _setSize() { const rect = this.refs.container.getBoundingClientRect(); + console.log('!!! _setSize - rect.width', rect.width, rect.height, (new Error()).stack); if (this.state.containerWidth !== rect.width || this.state.containerHeight !== rect.height) { this.setState({ containerWidth: rect.width, @@ -318,6 +320,12 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) }); } else { const timeRangeLength = timeRange.end - timeRange.start; + console.log('!!! updateProfileSelection', { + hasSelection: true, + isModifying: false, + selectionStart: timeRange.start + timeRangeLength * newViewportLeft, + selectionEnd: timeRange.start + timeRangeLength * newViewportRight, + }); updateProfileSelection({ hasSelection: true, isModifying: false, @@ -398,6 +406,13 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) const viewportVerticalChanged = newViewportTop !== viewportTop; if (viewportHorizontalChanged) { + console.log('!!! updateProfileSelection2', { + hasSelection: true, + isModifying: false, + selectionStart: timeRange.start + timeRangeLength * newViewportLeft, + selectionEnd: timeRange.start + timeRangeLength * newViewportRight, + }); + updateProfileSelection({ hasSelection: true, isModifying: false, diff --git a/src/content/containers/ProfileViewerHeader.js b/src/content/containers/ProfileViewerHeader.js index 63de081d89..5b389128b3 100644 --- a/src/content/containers/ProfileViewerHeader.js +++ b/src/content/containers/ProfileViewerHeader.js @@ -70,7 +70,7 @@ class ProfileViewerHeader extends PureComponent { }
- { + {/* threadOrder.map(threadIndex => { const threadName = threads[threadIndex].name; const processType = threads[threadIndex].processType; @@ -84,7 +84,7 @@ class ProfileViewerHeader extends PureComponent { onSelect={this._onIntervalMarkerSelect} /> : null) ); }) - } + */}
{ {}, + changeTimelineFlameChartExpandedThread: (number, boolean) => {}, updateProfileSelection: UpdateProfileSelection, viewHeight: CssPixels, getScrollElement: () => HTMLElement, @@ -53,8 +53,8 @@ class TimelineFlameChart extends PureComponent { } toggleThreadCollapse() { - const { changeTimelineExpandedThread, threadIndex, isRowExpanded } = this.props; - changeTimelineExpandedThread(threadIndex, !isRowExpanded); + const { changeTimelineFlameChartExpandedThread, threadIndex, isRowExpanded } = this.props; + changeTimelineFlameChartExpandedThread(threadIndex, !isRowExpanded); } /** @@ -145,7 +145,7 @@ class TimelineFlameChart extends PureComponent { export default connect((state, ownProps) => { const { threadIndex } = ownProps; const threadSelectors = selectorsForThread(threadIndex); - const isRowExpanded = getIsThreadExpanded(state, threadIndex); + const isRowExpanded = getIsFlameChartExpanded(state, threadIndex); const stackTimingByDepth = isRowExpanded ? threadSelectors.getStackTimingByDepthForFlameChart(state) : threadSelectors.getLeafCategoryStackTimingForFlameChart(state); diff --git a/src/content/containers/TimelineMarkers.css b/src/content/containers/TimelineMarkers.css new file mode 100644 index 0000000000..33397b6b84 --- /dev/null +++ b/src/content/containers/TimelineMarkers.css @@ -0,0 +1,53 @@ +.timelineMarkersLabels { + width: 135px; + display: flex; + padding: 9px 0 9px 14px 0; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.timelineMarkersLabels > span { + flex: 1; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.timelineMarkers { + display: flex; + flex-direction: row; +} + +.timelineMarkersCollapseButton { + width: 17px; + height: 22px; + background: transparent; + position: relative; + margin: 0; + padding: 0; + margin: -4px 0px; + border: none; + cursor: pointer; + transition: transform 100ms; +} + +.timelineMarkersCollapseButton.expanded { + transform: rotate(90deg); +} + +.timelineMarkersCollapseButton::after { + content: ""; + width: 0; + height: 0; + border-style: solid; + border-width: 6px 0 6px 8px; + border-color: transparent transparent transparent #888; + background: transparent; + position: absolute; + margin: 0; + padding: 0; + top: 5px; + left: 5px; +} diff --git a/src/content/containers/TimelineMarkers.js b/src/content/containers/TimelineMarkers.js new file mode 100644 index 0000000000..17e70fefe8 --- /dev/null +++ b/src/content/containers/TimelineMarkers.js @@ -0,0 +1,172 @@ +// @flow +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import TimelineMarkerCanvas from '../components/TimelineMarkerCanvas'; +import { selectorsForThread, getDisplayRange, getProfileInterval, getProfileViewOptions } from '../reducers/profile-view'; +import { getCategoryColorStrategy, getLabelingStrategy } from '../reducers/flame-chart'; +import { getAreMarkersExpanded } from '../reducers/timeline-view'; +import actions from '../actions'; +import { getImplementationName } from '../labeling-strategies'; +import classNames from 'classnames'; + +import type { Thread } from '../../common/types/profile'; +import type { TracingMarker, MarkerTimingRows } from '../../common/types/profile-derived'; +import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units'; +import type { StackTimingByDepth } from '../stack-timing'; +import type { GetCategory } from '../color-categories'; +import type { GetLabel } from '../labeling-strategies'; +import type { UpdateProfileSelection } from '../actions/profile-view'; +import type { ProfileSelection } from '../actions/types'; + +require('./TimelineMarkers.css'); + +const ROW_HEIGHT = 16; +const TIMELINE_ROW_HEIGHT = 34; + +type Props = { + thread: Thread, + isRowExpanded: boolean, + maxMarkerRows: number, + stackTimingByDepth: StackTimingByDepth, + isSelected: boolean, + timeRange: { start: Milliseconds, end: Milliseconds }, + threadIndex: number, + interval: Milliseconds, + getCategory: GetCategory, + getLabel: GetLabel, + changeTimelineMarkersExpandedThread: (number, boolean) => {}, + updateProfileSelection: UpdateProfileSelection, + viewHeight: CssPixels, + getScrollElement: () => HTMLElement, + selection: ProfileSelection, + threadName: string, + processDetails: string, + markerTimingRows: MarkerTimingRows, + markers: TracingMarker[], +}; + +class TimelineMarkers extends Component { + + props: Props + + constructor(props) { + super(props); + (this: any).toggleThreadCollapse = this.toggleThreadCollapse.bind(this); + } + + toggleThreadCollapse() { + const { changeTimelineMarkersExpandedThread, threadIndex, isRowExpanded } = this.props; + changeTimelineMarkersExpandedThread(threadIndex, !isRowExpanded); + } + + /** + * Expanding view sizing strategy: + * + * EXACT SIZE: Try and set it exactly to the size of the flame chart. + * SMALL GRAPH: The smallest it can be is 1.5 times the row height, giving a visual cue + * to the user that this row is expanded, even if it's super shallow. + * LARGE GRAPH: If the flame chart is too large, only expand out to most of the + * available space, leaving some margin to show the other rows. + */ + getViewHeight(maxViewportHeight: number): number { + const { viewHeight, isRowExpanded } = this.props; + const exactSize = isRowExpanded ? maxViewportHeight * 1.5 : maxViewportHeight; + const largeGraph = viewHeight - TIMELINE_ROW_HEIGHT * 2; + const smallGraph = TIMELINE_ROW_HEIGHT; + return Math.max(smallGraph, Math.min(exactSize, largeGraph)); + } + + /** + * Determine + */ + getMaximumZoom(): UnitIntervalOfProfileRange { + const { + timeRange: { start, end }, + interval, + } = this.props; + return interval / (end - start); + } + + render() { + const { + thread, isRowExpanded, maxMarkerRows, stackTimingByDepth, isSelected, timeRange, + threadIndex, interval, getCategory, getLabel, markerTimingRows, markers, + updateProfileSelection, selection, threadName, processDetails, getScrollElement, + } = this.props; + + // The viewport needs to know about the height of what it's drawing, calculate + // that here at the top level component. + const maxViewportHeight = maxMarkerRows * ROW_HEIGHT; + const height = this.getViewHeight(maxViewportHeight); + const buttonClass = classNames('timelineMarkersCollapseButton', { + expanded: isRowExpanded, + collapsed: !isRowExpanded, + }); + + return ( +
+
+ {threadName} +
+ { + return prevProps.stackTimingByDepth !== newProps.stackTimingByDepth; + }} + + // TimelineMarkerCanvas props + interval={interval} + thread={thread} + rangeStart={timeRange.start} + rangeEnd={timeRange.end} + markerTimingRows={markerTimingRows} + getCategory={getCategory} + getLabel={getLabel} + maxMarkerRows={maxMarkerRows} + markers={markers} + rowHeight={ROW_HEIGHT} /> +
+ ); + } +} + + + +export default connect((state, ownProps) => { + const { threadIndex } = ownProps; + const threadSelectors = selectorsForThread(threadIndex); + const isRowExpanded = getAreMarkersExpanded(state, threadIndex); + + const thread = threadSelectors.getThread(state); + const markers = threadSelectors.getTracingMarkers(state); + const markerTimingRows = isRowExpanded + ? threadSelectors.getMarkerTiming(state) + : []; + console.log('!!! markerTimingRows', markerTimingRows); + + return { + thread: threadSelectors.getFilteredThreadForFlameChart(state), + isRowExpanded, + markers, + markerTimingRows, + maxMarkerRows: markerTimingRows.length, + isSelected: true, + timeRange: getDisplayRange(state), + interval: getProfileInterval(state), + getCategory: getCategoryColorStrategy(state), + getLabel: isRowExpanded ? getLabelingStrategy(state) : getImplementationName, + threadIndex, + selection: getProfileViewOptions(state).selection, + threadName: threadSelectors.getFriendlyThreadName(state), + processDetails: threadSelectors.getThreadProcessDetails(state), + }; +}, (actions: Object))(TimelineMarkers); diff --git a/src/content/containers/TimelineView.css b/src/content/containers/TimelineView.css index 63a8f6152e..e737f255e9 100644 --- a/src/content/containers/TimelineView.css +++ b/src/content/containers/TimelineView.css @@ -46,6 +46,12 @@ background-color:#d6d6d6; } -.timelineViewRow { - /*padding-bottom: 5px;*/ +.timelineViewDivider { + background-color: #f9f9f9; + border: solid #d6d6d6; + border-width: 1px 0; + height: 25px; + line-height: 25px; + padding-left: 14px; + z-index: 1; } diff --git a/src/content/containers/TimelineView.js b/src/content/containers/TimelineView.js index 6bf09eb5e4..9cdcb24076 100644 --- a/src/content/containers/TimelineView.js +++ b/src/content/containers/TimelineView.js @@ -5,6 +5,7 @@ import { getThreads, getThreadOrder } from '../reducers/profile-view'; import actions from '../actions'; import FlameChartSettings from '../components/FlameChartSettings'; import TimelineFlameChart from './TimelineFlameChart'; +import TimelineMarkers from './TimelineMarkers'; import Reorderable from '../components/Reorderable'; import { withSize } from '../with-size'; @@ -34,16 +35,17 @@ class TimlineViewTimelinesImpl extends PureComponent { render() { const { threads, threadOrder, changeThreadOrder, height } = this.props; - const className = 'timelineViewTimelines'; - return (
{ this.scrollElement = element; }}> +
+ Sample based callstacks +
@@ -55,6 +57,18 @@ class TimlineViewTimelinesImpl extends PureComponent {
))} +
+ Marker Events +
+
+ {threads.map((thread, threadIndex) => ( +
+ +
+ ))} +
); diff --git a/src/content/marker-timing.js b/src/content/marker-timing.js new file mode 100644 index 0000000000..0e66e137da --- /dev/null +++ b/src/content/marker-timing.js @@ -0,0 +1,59 @@ +import type { TracingMarker, MarkerTiming, MarkerTimingRows } from '../common/types/profile-derived'; + +export function getMarkerTiming(tracingMarkers: TracingMarker[]): MarkerTimingRows { + // Each marker type will have it's own timing information, later collapse these into + // a single array. + const markerTimingsMap: Map = new Map(); + + // Go through all of the markers. + for (let tracingMarkerIndex = 0; tracingMarkerIndex < tracingMarkers.length; tracingMarkerIndex++) { + const marker = tracingMarkers[tracingMarkerIndex]; + let markerTimingsByName = markerTimingsMap.get(marker.name); + if (markerTimingsByName === undefined) { + markerTimingsByName = []; + markerTimingsMap.set(marker.name, markerTimingsByName); + } + + // Place the marker in the closest row that is empty. + markerTimingsLoop: for (let i = 0; true; i++) { + // Get or create a row for marker timings. + let markerTimingsRow = markerTimingsByName[i]; + if (!markerTimingsRow) { + markerTimingsRow = { + start: [], + end: [], + index: [], + name: marker.name, + length: 0, + }; + markerTimingsByName.push(markerTimingsRow); + } + + // Search for a spot not already taken up by another marker of this type. + otherMarkerLoop: for (let j = 0; j < markerTimingsRow.length; j++) { + const otherStart = markerTimingsRow.start[j]; + const otherEnd = markerTimingsRow.end[j]; + if (otherStart > marker.start + marker.dur) { + break otherMarkerLoop; + } + if (otherEnd > marker.start) { + continue markerTimingsLoop; + } + } + + // An empty spot was found, fill the values in the table. + markerTimingsRow.start.push(marker.start); + markerTimingsRow.end.push(marker.start + marker.dur); + markerTimingsRow.index.push(tracingMarkerIndex); + markerTimingsRow.length++; + break; + } + } + + // Flatten out the map into an array. + let markerTimingRows = []; + for (const [, value] of markerTimingsMap) { + markerTimingRows = markerTimingRows.concat(value); + } + return markerTimingRows; +} diff --git a/src/content/reducers/profile-view.js b/src/content/reducers/profile-view.js index ade0fc96e1..60169e47e6 100644 --- a/src/content/reducers/profile-view.js +++ b/src/content/reducers/profile-view.js @@ -6,6 +6,7 @@ import * as CallTreeFilters from '../call-tree-filters'; import * as URLState from './url-state'; import * as ProfileData from '../profile-data'; import * as StackTiming from '../stack-timing'; +import * as MarkerTiming from '../marker-timing'; import * as ProfileTree from '../profile-tree'; import * as TaskTracerTools from '../task-tracer'; import { getCategoryColorStrategy } from './flame-chart'; @@ -357,6 +358,7 @@ export type SelectorsForThread = { getRangeFilteredThread: State => Thread, getJankInstances: State => TracingMarker[], getTracingMarkers: State => TracingMarker[], + getMarkerTiming: State => MarkerTiming.MarkerTimingRows, getRangeSelectionFilteredTracingMarkers: State => TracingMarker[], getFilteredThread: State => Thread, getRangeSelectionFilteredThread: State => Thread, @@ -414,6 +416,10 @@ export const selectorsForThread = (threadIndex: ThreadIndex): SelectorsForThread getThread, ProfileData.getTracingMarkers ); + const getMarkerTiming = createSelector( + getTracingMarkers, + MarkerTiming.getMarkerTiming + ); const getRangeSelectionFilteredTracingMarkers = createSelector( getTracingMarkers, getDisplayRange, @@ -568,6 +574,7 @@ export const selectorsForThread = (threadIndex: ThreadIndex): SelectorsForThread getRangeFilteredThread, getJankInstances, getTracingMarkers, + getMarkerTiming, getRangeSelectionFilteredTracingMarkers, getFilteredThread, getRangeSelectionFilteredThread, diff --git a/src/content/reducers/timeline-view.js b/src/content/reducers/timeline-view.js index fbc3183735..e378a64999 100644 --- a/src/content/reducers/timeline-view.js +++ b/src/content/reducers/timeline-view.js @@ -5,13 +5,14 @@ import type { Action } from '../actions/types'; type IsThreadExpandedMap = Map; type TimelineViewState = { - isThreadExpanded: IsThreadExpandedMap, + isFlameChartExpanded: IsThreadExpandedMap, + areMarkersExpanded: IsThreadExpandedMap, hasZoomedViaMousewheel: boolean, } -function isThreadExpanded(state: IsThreadExpandedMap = new Map(), action: Action) { +function isFlameChartExpanded(state: IsThreadExpandedMap = new Map(), action: Action) { switch (action.type) { - case 'CHANGE_TIMELINE_EXPANDED_THREAD': { + case 'CHANGE_TIMELINE_FLAME_CHART_EXPANDED_THREAD': { const newState = new Map(state); // For now only allow one thread to be open at a time, evaluate whether or not do // more than one. @@ -29,6 +30,17 @@ function isThreadExpanded(state: IsThreadExpandedMap = new Map(), action: Action return state; } +function areMarkersExpanded(state: IsThreadExpandedMap = new Map(), action: Action) { + switch (action.type) { + case 'CHANGE_TIMELINE_MARKERS_EXPANDED_THREAD': { + const newState = new Map(state); + newState.set(action.threadIndex, action.isExpanded); + return newState; + } + } + return state; +} + function hasZoomedViaMousewheel(state: boolean = false, action: Action) { switch (action.type) { case 'HAS_ZOOMED_VIA_MOUSEWHEEL': { @@ -38,11 +50,15 @@ function hasZoomedViaMousewheel(state: boolean = false, action: Action) { return state; } -export default combineReducers({ isThreadExpanded, hasZoomedViaMousewheel }); +export default combineReducers({ isFlameChartExpanded, areMarkersExpanded, hasZoomedViaMousewheel }); export const getTimelineView = (state: Object): TimelineViewState => state.timelineView; -export const getIsThreadExpanded = (state: Object, threadIndex: ThreadIndex) => { - return Boolean(getTimelineView(state).isThreadExpanded.get(threadIndex)); +export const getIsFlameChartExpanded = (state: Object, threadIndex: ThreadIndex) => { + return Boolean(getTimelineView(state).isFlameChartExpanded.get(threadIndex)); +}; +export const getAreMarkersExpanded = (state: Object, threadIndex: ThreadIndex) => { + // Default to being expanded by checking if not equal to false. + return getTimelineView(state).areMarkersExpanded.get(threadIndex) !== false; }; export const getHasZoomedViaMousewheel = (state: Object): boolean => { return getTimelineView(state).hasZoomedViaMousewheel; diff --git a/src/test/store/actions.js b/src/test/store/actions.js index c49ce29947..59585b8394 100644 --- a/src/test/store/actions.js +++ b/src/test/store/actions.js @@ -18,7 +18,8 @@ import { } from '../../content/actions/profile-view'; import { changeFlameChartColorStrategy, - changeTimelineExpandedThread, + changeTimelineMarkersExpandedThread, + changeTimelineFlameChartExpandedThread, } from '../../content/actions/timeline'; import { getCategoryByImplementation } from '../../content/color-categories'; @@ -190,28 +191,50 @@ describe('selectors/getLeafCategoryStackTimingForFlameChart', function () { }); }); -describe('actions/changeTimelineExpandedThread', function () { - it('can set one timeline thread as expanded', function () { +describe('actions/changeTimelineFlameChartExpandedThread', function () { + it('can set one timeline flame chart thread as expanded', function () { const store = storeWithProfile(); const threads = ProfileViewSelectors.getThreads(store.getState()); function isExpanded(thread, threadIndex) { - return TimelineSelectors.getIsThreadExpanded(store.getState(), threadIndex); + return TimelineSelectors.getIsFlameChartExpanded(store.getState(), threadIndex); } assert.deepEqual(threads.map(isExpanded), [false, false, false]); - store.dispatch(changeTimelineExpandedThread(1, true)); + store.dispatch(changeTimelineFlameChartExpandedThread(1, true)); assert.deepEqual(threads.map(isExpanded), [false, true, false]); - store.dispatch(changeTimelineExpandedThread(2, true)); + store.dispatch(changeTimelineFlameChartExpandedThread(2, true)); assert.deepEqual(threads.map(isExpanded), [false, false, true]); - store.dispatch(changeTimelineExpandedThread(2, false)); + store.dispatch(changeTimelineFlameChartExpandedThread(2, false)); assert.deepEqual(threads.map(isExpanded), [false, false, false]); }); }); +describe('actions/changeTimelineMarkersExpandedThread', function () { + it('can set one timeline markers thread as expanded', function () { + const store = storeWithProfile(); + const threads = ProfileViewSelectors.getThreads(store.getState()); + + function isExpanded(thread, threadIndex) { + return TimelineSelectors.getAreMarkersExpanded(store.getState(), threadIndex); + } + // Timeline markers are open by default. + assert.deepEqual(threads.map(isExpanded), [true, true, true]); + + store.dispatch(changeTimelineMarkersExpandedThread(1, false)); + assert.deepEqual(threads.map(isExpanded), [true, false, true]); + + store.dispatch(changeTimelineMarkersExpandedThread(2, false)); + assert.deepEqual(threads.map(isExpanded), [true, false, false]); + + store.dispatch(changeTimelineMarkersExpandedThread(2, true)); + assert.deepEqual(threads.map(isExpanded), [true, false, true]); + }); +}); + describe('actions/changeImplementationFilter', function () { const store = storeWithProfile(); From 0974e45700dc6b4cd13023bfb85c12938ffa555b Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Thu, 27 Apr 2017 10:23:15 -0500 Subject: [PATCH 2/7] Pull out timeline canvas logic into separate component --- src/common/types/profile-derived.js | 2 + src/common/types/units.js | 5 + ...ineMarkerCanvas.css => TimelineCanvas.css} | 4 +- src/content/components/TimelineCanvas.js | 146 ++++++++++++++ .../components/TimelineMarkerCanvas.js | 179 +++++------------- src/content/containers/ProfileViewerHeader.js | 2 +- src/content/containers/TimelineMarkers.js | 8 +- 7 files changed, 205 insertions(+), 141 deletions(-) rename src/content/components/{TimelineMarkerCanvas.css => TimelineCanvas.css} (55%) create mode 100644 src/content/components/TimelineCanvas.js diff --git a/src/common/types/profile-derived.js b/src/common/types/profile-derived.js index 8b432b9684..34ab73466a 100644 --- a/src/common/types/profile-derived.js +++ b/src/common/types/profile-derived.js @@ -32,6 +32,8 @@ export type Node = { icon: string | null, }; +export type IndexIntoMarkerTiming = number; + export type MarkerTiming = { // Start time in milliseconds. start: number[], diff --git a/src/common/types/units.js b/src/common/types/units.js index bdfe1e9c81..955065cb7d 100644 --- a/src/common/types/units.js +++ b/src/common/types/units.js @@ -29,3 +29,8 @@ export type HorizontalViewport = { } export type StartEndRange = { start: Milliseconds, end: Milliseconds }; + +/** + * This is not really a unit, but doesn't warrant a separate file. + */ +export type NonNull = number | string | () => mixed | Object | Array; diff --git a/src/content/components/TimelineMarkerCanvas.css b/src/content/components/TimelineCanvas.css similarity index 55% rename from src/content/components/TimelineMarkerCanvas.css rename to src/content/components/TimelineCanvas.css index 35e496c41d..94b014b399 100644 --- a/src/content/components/TimelineMarkerCanvas.css +++ b/src/content/components/TimelineCanvas.css @@ -1,9 +1,9 @@ -.timelineMarkerCanvas { +.timelineCanvas { position: absolute; top: 0; left: 0; } -.timelineMarkerCanvas.hover { +.timelineCanvas.hover { cursor: default; } diff --git a/src/content/components/TimelineCanvas.js b/src/content/components/TimelineCanvas.js new file mode 100644 index 0000000000..bb024fe885 --- /dev/null +++ b/src/content/components/TimelineCanvas.js @@ -0,0 +1,146 @@ +// @flow +import React, { Component } from 'react'; +import { timeCode } from '../../common/time-code'; +import classNames from 'classnames'; + +import type { CssPixels, DevicePixels, NonNull } from '../../common/types/units'; + +type HoveredItem = NonNull; + +type Props = { + containerWidth: CssPixels, + containerHeight: CssPixels, + className: string, + onDoubleClickItem: HoveredItem => void, + getHoveredItemInfo: HoveredItem => string, + drawCanvas: (CanvasRenderingContext2D, HoveredItem) => void, + hitTest: (x: CssPixels, y: CssPixels) => null | HoveredItem, +}; + +require('./TimelineCanvas.css'); + +export default class TimelineCanvas extends Component { + + props: Props + _requestedAnimationFrame: boolean + _devicePixelRatio: 1 + _ctx: CanvasRenderingContext2D + state: { + hoveredItem: null | HoveredItem; + } + + constructor(props: Props) { + super(props); + this._requestedAnimationFrame = false; + this._devicePixelRatio = 1; + this.state = { hoveredItem: null }; + + (this: any).onMouseMove = this.onMouseMove.bind(this); + (this: any).onMouseOut = this.onMouseOut.bind(this); + (this: any).onDoubleClick = this.onDoubleClick.bind(this); + (this: any).getHoveredItemInfo = this.getHoveredItemInfo.bind(this); + } + + shouldComponentUpdate() { + // If the parent updates, always re-render. + return true; + } + + _scheduleDraw() { + const { className, drawCanvas } = this.props; + if (!this._requestedAnimationFrame) { + this._requestedAnimationFrame = true; + window.requestAnimationFrame(() => { + this._requestedAnimationFrame = false; + if (this.refs.canvas) { + timeCode(`${className} render`, () => { + this._prepCanvas(); + drawCanvas(this._ctx, this.state.hoveredItem); + }); + } + }); + } + } + + _prepCanvas() { + const {canvas} = this.refs; + const {containerWidth, containerHeight} = this.props; + const {devicePixelRatio} = window; + const pixelWidth: DevicePixels = containerWidth * devicePixelRatio; + const pixelHeight: DevicePixels = containerHeight * devicePixelRatio; + if (!this._ctx) { + this._ctx = canvas.getContext('2d'); + } + if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { + canvas.width = pixelWidth; + canvas.height = pixelHeight; + canvas.style.width = containerWidth + 'px'; + canvas.style.height = containerHeight + 'px'; + this._ctx.scale(this._devicePixelRatio, this._devicePixelRatio); + } + if (this._devicePixelRatio !== devicePixelRatio) { + // Make sure and multiply by the inverse of the previous ratio, as the scaling + // operates off of the previous set scale. + const scale = (1 / this._devicePixelRatio) * devicePixelRatio; + this._ctx.scale(scale, scale); + this._devicePixelRatio = devicePixelRatio; + } + return this._ctx; + } + + onMouseMove(event: SyntheticMouseEvent) { + const { canvas } = this.refs; + if (!canvas) { + return; + } + + const rect = canvas.getBoundingClientRect(); + const x: CssPixels = event.pageX - rect.left; + const y: CssPixels = event.pageY - rect.top; + + const maybeHoveredItem = this.props.hitTest(x, y); + if (maybeHoveredItem !== this.state.hoveredItem) { + this.setState({ hoveredItem: maybeHoveredItem }); + } + } + + onMouseOut() { + if (this.state.hoveredItem !== null) { + this.setState({ hoveredItem: null }); + } + } + + onDoubleClick() { + const { hoveredItem } = this.state; + if (hoveredItem === null) { + return; + } + this.props.onDoubleClickItem(hoveredItem); + } + + getHoveredItemInfo(): null | string { + const { hoveredItem } = this.state; + if (hoveredItem === null) { + return null; + } + return this.props.getHoveredItemInfo(hoveredItem); + } + + render() { + const { hoveredItem } = this.state; + this._scheduleDraw(); + + const className = classNames({ + timelineCanvas: true, + [this.props.className]: true, + hover: hoveredItem !== null, + }); + + return ; + } +} diff --git a/src/content/components/TimelineMarkerCanvas.js b/src/content/components/TimelineMarkerCanvas.js index e7d155dfc2..5be6b5a31b 100644 --- a/src/content/components/TimelineMarkerCanvas.js +++ b/src/content/components/TimelineMarkerCanvas.js @@ -1,12 +1,10 @@ // @flow -import React, { Component } from 'react'; -import shallowCompare from 'react-addons-shallow-compare'; -import { timeCode } from '../../common/time-code'; +import React, { PureComponent } from 'react'; import withTimelineViewport from './TimelineViewport'; -import classNames from 'classnames'; +import TimelineCanvas from './TimelineCanvas'; -import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange, DevicePixels } from '../../common/types/units'; -import type { TracingMarker, MarkerTimingRows } from '../../common/types/profile-derived'; +import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units'; +import type { TracingMarker, MarkerTimingRows, IndexIntoMarkerTiming } from '../../common/types/profile-derived'; import type { Action, ProfileSelection } from '../actions/types'; type Props = { @@ -25,15 +23,12 @@ type Props = { updateProfileSelection: ProfileSelection => Action, }; -require('./TimelineMarkerCanvas.css'); - const ROW_HEIGHT = 16; -const TEXT_OFFSET_START = 3; const TEXT_OFFSET_TOP = 11; const TWO_PI = Math.PI * 2; const MARKER_DOT_RADIUS = 0.25; -class TimelineMarkerCanvas extends Component { +class TimelineMarkerCanvas extends PureComponent { _requestedAnimationFrame: boolean _devicePixelRatio: number @@ -47,61 +42,13 @@ class TimelineMarkerCanvas extends Component { constructor(props: Props) { super(props); - this._requestedAnimationFrame = false; - this._devicePixelRatio = 1; - this.state = { hoveredItem: null }; - - (this: any).onMouseMove = this.onMouseMove.bind(this); - (this: any).onMouseOut = this.onMouseOut.bind(this); - (this: any).onDoubleClick = this.onDoubleClick.bind(this); - } - - _scheduleDraw() { - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - if (this.refs.canvas) { - timeCode('TimelineMarkerCanvas render', () => { - this.drawCanvas(); - }); - } - }); - } - } - - shouldComponentUpdate(nextProps: Props) { - return shallowCompare(this, nextProps); + (this: any).onDoubleClickMarker = this.onDoubleClickMarker.bind(this); + (this: any).getHoveredMarkerInfo = this.getHoveredMarkerInfo.bind(this); + (this: any).drawCanvas = this.drawCanvas.bind(this); + (this: any).hitTest = this.hitTest.bind(this); } - _prepCanvas() { - const {canvas} = this.refs; - const {containerWidth, containerHeight} = this.props; - const {devicePixelRatio} = window; - const pixelWidth: DevicePixels = containerWidth * devicePixelRatio; - const pixelHeight: DevicePixels = containerHeight * devicePixelRatio; - if (!this._ctx) { - this._ctx = canvas.getContext('2d'); - } - if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { - canvas.width = pixelWidth; - canvas.height = pixelHeight; - canvas.style.width = containerWidth + 'px'; - canvas.style.height = containerHeight + 'px'; - this._ctx.scale(this._devicePixelRatio, this._devicePixelRatio); - } - if (this._devicePixelRatio !== devicePixelRatio) { - // Make sure and multiply by the inverse of the previous ratio, as the scaling - // operates off of the previous set scale. - const scale = (1 / this._devicePixelRatio) * devicePixelRatio; - this._ctx.scale(scale, scale); - this._devicePixelRatio = devicePixelRatio; - } - return this._ctx; - } - - drawCanvas() { - const ctx = this._prepCanvas(); + drawCanvas(ctx: CanvasRenderingContext2D, hoveredItem: IndexIntoMarkerTiming) { const { viewportTop, viewportBottom, rowHeight, containerWidth, containerHeight, markerTimingRows, } = this.props; @@ -111,15 +58,20 @@ class TimelineMarkerCanvas extends Component { ctx.clearRect(0, 0, containerWidth, containerHeight); - this.drawMarkers(ctx, startRow, endRow); + this.drawMarkers(ctx, hoveredItem, startRow, endRow); this.drawSeparatorsAndLabels(ctx, startRow, endRow); } - drawMarkers(ctx, startRow, endRow) { - const { rangeStart, rangeEnd, containerWidth, markers, - containerHeight, markerTimingRows, rowHeight, - viewportLeft, viewportRight, viewportTop, viewportBottom } = this.props; - const { hoveredItem } = this.state; + drawMarkers( + ctx: CanvasRenderingContext2D, + hoveredItem: IndexIntoMarkerTiming, + startRow: number, + endRow: number + ) { + const { + rangeStart, rangeEnd, containerWidth, markerTimingRows, viewportLeft, + viewportRight, viewportTop, + } = this.props; const rangeLength: Milliseconds = rangeEnd - rangeStart; const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; @@ -155,9 +107,6 @@ class TimelineMarkerCanvas extends Component { } const markerIndex = markerTiming.index[i]; - const marker = markers[markerIndex]; - const text = marker.name; - ctx.fillStyle = hoveredItem === markerIndex ? '#38445F' : '#8296cb'; if (w >= h) { @@ -178,14 +127,12 @@ class TimelineMarkerCanvas extends Component { } } - drawSeparatorsAndLabels(ctx, startRow, endRow) { + drawSeparatorsAndLabels(ctx: CanvasRenderingContext2D, startRow: number, endRow: number) { const { markerTimingRows, rowHeight, viewportTop, containerWidth } = this.props; // Draw separators ctx.fillStyle = '#eee'; for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { - // Get the timing information for a row of stack frames. - const markerTiming = markerTimingRows[rowIndex]; const y = (rowIndex + 1) * rowHeight - viewportTop; ctx.fillRect(0, y, containerWidth, 1); } @@ -197,9 +144,7 @@ class TimelineMarkerCanvas extends Component { ctx.fillStyle = gradient; for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { // Get the timing information for a row of stack frames. - const { name } = markerTimingRows[rowIndex]; const y = rowIndex * rowHeight - viewportTop; - const textWidth = ctx.measureText(name); ctx.fillRect(0, y, 150, rowHeight); } @@ -213,20 +158,11 @@ class TimelineMarkerCanvas extends Component { } } - hitTest(event): number|null { - const { canvas } = this.refs; - if (!canvas) { - return null; - } - - const rect = canvas.getBoundingClientRect(); + hitTest(x: CssPixels, y: CssPixels): IndexIntoMarkerTiming | null { const { - rangeStart, rangeEnd, markerTimingRows, viewportLeft, viewportRight, - containerWidth, rowHeight, markers, - } = this.props; - const x: CssPixels = event.pageX - rect.left; - const y: CssPixels = event.pageY - rect.top; - + rangeStart, rangeEnd, markerTimingRows, viewportLeft, viewportRight, + containerWidth, rowHeight, + } = this.props; const rangeLength: Milliseconds = rangeEnd - rangeStart; const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; @@ -251,26 +187,9 @@ class TimelineMarkerCanvas extends Component { return null; } - onMouseMove(event: SyntheticMouseEvent) { - const hoveredItem = this.hitTest(event); - if (this.state.hoveredItem !== hoveredItem) { - this.setState({ hoveredItem }); - } - } - - onMouseOut() { - if (this.state.hoveredItem !== null) { - this.setState({ hoveredItem: null }); - } - } - - onDoubleClick() { - const { hoveredItem } = this.state; - if (hoveredItem === null) { - return; - } + onDoubleClickMarker(markerIndex: IndexIntoMarkerTiming) { const { markers, updateProfileSelection } = this.props; - const marker = markers[hoveredItem]; + const marker = markers[markerIndex]; updateProfileSelection({ hasSelection: true, isModifying: false, @@ -279,9 +198,14 @@ class TimelineMarkerCanvas extends Component { }); } - drawRoundedRect(ctx: CanvasRenderingContext2D, - x: CssPixels, y: CssPixels, width: CssPixels, height: CssPixels, - cornerSize: CssPixels) { + drawRoundedRect( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + width: CssPixels, + height: CssPixels, + cornerSize: CssPixels + ) { // Cut out c x c -sized squares in the corners. const c = Math.min(width / 2, Math.min(height / 2, cornerSize)); const bottom = y + height; @@ -290,12 +214,7 @@ class TimelineMarkerCanvas extends Component { ctx.fillRect(x + c, bottom - c, width - 2 * c, c); } - getHoveredMarkerInfo(): null | string { - const { hoveredItem } = this.state; - if (hoveredItem === null) { - return null; - } - + getHoveredMarkerInfo(hoveredItem: IndexIntoMarkerTiming): string { const { name, dur } = this.props.markers[hoveredItem]; let duration; if (dur >= 10) { @@ -311,20 +230,16 @@ class TimelineMarkerCanvas extends Component { } render() { - const { hoveredItem } = this.state; - this._scheduleDraw(); - - const className = classNames({ - timelineMarkerCanvas: true, - hover: hoveredItem !== null, - }); - - return ; + const { containerWidth, containerHeight } = this.props; + + return ; } } diff --git a/src/content/containers/ProfileViewerHeader.js b/src/content/containers/ProfileViewerHeader.js index 5b389128b3..8af544ac63 100644 --- a/src/content/containers/ProfileViewerHeader.js +++ b/src/content/containers/ProfileViewerHeader.js @@ -3,7 +3,7 @@ import ProfileThreadHeaderBar from '../components/ProfileThreadHeaderBar'; import Reorderable from '../components/Reorderable'; import TimeSelectionScrubber from '../components/TimeSelectionScrubber'; import ProfileThreadJankOverview from './ProfileThreadJankOverview'; -import ProfileThreadTracingMarkerOverview from './ProfileThreadTracingMarkerOverview'; +// import ProfileThreadTracingMarkerOverview from './ProfileThreadTracingMarkerOverview'; import OverflowEdgeIndicator from '../components/OverflowEdgeIndicator'; import { connect } from 'react-redux'; import { getProfile, getProfileViewOptions, getThreadOrder, getDisplayRange, getZeroAt } from '../reducers/profile-view'; diff --git a/src/content/containers/TimelineMarkers.js b/src/content/containers/TimelineMarkers.js index 17e70fefe8..6308a4d56c 100644 --- a/src/content/containers/TimelineMarkers.js +++ b/src/content/containers/TimelineMarkers.js @@ -12,7 +12,6 @@ import classNames from 'classnames'; import type { Thread } from '../../common/types/profile'; import type { TracingMarker, MarkerTimingRows } from '../../common/types/profile-derived'; import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units'; -import type { StackTimingByDepth } from '../stack-timing'; import type { GetCategory } from '../color-categories'; import type { GetLabel } from '../labeling-strategies'; import type { UpdateProfileSelection } from '../actions/profile-view'; @@ -27,7 +26,6 @@ type Props = { thread: Thread, isRowExpanded: boolean, maxMarkerRows: number, - stackTimingByDepth: StackTimingByDepth, isSelected: boolean, timeRange: { start: Milliseconds, end: Milliseconds }, threadIndex: number, @@ -89,7 +87,7 @@ class TimelineMarkers extends Component { render() { const { - thread, isRowExpanded, maxMarkerRows, stackTimingByDepth, isSelected, timeRange, + thread, isRowExpanded, maxMarkerRows, isSelected, timeRange, threadIndex, interval, getCategory, getLabel, markerTimingRows, markers, updateProfileSelection, selection, threadName, processDetails, getScrollElement, } = this.props; @@ -120,7 +118,7 @@ class TimelineMarkers extends Component { selection={selection} updateProfileSelection={updateProfileSelection} viewportNeedsUpdate={(prevProps, newProps) => { - return prevProps.stackTimingByDepth !== newProps.stackTimingByDepth; + return prevProps.markerTimingRows !== newProps.markerTimingRows; }} // TimelineMarkerCanvas props @@ -146,12 +144,10 @@ export default connect((state, ownProps) => { const threadSelectors = selectorsForThread(threadIndex); const isRowExpanded = getAreMarkersExpanded(state, threadIndex); - const thread = threadSelectors.getThread(state); const markers = threadSelectors.getTracingMarkers(state); const markerTimingRows = isRowExpanded ? threadSelectors.getMarkerTiming(state) : []; - console.log('!!! markerTimingRows', markerTimingRows); return { thread: threadSelectors.getFilteredThreadForFlameChart(state), From 17ed0bbc52e3a34e70d03d01f0a99ab0f52f6a4a Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Thu, 27 Apr 2017 15:56:02 -0500 Subject: [PATCH 3/7] Share TimelineCanvas with FlameChartCanvas --- src/content/components/FlameChartCanvas.js | 165 +++++++++++------- src/content/components/TimelineCanvas.js | 32 +++- .../components/TimelineMarkerCanvas.js | 9 +- src/content/components/TimelineViewport.js | 15 -- src/content/stack-timing.js | 11 +- 5 files changed, 143 insertions(+), 89 deletions(-) diff --git a/src/content/components/FlameChartCanvas.js b/src/content/components/FlameChartCanvas.js index a7f4c22dfb..f1d168baa9 100644 --- a/src/content/components/FlameChartCanvas.js +++ b/src/content/components/FlameChartCanvas.js @@ -1,14 +1,15 @@ // @flow import React, { PureComponent } from 'react'; -import { timeCode } from '../../common/time-code'; import TextMeasurement from '../../common/text-measurement'; import withTimelineViewport from './TimelineViewport'; +import TimelineCanvas from './TimelineCanvas'; import type { Thread } from '../../common/types/profile'; -import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange, DevicePixels } from '../../common/types/units'; -import type { StackTimingByDepth } from '../stack-timing'; +import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units'; +import type { StackTimingByDepth, StackTimingDepth, IndexIntoStackTiming } from '../stack-timing'; import type { GetCategory } from '../color-categories'; import type { GetLabel } from '../labeling-strategies'; +import type { Action, ProfileSelection } from '../actions/types'; type Props = { thread: Thread, @@ -25,6 +26,12 @@ type Props = { stackFrameHeight: CssPixels, getCategory: GetCategory, getLabel: GetLabel, + updateProfileSelection: ProfileSelection => Action, +}; + +type HoveredStackTiming = { + depth: StackTimingDepth, + stackTableIndex: IndexIntoStackTiming, }; require('./FlameChartCanvas.css'); @@ -35,62 +42,16 @@ const TEXT_OFFSET_TOP = 11; class FlameChartCanvas extends PureComponent { - _requestedAnimationFrame: boolean - _devicePixelRatio: number - _textMeasurement: null|TextMeasurement - _ctx: null|CanvasRenderingContext2D + _textMeasurement: null | TextMeasurement props: Props constructor(props: Props) { super(props); - this._requestedAnimationFrame = false; - this._devicePixelRatio = 1; - this._textMeasurement = null; - } - - _scheduleDraw() { - if (!this._requestedAnimationFrame) { - this._requestedAnimationFrame = true; - window.requestAnimationFrame(() => { - this._requestedAnimationFrame = false; - if (this.refs.canvas) { - timeCode('FlameChartCanvas render', () => { - this.drawCanvas(); - }); - } - }); - } - } - - componentDidMount() { - this._textMeasurement = new TextMeasurement(this.refs.canvas.getContext('2d')); - } - - _prepCanvas() { - const {canvas} = this.refs; - const {containerWidth, containerHeight} = this.props; - const {devicePixelRatio} = window; - const pixelWidth: DevicePixels = containerWidth * devicePixelRatio; - const pixelHeight: DevicePixels = containerHeight * devicePixelRatio; - if (!this._ctx) { - this._ctx = canvas.getContext('2d'); - } - if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) { - canvas.width = pixelWidth; - canvas.height = pixelHeight; - canvas.style.width = containerWidth + 'px'; - canvas.style.height = containerHeight + 'px'; - this._ctx.scale(this._devicePixelRatio, this._devicePixelRatio); - } - if (this._devicePixelRatio !== devicePixelRatio) { - // Make sure and multiply by the inverse of the previous ratio, as the scaling - // operates off of the previous set scale. - const scale = (1 / this._devicePixelRatio) * devicePixelRatio; - this._ctx.scale(scale, scale); - this._devicePixelRatio = devicePixelRatio; - } - return this._ctx; + (this: any).onDoubleClickStack = this.onDoubleClickStack.bind(this); + (this: any).getHoveredStackInfo = this.getHoveredStackInfo.bind(this); + (this: any).drawCanvas = this.drawCanvas.bind(this); + (this: any).hitTest = this.hitTest.bind(this); } /** @@ -100,15 +61,22 @@ class FlameChartCanvas extends PureComponent { * 0 - 1. This was done to make the calculations easier for computing various zoomed * and translated views independent of any particular scale. See TimelineViewport.js * for a diagram detailing the various components of this set-up. - * @param {HTMLCanvasElement} canvas - The current canvas. - * @returns {undefined} */ - drawCanvas() { + drawCanvas( + ctx: CanvasRenderingContext2D, + hoveredItem: HoveredStackTiming | null + ) { const { thread, rangeStart, rangeEnd, containerWidth, getLabel, containerHeight, stackTimingByDepth, stackFrameHeight, getCategory, viewportLeft, viewportRight, viewportTop, viewportBottom } = this.props; - const ctx = this._prepCanvas(); + // Ensure the text measurement tool is created, since this is the first time + // this class has access to a ctx. + if (!this._textMeasurement) { + this._textMeasurement = new TextMeasurement(ctx); + } + const textMeasurement = this._textMeasurement; + ctx.clearRect(0, 0, containerWidth, containerHeight); const rangeLength: Milliseconds = rangeEnd - rangeStart; @@ -158,8 +126,9 @@ class FlameChartCanvas extends PureComponent { const frameIndex = thread.stackTable.frame[stackIndex]; const text = getLabel(thread, stackIndex); const category = getCategory(thread, frameIndex); + const isHovered = hoveredItem && depth === hoveredItem.depth && i === hoveredItem.stackTableIndex; - ctx.fillStyle = category.color; + ctx.fillStyle = isHovered ? 'Highlight' : category.color; ctx.fillRect(x, y, w, h); // Ensure spacing between blocks. ctx.clearRect(x, y, 1, h); @@ -169,10 +138,10 @@ class FlameChartCanvas extends PureComponent { const x2: CssPixels = Math.max(x, 0) + TEXT_OFFSET_START; const w2: CssPixels = Math.max(0, w - (x2 - x)); - if (this._textMeasurement !== null && w2 > this._textMeasurement.minWidth) { - const fittedText = this._textMeasurement.getFittedText(text, w2); + if (w2 > textMeasurement.minWidth) { + const fittedText = textMeasurement.getFittedText(text, w2); if (fittedText) { - ctx.fillStyle = 'rgb(0, 0, 0)'; + ctx.fillStyle = isHovered ? 'HighlightText' : '#000000'; ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); } } @@ -181,9 +150,77 @@ class FlameChartCanvas extends PureComponent { } } + getHoveredStackInfo( + {depth, stackTableIndex}: HoveredStackTiming + ): string { + const { thread, getLabel, stackTimingByDepth } = this.props; + const label = getLabel(thread, stackTimingByDepth[depth].stack[stackTableIndex]); + + const duration = stackTimingByDepth[depth].end[stackTableIndex] - + stackTimingByDepth[depth].start[stackTableIndex]; + let durationString; + if (duration >= 10) { + durationString = duration.toFixed(0); + } else if (duration >= 1) { + durationString = duration.toFixed(1); + } else if (duration >= 0.1) { + durationString = duration.toFixed(2); + } else { + durationString = duration.toFixed(3); + } + + return `${durationString}ms - ${label}`; + } + + onDoubleClickStack({depth, stackTableIndex}: HoveredStackTiming) { + const { stackTimingByDepth, updateProfileSelection } = this.props; + updateProfileSelection({ + hasSelection: true, + isModifying: false, + selectionStart: stackTimingByDepth[depth].start[stackTableIndex], + selectionEnd: stackTimingByDepth[depth].end[stackTableIndex], + }); + } + + hitTest(x: CssPixels, y: CssPixels): HoveredStackTiming | null { + const { + rangeStart, rangeEnd, viewportLeft, viewportRight, viewportTop, + containerWidth, stackTimingByDepth, + } = this.props; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; + const unitIntervalTime: UnitIntervalOfProfileRange = viewportLeft + viewportLength * (x / containerWidth); + const time: Milliseconds = rangeStart + unitIntervalTime * rangeLength; + const depth = Math.floor((y + viewportTop) / ROW_HEIGHT); + const stackTiming = stackTimingByDepth[depth]; + + if (!stackTiming) { + return null; + } + + for (let i = 0; i < stackTiming.length; i++) { + const start = stackTiming.start[i]; + const end = stackTiming.end[i]; + if (start < time && end > time) { + return { depth, stackTableIndex: i }; + } + } + + return null; + } + + render() { - this._scheduleDraw(); - return ; + const { containerWidth, containerHeight } = this.props; + + return ; } } diff --git a/src/content/components/TimelineCanvas.js b/src/content/components/TimelineCanvas.js index bb024fe885..a70924b13f 100644 --- a/src/content/components/TimelineCanvas.js +++ b/src/content/components/TimelineCanvas.js @@ -99,7 +99,7 @@ export default class TimelineCanvas extends Component { const y: CssPixels = event.pageY - rect.top; const maybeHoveredItem = this.props.hitTest(x, y); - if (maybeHoveredItem !== this.state.hoveredItem) { + if (!hoveredItemsAreEqual(maybeHoveredItem, this.state.hoveredItem)) { this.setState({ hoveredItem: maybeHoveredItem }); } } @@ -144,3 +144,33 @@ export default class TimelineCanvas extends Component { title={this.getHoveredItemInfo()} />; } } + +/** + * Check for shallow equality for objects, and strict equality for everything else. + */ +function hoveredItemsAreEqual(a: any, b: any) { + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.length !== b.length) { + return false; + } + let hasAllKeys = true; + for (const aKey in a) { + let hasKey = false; + for (const bKey in b) { + if (aKey === bKey) { + if (a[aKey] !== b[bKey]) { + return false; + } + hasKey = true; + break; + } + } + hasAllKeys = hasAllKeys && hasKey; + if (!hasAllKeys) { + return false; + } + } + return true; + } + return a === b; +} diff --git a/src/content/components/TimelineMarkerCanvas.js b/src/content/components/TimelineMarkerCanvas.js index 5be6b5a31b..c010249a6f 100644 --- a/src/content/components/TimelineMarkerCanvas.js +++ b/src/content/components/TimelineMarkerCanvas.js @@ -107,7 +107,7 @@ class TimelineMarkerCanvas extends PureComponent { } const markerIndex = markerTiming.index[i]; - ctx.fillStyle = hoveredItem === markerIndex ? '#38445F' : '#8296cb'; + ctx.fillStyle = hoveredItem === markerIndex ? 'Highlight' : '#8296cb'; if (w >= h) { this.drawRoundedRect(ctx, x, y + 1, w, h - 1, 1); @@ -160,7 +160,7 @@ class TimelineMarkerCanvas extends PureComponent { hitTest(x: CssPixels, y: CssPixels): IndexIntoMarkerTiming | null { const { - rangeStart, rangeEnd, markerTimingRows, viewportLeft, viewportRight, + rangeStart, rangeEnd, markerTimingRows, viewportLeft, viewportRight, viewportTop, containerWidth, rowHeight, } = this.props; @@ -168,7 +168,7 @@ class TimelineMarkerCanvas extends PureComponent { const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; const unitIntervalTime: UnitIntervalOfProfileRange = viewportLeft + viewportLength * (x / containerWidth); const time: Milliseconds = rangeStart + unitIntervalTime * rangeLength; - const rowIndex = Math.floor(y / rowHeight); + const rowIndex = Math.floor((y + viewportTop) / rowHeight); const minDuration = rangeLength * viewportLength * (rowHeight * 2 * MARKER_DOT_RADIUS / containerWidth); const markerTiming = markerTimingRows[rowIndex]; @@ -232,8 +232,7 @@ class TimelineMarkerCanvas extends PureComponent { render() { const { containerWidth, containerHeight } = this.props; - return (WrappedComponent: ReactClass) constructor(props: Props) { super(props); - console.log('!!! TimelineViewport constructor'); (this: any)._mouseWheelListener = this._mouseWheelListener.bind(this); (this: any)._mouseDownListener = this._mouseDownListener.bind(this); (this: any)._mouseMoveListener = this._mouseMoveListener.bind(this); @@ -176,7 +175,6 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) _setSize() { const rect = this.refs.container.getBoundingClientRect(); - console.log('!!! _setSize - rect.width', rect.width, rect.height, (new Error()).stack); if (this.state.containerWidth !== rect.width || this.state.containerHeight !== rect.height) { this.setState({ containerWidth: rect.width, @@ -320,12 +318,6 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) }); } else { const timeRangeLength = timeRange.end - timeRange.start; - console.log('!!! updateProfileSelection', { - hasSelection: true, - isModifying: false, - selectionStart: timeRange.start + timeRangeLength * newViewportLeft, - selectionEnd: timeRange.start + timeRangeLength * newViewportRight, - }); updateProfileSelection({ hasSelection: true, isModifying: false, @@ -406,13 +398,6 @@ export default function withTimelineViewport(WrappedComponent: ReactClass) const viewportVerticalChanged = newViewportTop !== viewportTop; if (viewportHorizontalChanged) { - console.log('!!! updateProfileSelection2', { - hasSelection: true, - isModifying: false, - selectionStart: timeRange.start + timeRangeLength * newViewportLeft, - selectionEnd: timeRange.start + timeRangeLength * newViewportRight, - }); - updateProfileSelection({ hasSelection: true, isModifying: false, diff --git a/src/content/stack-timing.js b/src/content/stack-timing.js index 84720196a1..34e1976000 100644 --- a/src/content/stack-timing.js +++ b/src/content/stack-timing.js @@ -1,5 +1,6 @@ // @flow import type { IndexIntoStackTable, IndexIntoFrameTable, Thread, StackTable } from '../common/types/profile'; +import type { Milliseconds } from '../common/types/units'; import type { FuncStackInfo } from '../common/types/profile-derived'; import type { GetCategory } from './color-categories'; /** @@ -36,11 +37,13 @@ import type { GetCategory } from './color-categories'; * {start: [25, 45], end: [35, 55], stack: [123, 159]} * ] */ + +export type StackTimingDepth = number; +export type IndexIntoStackTiming = number; + export type StackTimingByDepth = Array<{ - // Start time of stack in milliseconds. - start: number[], - // End time of stack in milliseconds. - end: number[], + start: Milliseconds[], + end: Milliseconds[], stack: IndexIntoStackTable[], length: number, }> From be19a43723db607fd98d8b647e69f4c711a96626 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Wed, 3 May 2017 11:17:00 -0500 Subject: [PATCH 4/7] Label user timings --- src/common/types/profile-derived.js | 7 +++- src/common/types/profile.js | 30 ++++++++++++---- .../components/TimelineMarkerCanvas.js | 35 +++++++++++++++++-- src/content/marker-timing.js | 27 ++++++++++++-- src/content/profile-data.js | 4 +++ src/content/reducers/profile-view.js | 9 +++-- src/test/unit/profile-data.js | 31 +++++++--------- 7 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/common/types/profile-derived.js b/src/common/types/profile-derived.js index 34ab73466a..cd9ef6bca7 100644 --- a/src/common/types/profile-derived.js +++ b/src/common/types/profile-derived.js @@ -1,5 +1,6 @@ // @flow import type { Milliseconds } from './units'; +import type { MarkerPayload } from './profile'; export type IndexIntoFuncStackTable = number; @@ -20,8 +21,11 @@ export type TracingMarker = { dur: Milliseconds, name: string, title: string|null, + data: MarkerPayload, }; +export type IndexIntoTracingMarkers = number; + export type Node = { totalTime: string, totalTimePercent: string, @@ -39,7 +43,8 @@ export type MarkerTiming = { start: number[], // End time in milliseconds. end: number[], - index: number[], + index: IndexIntoTracingMarkers[], + label: string[], name: string, length: number, }; diff --git a/src/common/types/profile.js b/src/common/types/profile.js index 87d78ab435..9203394cbc 100644 --- a/src/common/types/profile.js +++ b/src/common/types/profile.js @@ -97,6 +97,28 @@ export type ProfilerMarkerTracing = { // TODO - Add more markers. ); +/** + * The payload for the UserTimings API. These are added through performance.measure() + * and performance.mark(). https://developer.mozilla.org/en-US/docs/Web/API/Performance + */ +export type UserTimingMarkerPayload = { + type: "UserTiming", + startTime: Milliseconds, + endTime: Milliseconds, + name: string, + entryType: "measure" | "mark", +} + +/** + * The union of all the different marker payloads that perf.html knows about, this is + * not guaranteed to be all the payloads that we actually get from the profiler. + */ +export type MarkerPayload = + GPUMarkerPayload | + ProfilerMarkerTracing | + UserTimingMarkerPayload | + null; + /** * Markers represent arbitrary events that happen within the browser. They have a * name, time, and potentially a JSON data payload. These can come from all over the @@ -105,13 +127,7 @@ export type ProfilerMarkerTracing = { * perf.html to instrument their code. */ export type MarkersTable = { - data: ( - GPUMarkerPayload | - ProfilerMarkerTracing | - Object | - null | - void - )[], + data: MarkerPayload[], name: IndexIntoStringTable[], time: number[], length: number, diff --git a/src/content/components/TimelineMarkerCanvas.js b/src/content/components/TimelineMarkerCanvas.js index c010249a6f..b9748fd2c1 100644 --- a/src/content/components/TimelineMarkerCanvas.js +++ b/src/content/components/TimelineMarkerCanvas.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import withTimelineViewport from './TimelineViewport'; import TimelineCanvas from './TimelineCanvas'; +import TextMeasurement from '../../common/text-measurement'; import type { Milliseconds, CssPixels, UnitIntervalOfProfileRange } from '../../common/types/units'; import type { TracingMarker, MarkerTimingRows, IndexIntoMarkerTiming } from '../../common/types/profile-derived'; @@ -27,12 +28,14 @@ const ROW_HEIGHT = 16; const TEXT_OFFSET_TOP = 11; const TWO_PI = Math.PI * 2; const MARKER_DOT_RADIUS = 0.25; +const TEXT_OFFSET_START = 3; class TimelineMarkerCanvas extends PureComponent { _requestedAnimationFrame: boolean _devicePixelRatio: number _ctx: null|CanvasRenderingContext2D + _textMeasurement: null | TextMeasurement props: Props @@ -73,6 +76,13 @@ class TimelineMarkerCanvas extends PureComponent { viewportRight, viewportTop, } = this.props; + // Ensure the text measurement tool is created, since this is the first time + // this class has access to a ctx. + if (!this._textMeasurement) { + this._textMeasurement = new TextMeasurement(ctx); + } + const textMeasurement = this._textMeasurement; + const rangeLength: Milliseconds = rangeEnd - rangeStart; const viewportLength: UnitIntervalOfProfileRange = viewportRight - viewportLeft; @@ -106,11 +116,27 @@ class TimelineMarkerCanvas extends PureComponent { continue; } - const markerIndex = markerTiming.index[i]; - ctx.fillStyle = hoveredItem === markerIndex ? 'Highlight' : '#8296cb'; + const tracingMarkerIndex = markerTiming.index[i]; + const isHovered = hoveredItem === tracingMarkerIndex; + ctx.fillStyle = isHovered ? 'Highlight' : '#8296cb'; if (w >= h) { this.drawRoundedRect(ctx, x, y + 1, w, h - 1, 1); + + const text = markerTiming.label[i]; + // Draw the text label + // TODO - L10N RTL. + // Constrain the x coordinate to the leftmost area. + const x2: CssPixels = Math.max(x, 0) + TEXT_OFFSET_START; + const w2: CssPixels = Math.max(0, w - (x2 - x)); + + if (w2 > textMeasurement.minWidth) { + const fittedText = textMeasurement.getFittedText(text, w2); + if (fittedText) { + ctx.fillStyle = isHovered ? 'HighlightText' : '#ffffff'; + ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); + } + } } else { ctx.beginPath(); ctx.arc( @@ -149,10 +175,13 @@ class TimelineMarkerCanvas extends PureComponent { } // Draw the text - ctx.fillStyle = '#000'; + ctx.fillStyle = '#000000'; for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { // Get the timing information for a row of stack frames. const { name } = markerTimingRows[rowIndex]; + if (rowIndex > 0 && name === markerTimingRows[rowIndex - 1].name) { + continue; + } const y = rowIndex * rowHeight - viewportTop; ctx.fillText(name, 5, y + TEXT_OFFSET_TOP); } diff --git a/src/content/marker-timing.js b/src/content/marker-timing.js index 0e66e137da..a34452ed37 100644 --- a/src/content/marker-timing.js +++ b/src/content/marker-timing.js @@ -1,6 +1,14 @@ -import type { TracingMarker, MarkerTiming, MarkerTimingRows } from '../common/types/profile-derived'; +// @flow +import type { + UserTimingMarkerPayload, MarkerPayload, +} from '../common/types/profile'; +import type { + TracingMarker, MarkerTiming, MarkerTimingRows, +} from '../common/types/profile-derived'; -export function getMarkerTiming(tracingMarkers: TracingMarker[]): MarkerTimingRows { +export function getMarkerTiming( + tracingMarkers: TracingMarker[] +): MarkerTimingRows { // Each marker type will have it's own timing information, later collapse these into // a single array. const markerTimingsMap: Map = new Map(); @@ -23,6 +31,7 @@ export function getMarkerTiming(tracingMarkers: TracingMarker[]): MarkerTimingRo start: [], end: [], index: [], + label: [], name: marker.name, length: 0, }; @@ -44,6 +53,7 @@ export function getMarkerTiming(tracingMarkers: TracingMarker[]): MarkerTimingRo // An empty spot was found, fill the values in the table. markerTimingsRow.start.push(marker.start); markerTimingsRow.end.push(marker.start + marker.dur); + markerTimingsRow.label.push(computeMarkerLabel(marker.data)); markerTimingsRow.index.push(tracingMarkerIndex); markerTimingsRow.length++; break; @@ -57,3 +67,16 @@ export function getMarkerTiming(tracingMarkers: TracingMarker[]): MarkerTimingRo } return markerTimingRows; } + +function computeMarkerLabel(data: MarkerPayload): string { + // Satisfy flow's type checker. + if (data !== null && typeof data === 'object') { + // Handle different marker payloads. + switch (data.type) { + case 'UserTiming': + return (data: UserTimingMarkerPayload).name; + } + } + + return ''; +} diff --git a/src/content/profile-data.js b/src/content/profile-data.js index d075ced167..76a1ce5f4d 100644 --- a/src/content/profile-data.js +++ b/src/content/profile-data.js @@ -698,6 +698,7 @@ export function getJankInstances(samples: SamplesTable, processType: string, thr dur: lastResponsiveness, title: `${lastResponsiveness.toFixed(2)}ms event processing delay on ${processType} thread`, name: 'Jank', + data: null, }); } } @@ -710,6 +711,7 @@ export function getJankInstances(samples: SamplesTable, processType: string, thr dur: lastResponsiveness, title: `${lastResponsiveness.toFixed(2)}ms event processing delay on ${processType} thread`, name: 'Jank', + data: null, }); } return jankInstances; @@ -733,6 +735,7 @@ export function getTracingMarkers(thread: Thread): TracingMarker[] { name: stringTable.getString(nameStringIndex), dur: 0, title: null, + data, }); } else if (data.interval === 'end') { const marker = openMarkers.get(nameStringIndex); @@ -757,6 +760,7 @@ export function getTracingMarkers(thread: Thread): TracingMarker[] { start: startTime, dur: duration, name, + data, title: `${name} for ${duration.toFixed(2)}ms`, }); } diff --git a/src/content/reducers/profile-view.js b/src/content/reducers/profile-view.js index 60169e47e6..37c2c3594b 100644 --- a/src/content/reducers/profile-view.js +++ b/src/content/reducers/profile-view.js @@ -19,7 +19,12 @@ import type { SamplesTable, TaskTracer, } from '../../common/types/profile'; -import type { TracingMarker, FuncStackInfo, IndexIntoFuncStackTable } from '../../common/types/profile-derived'; +import type { + TracingMarker, + FuncStackInfo, + IndexIntoFuncStackTable, + MarkerTimingRows, +} from '../../common/types/profile-derived'; import type { Milliseconds, StartEndRange } from '../../common/types/units'; import type { Action, CallTreeFilter, ProfileSelection } from '../actions/types'; import type { @@ -358,7 +363,7 @@ export type SelectorsForThread = { getRangeFilteredThread: State => Thread, getJankInstances: State => TracingMarker[], getTracingMarkers: State => TracingMarker[], - getMarkerTiming: State => MarkerTiming.MarkerTimingRows, + getMarkerTiming: State => MarkerTimingRows, getRangeSelectionFilteredTracingMarkers: State => TracingMarker[], getFilteredThread: State => Thread, getRangeSelectionFilteredThread: State => Thread, diff --git a/src/test/unit/profile-data.js b/src/test/unit/profile-data.js index e592baaf48..7db767e0f0 100644 --- a/src/test/unit/profile-data.js +++ b/src/test/unit/profile-data.js @@ -218,32 +218,27 @@ describe('profile-data', function () { const profile = processProfile(exampleProfile); const thread = profile.threads[0]; const tracingMarkers = getTracingMarkers(thread); + console.log(tracingMarkers); it('should fold the two reflow markers into one tracing marker', function () { assert.equal(tracingMarkers.length, 3); - assert.deepEqual(tracingMarkers[0], { - start: 2, - name: 'Reflow', - dur: 6, - title: 'Reflow for 6.00ms', - }); + assert.equal(tracingMarkers[0].start, 2); + assert.equal(tracingMarkers[0].name, 'Reflow'); + assert.equal(tracingMarkers[0].dur, 6); + assert.equal(tracingMarkers[0].title, 'Reflow for 6.00ms'); }); it('should fold the two Rasterize markers into one tracing marker, after the reflow tracing marker', function () { assert.equal(tracingMarkers.length, 3); - assert.deepEqual(tracingMarkers[1], { - start: 4, - name: 'Rasterize', - dur: 1, - title: 'Rasterize for 1.00ms', - }); + assert.equal(tracingMarkers[1].start, 4); + assert.equal(tracingMarkers[1].name, 'Rasterize'); + assert.equal(tracingMarkers[1].dur, 1); + assert.equal(tracingMarkers[1].title, 'Rasterize for 1.00ms'); }); it('should create a tracing marker for the MinorGC startTime/endTime marker', function () { assert.equal(tracingMarkers.length, 3); - assert.deepEqual(tracingMarkers[2], { - start: 11, - name: 'MinorGC', - dur: 1, - title: 'MinorGC for 1.00ms', - }); + assert.equal(tracingMarkers[2].start, 11); + assert.equal(tracingMarkers[2].name, 'MinorGC'); + assert.equal(tracingMarkers[2].dur, 1); + assert.equal(tracingMarkers[2].title, 'MinorGC for 1.00ms'); }); }); }); From 716ec497cc39399db2942dede521386663f2a53e Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Mon, 1 May 2017 10:17:28 -0500 Subject: [PATCH 5/7] Add timeline marker tests --- src/content/marker-timing.js | 54 ++++++++++++++++++--- src/test/store/actions.js | 74 ++++++++++++++++++++++++++++- src/test/store/fixtures/profiles.js | 31 +++++++++++- 3 files changed, 150 insertions(+), 9 deletions(-) diff --git a/src/content/marker-timing.js b/src/content/marker-timing.js index a34452ed37..4b14641c64 100644 --- a/src/content/marker-timing.js +++ b/src/content/marker-timing.js @@ -1,11 +1,53 @@ // @flow -import type { - UserTimingMarkerPayload, MarkerPayload, -} from '../common/types/profile'; -import type { - TracingMarker, MarkerTiming, MarkerTimingRows, -} from '../common/types/profile-derived'; +import type { UserTimingMarkerPayload, MarkerPayload } from '../common/types/profile'; +import type { TracingMarker, MarkerTiming, MarkerTimingRows } from '../common/types/profile-derived'; +/** + * This function computes the timing information for laying out the markers in the + * TimelineMarkers component. Each marker is put into a single row based on its name. + * + * e.g. An array of 15 markers named either "A", "B", or "C" would be translated into + * something that looks like: + * + * [ + * { + * name: "A", + * start: [0, 23, 35, 65, 75], + * end: [1, 25, 37, 67, 77], + * index: [0, 2, 5, 6, 8], + * label: ["Aye", "Aye", "Aye", "Aye", "Aye"] + * } + * { + * name: "B", + * start: [1, 28, 39, 69, 70], + * end: [2, 29, 49, 70, 77], + * index: [1, 3, 7, 9, 10], + * label: ["Bee", "Bee", "Bee", "Bee", "Bee"] + * } + * { + * name: "C", + * start: [10, 33, 45, 75, 85], + * end: [11, 35, 47, 77, 87], + * index: [4, 11, 12, 13, 14], + * label: ["Sea", "Sea", "Sea", "Sea", "Sea"] + * } + * ] + * + * If a marker of a name has timings that overlap in a single row, then it is broken + * out into multiple rows, with the overlapping timings going in the next rows. The + * getMarkerTiming tests show the behavior of how this works in practice. + * + * This structure allows the markers to easily be laid out like this example below: + * ____________________________________________ + * | GC | *--* *--* *--* | + * | | | + * | Scripts | *---------------------* | + * | | | + * | User Timings | *----------------* | + * | User Timings | *------------* | + * | User Timings | *--* *---* | + * |______________|_____________________________| + */ export function getMarkerTiming( tracingMarkers: TracingMarker[] ): MarkerTimingRows { diff --git a/src/test/store/actions.js b/src/test/store/actions.js index 59585b8394..e2c59cc676 100644 --- a/src/test/store/actions.js +++ b/src/test/store/actions.js @@ -3,7 +3,7 @@ import { storeWithProfile } from '../fixtures/stores'; import * as ProfileViewSelectors from '../../content/reducers/profile-view'; import * as TimelineSelectors from '../../content/reducers/timeline-view'; import * as UrlStateSelectors from '../../content/reducers/url-state'; -import { getProfileWithNamedThreads } from './fixtures/profiles'; +import { getProfileWithNamedThreads, getProfileWithMarkers } from './fixtures/profiles'; import { changeCallTreeSearchString, @@ -340,3 +340,75 @@ describe('thread ordering and toggling', function () { }); }); }); + +describe('selectors/getMarkerTiming', function () { + function getMarkerTiming(testMarkers) { + const profile = getProfileWithMarkers(testMarkers); + const { getState } = storeWithProfile(profile); + return selectedThreadSelectors.getMarkerTiming(getState()); + } + + it('has no marker timing if no markers are present', function () { + assert.deepEqual(getMarkerTiming([]), []); + }); + + describe('markers of the same name', function () { + it('puts markers of the same time in two rows', function () { + // The timing should look like this: + // 'Marker Name': *------* + // : *------* + const markerTiming = getMarkerTiming([ + ['Marker Name', 0, {startTime: 0, endTime: 10}], + ['Marker Name', 0, {startTime: 0, endTime: 10}], + ]); + assert.lengthOf(markerTiming, 2); + }); + + it('puts markers of disjoint times in one row', function () { + // The timing should look like this: + // 'Marker Name': *------* *------* + const markerTiming = getMarkerTiming([ + ['Marker Name', 0, {startTime: 0, endTime: 10}], + ['Marker Name', 0, {startTime: 15, endTime: 25}], + ]); + assert.lengthOf(markerTiming, 1); + }); + + it('puts markers of overlapping times in two rows', function () { + // The timing should look like this: + // 'Marker Name': *------* + // : *------* + const markerTiming = getMarkerTiming([ + ['Marker Name', 0, {startTime: 0, endTime: 10}], + ['Marker Name', 0, {startTime: 5, endTime: 15}], + ]); + assert.lengthOf(markerTiming, 2); + }); + + it('puts markers of inclusive overlapping times in two rows', function () { + // The timing should look like this: + // 'Marker Name': *--------* + // : *---* + const markerTiming = getMarkerTiming([ + ['Marker Name', 0, {startTime: 0, endTime: 20}], + ['Marker Name', 0, {startTime: 5, endTime: 15}], + ]); + assert.lengthOf(markerTiming, 2); + }); + }); + + describe('markers of the different names', function () { + it('puts them in different rows', function () { + // The timing should look like this: + // 'Marker Name A': *------* + // 'Marker Name B': *------* + const markerTiming = getMarkerTiming([ + ['Marker Name A', 0, {startTime: 0, endTime: 10}], + ['Marker Name B', 0, {startTime: 20, endTime: 30}], + ]); + assert.lengthOf(markerTiming, 2); + assert.equal(markerTiming[0].name, 'Marker Name A'); + assert.equal(markerTiming[1].name, 'Marker Name B'); + }); + }); +}); diff --git a/src/test/store/fixtures/profiles.js b/src/test/store/fixtures/profiles.js index 09129e21d5..c691f56b13 100644 --- a/src/test/store/fixtures/profiles.js +++ b/src/test/store/fixtures/profiles.js @@ -1,7 +1,34 @@ // @flow import { getEmptyProfile } from '../../../content/profile-data'; import { UniqueStringArray } from '../../../content/unique-string-array'; -import type { Profile, Thread } from '../../../common/types/profile'; +import type { Profile, Thread, MarkersTable } from '../../../common/types/profile'; +import type { Milliseconds } from '../../../common/types/units'; + +// Array<[MarkerName, Milliseconds, Data]> +type MarkerName = string; +type MarkerTime = Milliseconds; +type DataPayload = Object; +type TestDefinedMarkers = Array<[MarkerName, MarkerTime, DataPayload]>; + +export function getProfileWithMarkers(markers: TestDefinedMarkers): Profile { + const profile = getEmptyProfile(); + const thread = getEmptyThread(); + const stringTable = thread.stringTable; + const markersTable: MarkersTable = { + name: [], + time: [], + data: [], + length: 0, + }; + markers.map(([name, time, data]) => { + markersTable.name.push(stringTable.indexForString(name)); + markersTable.time.push(time); + markersTable.data.push(data); + markersTable.length++; + }); + profile.threads.push(Object.assign({}, thread, { markers: markersTable })); + return profile; +} export function getProfileWithNamedThreads(threadNames: string[]): Profile { const profile = getEmptyProfile(); @@ -9,7 +36,7 @@ export function getProfileWithNamedThreads(threadNames: string[]): Profile { return profile; } -export function getEmptyThread(overrides: Object): Thread { +export function getEmptyThread(overrides: ?Object): Thread { return Object.assign({ processType: 'default', name: 'Empty', From ec7e264a13976f0a0fb1d8d2605efad8513e1f12 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Thu, 4 May 2017 14:30:33 -0500 Subject: [PATCH 6/7] Avoid label statements --- src/content/marker-timing.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/content/marker-timing.js b/src/content/marker-timing.js index 4b14641c64..1db17032fd 100644 --- a/src/content/marker-timing.js +++ b/src/content/marker-timing.js @@ -2,6 +2,9 @@ import type { UserTimingMarkerPayload, MarkerPayload } from '../common/types/profile'; import type { TracingMarker, MarkerTiming, MarkerTimingRows } from '../common/types/profile-derived'; +// Arbitrarily set an upper limit for adding marker depths, avoiding an infinite loop. +const MAX_STACKING_DEPTH = 300; + /** * This function computes the timing information for laying out the markers in the * TimelineMarkers component. Each marker is put into a single row based on its name. @@ -65,7 +68,7 @@ export function getMarkerTiming( } // Place the marker in the closest row that is empty. - markerTimingsLoop: for (let i = 0; true; i++) { + for (let i = 0; i < MAX_STACKING_DEPTH; i++) { // Get or create a row for marker timings. let markerTimingsRow = markerTimingsByName[i]; if (!markerTimingsRow) { @@ -80,25 +83,30 @@ export function getMarkerTiming( markerTimingsByName.push(markerTimingsRow); } + let continueSearching = false; + // Search for a spot not already taken up by another marker of this type. - otherMarkerLoop: for (let j = 0; j < markerTimingsRow.length; j++) { + for (let j = 0; j < markerTimingsRow.length; j++) { const otherStart = markerTimingsRow.start[j]; const otherEnd = markerTimingsRow.end[j]; if (otherStart > marker.start + marker.dur) { - break otherMarkerLoop; + break; } if (otherEnd > marker.start) { - continue markerTimingsLoop; + continueSearching = true; + break; } } - // An empty spot was found, fill the values in the table. - markerTimingsRow.start.push(marker.start); - markerTimingsRow.end.push(marker.start + marker.dur); - markerTimingsRow.label.push(computeMarkerLabel(marker.data)); - markerTimingsRow.index.push(tracingMarkerIndex); - markerTimingsRow.length++; - break; + if (!continueSearching) { + // An empty spot was found, fill the values in the table. + markerTimingsRow.start.push(marker.start); + markerTimingsRow.end.push(marker.start + marker.dur); + markerTimingsRow.label.push(computeMarkerLabel(marker.data)); + markerTimingsRow.index.push(tracingMarkerIndex); + markerTimingsRow.length++; + break; + } } } From e15876f7a658a7dd9f8ca7118fe2df32b1905531 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Mon, 8 May 2017 13:49:10 -0500 Subject: [PATCH 7/7] Address review for Timeline Markers --- src/content/components/FlameChartCanvas.js | 28 +++++++-------- src/content/components/TimelineCanvas.js | 35 ++++++++++--------- .../components/TimelineMarkerCanvas.js | 14 ++++---- src/content/containers/ProfileViewerHeader.js | 6 ++-- src/content/containers/TimelineFlameChart.js | 8 +++-- src/content/containers/TimelineMarkers.css | 23 ++++++------ src/content/containers/TimelineMarkers.js | 19 +++++----- src/content/containers/TimelineView.js | 4 +-- src/content/marker-timing.js | 8 ++--- src/test/unit/profile-data.js | 1 - 10 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/content/components/FlameChartCanvas.js b/src/content/components/FlameChartCanvas.js index f1d168baa9..d2821f0858 100644 --- a/src/content/components/FlameChartCanvas.js +++ b/src/content/components/FlameChartCanvas.js @@ -42,16 +42,16 @@ const TEXT_OFFSET_TOP = 11; class FlameChartCanvas extends PureComponent { - _textMeasurement: null | TextMeasurement + _textMeasurement: null | TextMeasurement; - props: Props + props: Props; constructor(props: Props) { super(props); - (this: any).onDoubleClickStack = this.onDoubleClickStack.bind(this); - (this: any).getHoveredStackInfo = this.getHoveredStackInfo.bind(this); - (this: any).drawCanvas = this.drawCanvas.bind(this); - (this: any).hitTest = this.hitTest.bind(this); + (this: any)._onDoubleClickStack = this._onDoubleClickStack.bind(this); + (this: any)._getHoveredStackInfo = this._getHoveredStackInfo.bind(this); + (this: any)._drawCanvas = this._drawCanvas.bind(this); + (this: any)._hitTest = this._hitTest.bind(this); } /** @@ -62,7 +62,7 @@ class FlameChartCanvas extends PureComponent { * and translated views independent of any particular scale. See TimelineViewport.js * for a diagram detailing the various components of this set-up. */ - drawCanvas( + _drawCanvas( ctx: CanvasRenderingContext2D, hoveredItem: HoveredStackTiming | null ) { @@ -150,7 +150,7 @@ class FlameChartCanvas extends PureComponent { } } - getHoveredStackInfo( + _getHoveredStackInfo( {depth, stackTableIndex}: HoveredStackTiming ): string { const { thread, getLabel, stackTimingByDepth } = this.props; @@ -172,7 +172,7 @@ class FlameChartCanvas extends PureComponent { return `${durationString}ms - ${label}`; } - onDoubleClickStack({depth, stackTableIndex}: HoveredStackTiming) { + _onDoubleClickStack({depth, stackTableIndex}: HoveredStackTiming) { const { stackTimingByDepth, updateProfileSelection } = this.props; updateProfileSelection({ hasSelection: true, @@ -182,7 +182,7 @@ class FlameChartCanvas extends PureComponent { }); } - hitTest(x: CssPixels, y: CssPixels): HoveredStackTiming | null { + _hitTest(x: CssPixels, y: CssPixels): HoveredStackTiming | null { const { rangeStart, rangeEnd, viewportLeft, viewportRight, viewportTop, containerWidth, stackTimingByDepth, @@ -217,10 +217,10 @@ class FlameChartCanvas extends PureComponent { return ; + onDoubleClickItem={this._onDoubleClickStack} + getHoveredItemInfo={this._getHoveredStackInfo} + drawCanvas={this._drawCanvas} + hitTest={this._hitTest} />; } } diff --git a/src/content/components/TimelineCanvas.js b/src/content/components/TimelineCanvas.js index a70924b13f..2d2f2eec49 100644 --- a/src/content/components/TimelineCanvas.js +++ b/src/content/components/TimelineCanvas.js @@ -21,13 +21,13 @@ require('./TimelineCanvas.css'); export default class TimelineCanvas extends Component { - props: Props - _requestedAnimationFrame: boolean - _devicePixelRatio: 1 - _ctx: CanvasRenderingContext2D + props: Props; + _requestedAnimationFrame: boolean; + _devicePixelRatio: 1; + _ctx: CanvasRenderingContext2D; state: { hoveredItem: null | HoveredItem; - } + }; constructor(props: Props) { super(props); @@ -35,10 +35,10 @@ export default class TimelineCanvas extends Component { this._devicePixelRatio = 1; this.state = { hoveredItem: null }; - (this: any).onMouseMove = this.onMouseMove.bind(this); - (this: any).onMouseOut = this.onMouseOut.bind(this); - (this: any).onDoubleClick = this.onDoubleClick.bind(this); - (this: any).getHoveredItemInfo = this.getHoveredItemInfo.bind(this); + (this: any)._onMouseMove = this._onMouseMove.bind(this); + (this: any)._onMouseOut = this._onMouseOut.bind(this); + (this: any)._onDoubleClick = this._onDoubleClick.bind(this); + (this: any)._getHoveredItemInfo = this._getHoveredItemInfo.bind(this); } shouldComponentUpdate() { @@ -68,6 +68,7 @@ export default class TimelineCanvas extends Component { const {devicePixelRatio} = window; const pixelWidth: DevicePixels = containerWidth * devicePixelRatio; const pixelHeight: DevicePixels = containerHeight * devicePixelRatio; + // Satisfy the null check for Flow. if (!this._ctx) { this._ctx = canvas.getContext('2d'); } @@ -88,7 +89,7 @@ export default class TimelineCanvas extends Component { return this._ctx; } - onMouseMove(event: SyntheticMouseEvent) { + _onMouseMove(event: SyntheticMouseEvent) { const { canvas } = this.refs; if (!canvas) { return; @@ -104,13 +105,13 @@ export default class TimelineCanvas extends Component { } } - onMouseOut() { + _onMouseOut() { if (this.state.hoveredItem !== null) { this.setState({ hoveredItem: null }); } } - onDoubleClick() { + _onDoubleClick() { const { hoveredItem } = this.state; if (hoveredItem === null) { return; @@ -118,7 +119,7 @@ export default class TimelineCanvas extends Component { this.props.onDoubleClickItem(hoveredItem); } - getHoveredItemInfo(): null | string { + _getHoveredItemInfo(): null | string { const { hoveredItem } = this.state; if (hoveredItem === null) { return null; @@ -138,10 +139,10 @@ export default class TimelineCanvas extends Component { return ; + onMouseMove={this._onMouseMove} + onMouseOut={this._onMouseOut} + onDoubleClick={this._onDoubleClick} + title={this._getHoveredItemInfo()} />; } } diff --git a/src/content/components/TimelineMarkerCanvas.js b/src/content/components/TimelineMarkerCanvas.js index b9748fd2c1..b3755b147e 100644 --- a/src/content/components/TimelineMarkerCanvas.js +++ b/src/content/components/TimelineMarkerCanvas.js @@ -32,16 +32,16 @@ const TEXT_OFFSET_START = 3; class TimelineMarkerCanvas extends PureComponent { - _requestedAnimationFrame: boolean - _devicePixelRatio: number - _ctx: null|CanvasRenderingContext2D - _textMeasurement: null | TextMeasurement + _requestedAnimationFrame: boolean; + _devicePixelRatio: number; + _ctx: null | CanvasRenderingContext2D; + _textMeasurement: null | TextMeasurement; - props: Props + props: Props; state: { - hoveredItem: null | number; - } + hoveredItem: null | number, + }; constructor(props: Props) { super(props); diff --git a/src/content/containers/ProfileViewerHeader.js b/src/content/containers/ProfileViewerHeader.js index 8af544ac63..63de081d89 100644 --- a/src/content/containers/ProfileViewerHeader.js +++ b/src/content/containers/ProfileViewerHeader.js @@ -3,7 +3,7 @@ import ProfileThreadHeaderBar from '../components/ProfileThreadHeaderBar'; import Reorderable from '../components/Reorderable'; import TimeSelectionScrubber from '../components/TimeSelectionScrubber'; import ProfileThreadJankOverview from './ProfileThreadJankOverview'; -// import ProfileThreadTracingMarkerOverview from './ProfileThreadTracingMarkerOverview'; +import ProfileThreadTracingMarkerOverview from './ProfileThreadTracingMarkerOverview'; import OverflowEdgeIndicator from '../components/OverflowEdgeIndicator'; import { connect } from 'react-redux'; import { getProfile, getProfileViewOptions, getThreadOrder, getDisplayRange, getZeroAt } from '../reducers/profile-view'; @@ -70,7 +70,7 @@ class ProfileViewerHeader extends PureComponent { }
- {/* + { threadOrder.map(threadIndex => { const threadName = threads[threadIndex].name; const processType = threads[threadIndex].processType; @@ -84,7 +84,7 @@ class ProfileViewerHeader extends PureComponent { onSelect={this._onIntervalMarkerSelect} /> : null) ); }) - */} + }
{ { - return prevProps.stackTimingByDepth !== newProps.stackTimingByDepth; - }} + viewportNeedsUpdate={viewportNeedsUpdate} // FlameChartCanvas props interval={interval} @@ -166,3 +164,7 @@ export default connect((state, ownProps) => { processDetails: threadSelectors.getThreadProcessDetails(state), }; }, (actions: Object))(TimelineFlameChart); + +function viewportNeedsUpdate(prevProps, newProps) { + return prevProps.stackTimingByDepth !== newProps.stackTimingByDepth; +} diff --git a/src/content/containers/TimelineMarkers.css b/src/content/containers/TimelineMarkers.css index 33397b6b84..24d9805cba 100644 --- a/src/content/containers/TimelineMarkers.css +++ b/src/content/containers/TimelineMarkers.css @@ -1,13 +1,14 @@ .timelineMarkersLabels { width: 135px; display: flex; - padding: 9px 0 9px 14px 0; + padding: 9px 0 9px 14px; -webkit-user-select: none; -moz-user-select: none; + -ms-user-select: none; user-select: none; } -.timelineMarkersLabels > span { +.timelineMarkersLabelsName { flex: 1; cursor: default; white-space: nowrap; @@ -21,16 +22,16 @@ } .timelineMarkersCollapseButton { + position: relative; width: 17px; height: 22px; - background: transparent; - position: relative; margin: 0; padding: 0; - margin: -4px 0px; border: none; + margin: -4px 0px; cursor: pointer; transition: transform 100ms; + background: transparent; } .timelineMarkersCollapseButton.expanded { @@ -38,16 +39,16 @@ } .timelineMarkersCollapseButton::after { - content: ""; + position: absolute; + top: 5px; + left: 5px; width: 0; height: 0; + margin: 0; + padding: 0; + content: ""; border-style: solid; border-width: 6px 0 6px 8px; border-color: transparent transparent transparent #888; background: transparent; - position: absolute; - margin: 0; - padding: 0; - top: 5px; - left: 5px; } diff --git a/src/content/containers/TimelineMarkers.js b/src/content/containers/TimelineMarkers.js index 6308a4d56c..e942057708 100644 --- a/src/content/containers/TimelineMarkers.js +++ b/src/content/containers/TimelineMarkers.js @@ -17,6 +17,7 @@ import type { GetLabel } from '../labeling-strategies'; import type { UpdateProfileSelection } from '../actions/profile-view'; import type { ProfileSelection } from '../actions/types'; + require('./TimelineMarkers.css'); const ROW_HEIGHT = 16; @@ -45,14 +46,14 @@ type Props = { class TimelineMarkers extends Component { - props: Props + props: Props; constructor(props) { super(props); - (this: any).toggleThreadCollapse = this.toggleThreadCollapse.bind(this); + (this: any)._toggleThreadCollapse = this._toggleThreadCollapse.bind(this); } - toggleThreadCollapse() { + _toggleThreadCollapse() { const { changeTimelineMarkersExpandedThread, threadIndex, isRowExpanded } = this.props; changeTimelineMarkersExpandedThread(threadIndex, !isRowExpanded); } @@ -104,8 +105,8 @@ class TimelineMarkers extends Component { return (
- {threadName} -
{ - return prevProps.markerTimingRows !== newProps.markerTimingRows; - }} - + viewportNeedsUpdate={viewportNeedsUpdate} // TimelineMarkerCanvas props interval={interval} thread={thread} @@ -137,6 +135,9 @@ class TimelineMarkers extends Component { } } +function viewportNeedsUpdate(prevProps, newProps) { + return prevProps.markerTimingRows !== newProps.markerTimingRows; +} export default connect((state, ownProps) => { diff --git a/src/content/containers/TimelineView.js b/src/content/containers/TimelineView.js index 9cdcb24076..7733d3a704 100644 --- a/src/content/containers/TimelineView.js +++ b/src/content/containers/TimelineView.js @@ -45,7 +45,7 @@ class TimlineViewTimelinesImpl extends PureComponent { Sample based callstacks
@@ -60,7 +60,7 @@ class TimlineViewTimelinesImpl extends PureComponent {
Marker Events
-
+
{threads.map((thread, threadIndex) => (